Merge pull request #1 from executeautomation/Support-for-SQL-Server

Added support for SQL Server
This commit is contained in:
ExecuteAutomation
2025-04-14 15:45:33 +12:00
committed by GitHub
14 changed files with 2384 additions and 376 deletions

BIN
.DS_Store vendored

Binary file not shown.

0
data/test.db Normal file
View File

155
docs/sql-server-setup.md Normal file
View File

@@ -0,0 +1,155 @@
# SQL Server Setup Guide
This guide provides instructions for setting up and using the SQL Server adapter with the MCP Database Server.
## Prerequisites
1. Access to a SQL Server instance (2012 or later)
2. Node.js 18 or later
3. Required permissions to connect to the SQL Server database
## Installation
1. Follow the main installation steps in the README.md file
2. Ensure the mssql package is installed:
```bash
npm install mssql
npm install @types/mssql --save-dev
```
## Authentication Options
The SQL Server adapter supports multiple authentication methods:
### SQL Server Authentication
Use the `--user` and `--password` parameters to authenticate with SQL Server credentials:
```bash
node dist/src/index.js --sqlserver --server myserver --database mydatabase --user myuser --password mypassword
```
### Windows Authentication
Omit the `--user` and `--password` parameters to use Windows Authentication (trusted connection):
```bash
node dist/src/index.js --sqlserver --server myserver --database mydatabase
```
### Azure Active Directory
For Azure SQL Database with Azure AD authentication, you'll need to set up connection options:
```json
{
"mcpServers": {
"sqlserver": {
"command": "node",
"args": [
"/path/to/mcp-database-server/dist/src/index.js",
"--sqlserver",
"--server", "myserver.database.windows.net",
"--database", "mydatabase",
"--user", "myuser@mydomain.com",
"--password", "mypassword"
]
}
}
}
```
## Configuring Claude
Update your Claude configuration file to add SQL Server support:
```json
{
"mcpServers": {
"sqlserver": {
"command": "node",
"args": [
"/path/to/mcp-database-server/dist/src/index.js",
"--sqlserver",
"--server", "your-server-name",
"--database", "your-database-name",
"--user", "your-username",
"--password", "your-password"
]
}
}
}
```
For local SQL Server with Windows Authentication:
```json
{
"mcpServers": {
"sqlserver": {
"command": "node",
"args": [
"/path/to/mcp-database-server/dist/src/index.js",
"--sqlserver",
"--server", "localhost\\SQLEXPRESS",
"--database", "your-database-name"
]
}
}
}
```
## Connection Options
Additional connection options include:
- `--port`: Specify a non-default port (default is 1433)
- Add `--trustServerCertificate true` if you're connecting to a development/test server with a self-signed certificate
## Troubleshooting
### Common Connection Issues
1. **Login failed for user**
- Verify username and password
- Check if the SQL Server account is enabled and not locked
2. **Cannot connect to server**
- Ensure SQL Server is running
- Check firewall settings
- Verify server name is correct (including instance name if applicable)
3. **SSL errors**
- Add `--trustServerCertificate true` for development environments
### Verifying Connection
You can test your SQL Server connection using the standard SQL Server tools:
1. Using SQL Server Management Studio (SSMS)
2. Using the `sqlcmd` utility:
```
sqlcmd -S server_name -d database_name -U username -P password
```
## SQL Syntax Differences
Note that there may be syntax differences between SQLite and SQL Server. Here are some common differences:
1. **String concatenation**
- SQLite: `||`
- SQL Server: `+`
2. **Limit/Offset**
- SQLite: `LIMIT x OFFSET y`
- SQL Server: `OFFSET y ROWS FETCH NEXT x ROWS ONLY`
3. **Date formatting**
- SQLite: `strftime()`
- SQL Server: `FORMAT()` or `CONVERT()`
4. **Auto-increment columns**
- SQLite: `INTEGER PRIMARY KEY AUTOINCREMENT`
- SQL Server: `INT IDENTITY(1,1)`
When using Claude, be aware of these syntax differences when crafting SQL queries.

1575
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@executeautomation/database-server",
"version": "1.0.0",
"description": "MCP server for interacting with SQLite databases by ExecuteAutomation",
"description": "MCP server for interacting with SQLite and SQL Server databases by ExecuteAutomation",
"license": "MIT",
"author": "ExecuteAutomation (https://executeautomation.com)",
"homepage": "https://executeautomation.com",
@@ -24,12 +24,14 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.9.0",
"mssql": "11.0.1",
"sqlite3": "5.1.7"
},
"devDependencies": {
"@types/mssql": "^9.1.5",
"@types/sqlite3": "5.1.0",
"rimraf": "^5.0.5",
"shx": "0.4.0",
"typescript": "5.8.3"
}
}
}

279
readme.md
View File

@@ -1,202 +1,115 @@
# ExecuteAutomation Database Server
# MCP Database Server
A Model Context Protocol server that provides access to SQLite databases. This server enables LLMs like Claude to inspect database schemas and execute SQL queries.
This MCP (Model Context Protocol) server provides database access capabilities to Claude, supporting both SQLite and SQL Server databases.
## Components
## Installation
### Tools
The server offers nine core tools:
#### Query Tools
- **read_query**
- Execute SELECT queries to read data from the database
- Input:
- `query` (string): The SELECT SQL query to execute
- Returns: Query results as array of objects
- **write_query**
- Execute INSERT, UPDATE, or DELETE queries
- Input:
- `query` (string): The SQL modification query
- Returns: `{ affected_rows: number }`
#### Schema Management Tools
- **create_table**
- Create new tables in the database
- Input:
- `query` (string): CREATE TABLE SQL statement
- Returns: Confirmation of table creation
- **alter_table**
- Modify existing table schema (add columns, rename tables, etc.)
- Input:
- `query` (string): ALTER TABLE SQL statement
- Returns: Confirmation of table alteration
- **drop_table**
- Remove a table from the database with safety confirmation
- Input:
- `table_name` (string): Name of the table to drop
- `confirm` (boolean): Safety confirmation flag (must be true to actually drop)
- Returns: Confirmation message or safety warning
#### Schema Information Tools
- **list_tables**
- Get a list of all tables in the database
- No input required
- Returns: Array of table names
- **describe_table**
- View schema information for a specific table
- Input:
- `table_name` (string): Name of table to describe
- Returns: Array of column definitions with names and types
#### Data Export Tools
- **export_query**
- Export query results to various formats
- Input:
- `query` (string): The SELECT SQL query to execute
- `format` (string): Output format - either "csv" or "json"
- Returns: Query results formatted as CSV or JSON
#### Insights
- **append_insight**
- Add a business insight to the memo
- Input:
- `insight` (string): Business insight discovered from data analysis
- Returns: Confirmation message
### Resources
The server provides schema information for each table in the database:
- **Table Schemas** (`sqlite:///<path>/<table>/schema`)
- JSON schema information for each table
- Includes column names and data types
- Automatically discovered from database metadata
## Usage with Claude Desktop
To use this server with the Claude Desktop app, add the following configuration to the "mcpServers" section of your `claude_desktop_config.json`:
### Docker
```json
{
"mcpServers": {
"database": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"executeautomation/database-server",
"/path/to/database.db"]
}
}
}
1. Clone the repository:
```
git clone https://github.com/executeautomation/database-server.git
cd database-server
```
### NPX (Published Package)
```json
{
"mcpServers": {
"database": {
"command": "npx",
"args": [
"-y",
"@executeautomation/database-server",
"/path/to/database.db"
]
}
}
}
2. Install dependencies:
```
### Local Development
For local development and debugging, you can point directly to your compiled JavaScript file:
```json
{
"mcpServers": {
"database": {
"command": "node",
"args": [
"/path/to/your/project/dist/index.js",
"/path/to/your/database.db"
]
}
}
}
```
Example with full paths:
```json
{
"mcpServers": {
"database": {
"command": "node",
"args": [
"/Users/username/projects/database-server/dist/index.js",
"/Users/username/databases/mydata.db"
]
}
}
}
```
The Claude Desktop config file is typically located at:
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
- Linux: `~/.config/Claude/claude_desktop_config.json`
Replace all paths with your actual file paths.
## Building and Development
### Quick Build
Use the included build script to install dependencies and build the project:
```sh
chmod +x build.sh
./build.sh
```
### Manual Build Steps
```sh
# Install dependencies
npm install
```
# Build the TypeScript
3. Build the project:
```
npm run build
```
### Docker Build
## Usage
```sh
docker build -t executeautomation/database-server -f Dockerfile .
### SQLite Database
To use with an SQLite database:
```
node dist/src/index.js /path/to/your/database.db
```
## Troubleshooting
### SQL Server Database
If you encounter connection issues:
To use with a SQL Server database:
1. Make sure the database file path is correct and accessible
2. Verify that the compiled JavaScript file exists at the specified path
3. Check the Claude Desktop logs for detailed error messages
4. Restart Claude Desktop after making configuration changes
```
node dist/src/index.js --sqlserver --server <server-name> --database <database-name> [--user <username> --password <password>]
```
Required parameters:
- `--server`: SQL Server host name or IP address
- `--database`: Name of the database
Optional parameters:
- `--user`: Username for SQL Server authentication (if not provided, Windows Authentication will be used)
- `--password`: Password for SQL Server authentication
- `--port`: Port number (default: 1433)
## Configuring Claude
Update your Claude configuration file to add the MCP Database Server:
```json
{
"mcpServers": {
"sqlite": {
"command": "node",
"args": [
"/path/to/mcp-database-server/dist/src/index.js",
"/path/to/your/database.db"
]
},
"sqlserver": {
"command": "node",
"args": [
"/path/to/mcp-database-server/dist/src/index.js",
"--sqlserver",
"--server", "your-server-name",
"--database", "your-database-name",
"--user", "your-username",
"--password", "your-password"
]
}
}
}
```
## Available Tools
The MCP Database Server provides the following tools:
- `read_query`: Execute SELECT queries to read data from the database
- `write_query`: Execute INSERT, UPDATE, or DELETE queries
- `create_table`: Create new tables in the database
- `alter_table`: Modify existing table schema (add columns, rename tables, etc.)
- `drop_table`: Remove a table from the database with safety confirmation
- `export_query`: Export query results to various formats (CSV, JSON)
- `list_tables`: Get a list of all tables in the database
- `describe_table`: View schema information for a specific table
- `append_insight`: Add a business insight to the memo
- `list_insights`: List all business insights in the memo
## Development
To run the server in development mode:
```
npm run dev
```
To watch for changes during development:
```
npm run watch
```
## Requirements
- Node.js 18+
- For SQL Server connectivity: SQL Server 2012 or later
## License
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License.
## About ExecuteAutomation
This server is maintained by ExecuteAutomation. Visit [executeautomation.com](https://executeautomation.com) for more tools and resources.
MIT

BIN
src/.DS_Store vendored

Binary file not shown.

74
src/db/adapter.ts Normal file
View File

@@ -0,0 +1,74 @@
/**
* Database adapter interface
* Defines the contract for all database implementations (SQLite, SQL Server)
*/
export interface DbAdapter {
/**
* Initialize database connection
*/
init(): Promise<void>;
/**
* Close database connection
*/
close(): Promise<void>;
/**
* Execute a query and return all results
* @param query SQL query to execute
* @param params Query parameters
*/
all(query: string, params?: any[]): Promise<any[]>;
/**
* Execute a query that modifies data
* @param query SQL query to execute
* @param params Query parameters
*/
run(query: string, params?: any[]): Promise<{ changes: number, lastID: number }>;
/**
* Execute multiple SQL statements
* @param query SQL statements to execute
*/
exec(query: string): Promise<void>;
/**
* Get database metadata
*/
getMetadata(): { name: string, type: string, path?: string, server?: string, database?: string };
/**
* Get database-specific query for listing tables
*/
getListTablesQuery(): string;
/**
* Get database-specific query for describing a table
* @param tableName Table name
*/
getDescribeTableQuery(tableName: string): string;
}
// Import adapters using dynamic imports
import { SqliteAdapter } from './sqlite-adapter.js';
import { SqlServerAdapter } from './sqlserver-adapter.js';
/**
* Factory function to create the appropriate database adapter
*/
export function createDbAdapter(type: string, connectionInfo: any): DbAdapter {
switch (type.toLowerCase()) {
case 'sqlite':
// For SQLite, if connectionInfo is a string, use it directly as path
if (typeof connectionInfo === 'string') {
return new SqliteAdapter(connectionInfo);
} else {
return new SqliteAdapter(connectionInfo.path);
}
case 'sqlserver':
return new SqlServerAdapter(connectionInfo);
default:
throw new Error(`Unsupported database type: ${type}`);
}
}

View File

@@ -1,23 +1,28 @@
import sqlite3 from "sqlite3";
import { DbAdapter, createDbAdapter } from './adapter.js';
let db: sqlite3.Database;
let databasePath: string;
// Store the active database adapter
let dbAdapter: DbAdapter | null = null;
/**
* Initialize the SQLite database connection
* @param dbPath Path to the SQLite database file
* Initialize the database connection
* @param connectionInfo Connection information object or SQLite path string
* @param dbType Database type ('sqlite' or 'sqlserver')
*/
export function initDatabase(dbPath: string): Promise<void> {
databasePath = dbPath;
return new Promise((resolve, reject) => {
db = new sqlite3.Database(dbPath, (err) => {
if (err) {
reject(err);
} else {
resolve();
export async function initDatabase(connectionInfo: any, dbType: string = 'sqlite'): Promise<void> {
try {
// If connectionInfo is a string, assume it's a SQLite path
if (typeof connectionInfo === 'string') {
connectionInfo = { path: connectionInfo };
}
// Create appropriate adapter based on database type
dbAdapter = createDbAdapter(dbType, connectionInfo);
// Initialize the connection
await dbAdapter.init();
} catch (error) {
throw new Error(`Failed to initialize database: ${(error as Error).message}`);
}
});
});
}
/**
@@ -27,15 +32,10 @@ export function initDatabase(dbPath: string): Promise<void> {
* @returns Promise with query results
*/
export function dbAll(query: string, params: any[] = []): Promise<any[]> {
return new Promise((resolve, reject) => {
db.all(query, params, (err: Error | null, rows: any[]) => {
if (err) {
reject(err);
} else {
resolve(rows);
if (!dbAdapter) {
throw new Error("Database not initialized");
}
});
});
return dbAdapter.all(query, params);
}
/**
@@ -45,15 +45,10 @@ export function dbAll(query: string, params: any[] = []): Promise<any[]> {
* @returns Promise with result info
*/
export function dbRun(query: string, params: any[] = []): Promise<{ changes: number, lastID: number }> {
return new Promise((resolve, reject) => {
db.run(query, params, function(this: sqlite3.RunResult, err: Error | null) {
if (err) {
reject(err);
} else {
resolve({ changes: this.changes, lastID: this.lastID });
if (!dbAdapter) {
throw new Error("Database not initialized");
}
});
});
return dbAdapter.run(query, params);
}
/**
@@ -62,40 +57,49 @@ export function dbRun(query: string, params: any[] = []): Promise<{ changes: num
* @returns Promise that resolves when execution completes
*/
export function dbExec(query: string): Promise<void> {
return new Promise((resolve, reject) => {
db.exec(query, (err: Error | null) => {
if (err) {
reject(err);
} else {
resolve();
if (!dbAdapter) {
throw new Error("Database not initialized");
}
});
});
return dbAdapter.exec(query);
}
/**
* Close the database connection
*/
export function closeDatabase(): Promise<void> {
return new Promise((resolve, reject) => {
if (!db) {
resolve();
return;
if (!dbAdapter) {
return Promise.resolve();
}
db.close((err: Error | null) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
return dbAdapter.close();
}
/**
* Get the current database path
* Get database metadata
*/
export function getDatabasePath(): string {
return databasePath;
export function getDatabaseMetadata(): { name: string, type: string, path?: string, server?: string, database?: string } {
if (!dbAdapter) {
throw new Error("Database not initialized");
}
return dbAdapter.getMetadata();
}
/**
* Get database-specific query for listing tables
*/
export function getListTablesQuery(): string {
if (!dbAdapter) {
throw new Error("Database not initialized");
}
return dbAdapter.getListTablesQuery();
}
/**
* Get database-specific query for describing a table
* @param tableName Table name
*/
export function getDescribeTableQuery(tableName: string): string {
if (!dbAdapter) {
throw new Error("Database not initialized");
}
return dbAdapter.getDescribeTableQuery(tableName);
}

145
src/db/sqlite-adapter.ts Normal file
View File

@@ -0,0 +1,145 @@
import sqlite3 from "sqlite3";
import { DbAdapter } from "./adapter.js";
/**
* SQLite database adapter implementation
*/
export class SqliteAdapter implements DbAdapter {
private db: sqlite3.Database | null = null;
private dbPath: string;
constructor(dbPath: string) {
this.dbPath = dbPath;
}
/**
* Initialize the SQLite database connection
*/
async init(): Promise<void> {
return new Promise((resolve, reject) => {
// Ensure the dbPath is accessible
console.error(`[INFO] Opening SQLite database at: ${this.dbPath}`);
this.db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, (err) => {
if (err) {
console.error(`[ERROR] SQLite connection error: ${err.message}`);
reject(err);
} else {
console.error("[INFO] SQLite database opened successfully");
resolve();
}
});
});
}
/**
* Execute a SQL query and get all results
* @param query SQL query to execute
* @param params Query parameters
* @returns Promise with query results
*/
async all(query: string, params: any[] = []): Promise<any[]> {
if (!this.db) {
throw new Error("Database not initialized");
}
return new Promise((resolve, reject) => {
this.db!.all(query, params, (err: Error | null, rows: any[]) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
});
});
}
/**
* Execute a SQL query that modifies data
* @param query SQL query to execute
* @param params Query parameters
* @returns Promise with result info
*/
async run(query: string, params: any[] = []): Promise<{ changes: number, lastID: number }> {
if (!this.db) {
throw new Error("Database not initialized");
}
return new Promise((resolve, reject) => {
this.db!.run(query, params, function(this: sqlite3.RunResult, err: Error | null) {
if (err) {
reject(err);
} else {
resolve({ changes: this.changes, lastID: this.lastID });
}
});
});
}
/**
* Execute multiple SQL statements
* @param query SQL statements to execute
* @returns Promise that resolves when execution completes
*/
async exec(query: string): Promise<void> {
if (!this.db) {
throw new Error("Database not initialized");
}
return new Promise((resolve, reject) => {
this.db!.exec(query, (err: Error | null) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
/**
* Close the database connection
*/
async close(): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.db) {
resolve();
return;
}
this.db.close((err: Error | null) => {
if (err) {
reject(err);
} else {
this.db = null;
resolve();
}
});
});
}
/**
* Get database metadata
*/
getMetadata(): { name: string, type: string, path: string } {
return {
name: "SQLite",
type: "sqlite",
path: this.dbPath
};
}
/**
* Get database-specific query for listing tables
*/
getListTablesQuery(): string {
return "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'";
}
/**
* Get database-specific query for describing a table
* @param tableName Table name
*/
getDescribeTableQuery(tableName: string): string {
return `PRAGMA table_info(${tableName})`;
}
}

215
src/db/sqlserver-adapter.ts Normal file
View File

@@ -0,0 +1,215 @@
import { DbAdapter } from "./adapter.js";
import sql from 'mssql';
/**
* SQL Server database adapter implementation
*/
export class SqlServerAdapter implements DbAdapter {
private pool: sql.ConnectionPool | null = null;
private config: sql.config;
private server: string;
private database: string;
constructor(connectionInfo: {
server: string;
database: string;
user?: string;
password?: string;
port?: number;
trustServerCertificate?: boolean;
options?: any;
}) {
this.server = connectionInfo.server;
this.database = connectionInfo.database;
// Create SQL Server connection config
this.config = {
server: connectionInfo.server,
database: connectionInfo.database,
port: connectionInfo.port || 1433,
options: {
trustServerCertificate: connectionInfo.trustServerCertificate ?? true,
...connectionInfo.options
}
};
// Add authentication options
if (connectionInfo.user && connectionInfo.password) {
this.config.user = connectionInfo.user;
this.config.password = connectionInfo.password;
} else {
// Use Windows authentication if no username/password provided
this.config.options!.trustedConnection = true;
this.config.options!.enableArithAbort = true;
}
}
/**
* Initialize SQL Server connection
*/
async init(): Promise<void> {
try {
console.error(`[INFO] Connecting to SQL Server: ${this.server}, Database: ${this.database}`);
this.pool = await new sql.ConnectionPool(this.config).connect();
console.error(`[INFO] SQL Server connection established successfully`);
} catch (err) {
console.error(`[ERROR] SQL Server connection error: ${(err as Error).message}`);
throw new Error(`Failed to connect to SQL Server: ${(err as Error).message}`);
}
}
/**
* Execute a SQL query and get all results
* @param query SQL query to execute
* @param params Query parameters
* @returns Promise with query results
*/
async all(query: string, params: any[] = []): Promise<any[]> {
if (!this.pool) {
throw new Error("Database not initialized");
}
try {
const request = this.pool.request();
// Add parameters to the request
params.forEach((param, index) => {
request.input(`param${index}`, param);
});
// Replace ? with named parameters
const preparedQuery = query.replace(/\?/g, (_, i) => `@param${i}`);
const result = await request.query(preparedQuery);
return result.recordset;
} catch (err) {
throw new Error(`SQL Server query error: ${(err as Error).message}`);
}
}
/**
* Execute a SQL query that modifies data
* @param query SQL query to execute
* @param params Query parameters
* @returns Promise with result info
*/
async run(query: string, params: any[] = []): Promise<{ changes: number, lastID: number }> {
if (!this.pool) {
throw new Error("Database not initialized");
}
try {
const request = this.pool.request();
// Add parameters to the request
params.forEach((param, index) => {
request.input(`param${index}`, param);
});
// Replace ? with named parameters
const preparedQuery = query.replace(/\?/g, (_, i) => `@param${i}`);
// Add output parameter for identity value if it's an INSERT
let lastID = 0;
if (query.trim().toUpperCase().startsWith('INSERT')) {
request.output('insertedId', sql.Int, 0);
const updatedQuery = `${preparedQuery}; SELECT @insertedId = SCOPE_IDENTITY();`;
const result = await request.query(updatedQuery);
lastID = result.output.insertedId || 0;
} else {
const result = await request.query(preparedQuery);
lastID = 0;
}
return {
changes: this.getAffectedRows(query, lastID),
lastID: lastID
};
} catch (err) {
throw new Error(`SQL Server query error: ${(err as Error).message}`);
}
}
/**
* Execute multiple SQL statements
* @param query SQL statements to execute
* @returns Promise that resolves when execution completes
*/
async exec(query: string): Promise<void> {
if (!this.pool) {
throw new Error("Database not initialized");
}
try {
const request = this.pool.request();
await request.batch(query);
} catch (err) {
throw new Error(`SQL Server batch error: ${(err as Error).message}`);
}
}
/**
* Close the database connection
*/
async close(): Promise<void> {
if (this.pool) {
await this.pool.close();
this.pool = null;
}
}
/**
* Get database metadata
*/
getMetadata(): { name: string, type: string, server: string, database: string } {
return {
name: "SQL Server",
type: "sqlserver",
server: this.server,
database: this.database
};
}
/**
* Get database-specific query for listing tables
*/
getListTablesQuery(): string {
return "SELECT TABLE_NAME as name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME";
}
/**
* Get database-specific query for describing a table
* @param tableName Table name
*/
getDescribeTableQuery(tableName: string): string {
return `
SELECT
c.COLUMN_NAME as name,
c.DATA_TYPE as type,
CASE WHEN c.IS_NULLABLE = 'YES' THEN 1 ELSE 0 END as notnull,
CASE WHEN pk.CONSTRAINT_TYPE = 'PRIMARY KEY' THEN 1 ELSE 0 END as pk,
c.COLUMN_DEFAULT as dflt_value
FROM
INFORMATION_SCHEMA.COLUMNS c
LEFT JOIN
INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON c.TABLE_NAME = kcu.TABLE_NAME AND c.COLUMN_NAME = kcu.COLUMN_NAME
LEFT JOIN
INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ON kcu.CONSTRAINT_NAME = pk.CONSTRAINT_NAME AND pk.CONSTRAINT_TYPE = 'PRIMARY KEY'
WHERE
c.TABLE_NAME = '${tableName}'
ORDER BY
c.ORDINAL_POSITION
`;
}
/**
* Helper to get the number of affected rows based on query type
*/
private getAffectedRows(query: string, lastID: number): number {
const queryType = query.trim().split(' ')[0].toUpperCase();
if (queryType === 'INSERT' && lastID > 0) {
return 1;
}
return 0; // For SELECT, unknown for UPDATE/DELETE without additional query
}
}

View File

@@ -1,5 +1,4 @@
import { dbAll } from '../db/index.js';
import { getDatabasePath } from '../db/index.js';
import { dbAll, getListTablesQuery, getDescribeTableQuery, getDatabaseMetadata } from '../db/index.js';
/**
* Handle listing resources request
@@ -7,13 +6,24 @@ import { getDatabasePath } from '../db/index.js';
*/
export async function handleListResources() {
try {
const databasePath = getDatabasePath();
const resourceBaseUrl = new URL(`sqlite:///${databasePath}`);
const dbInfo = getDatabaseMetadata();
const dbType = dbInfo.type;
let resourceBaseUrl: URL;
// Create appropriate URL based on database type
if (dbType === 'sqlite' && dbInfo.path) {
resourceBaseUrl = new URL(`sqlite:///${dbInfo.path}`);
} else if (dbType === 'sqlserver' && dbInfo.server && dbInfo.database) {
resourceBaseUrl = new URL(`sqlserver://${dbInfo.server}/${dbInfo.database}`);
} else {
resourceBaseUrl = new URL(`db:///database`);
}
const SCHEMA_PATH = "schema";
const result = await dbAll(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
);
// Use adapter-specific query to list tables
const query = getListTablesQuery();
const result = await dbAll(query);
return {
resources: result.map((row: any) => ({
@@ -45,8 +55,9 @@ export async function handleReadResource(uri: string) {
throw new Error("Invalid resource URI");
}
// Query to get column information for a table
const result = await dbAll(`PRAGMA table_info("${tableName}")`);
// Use adapter-specific query to describe the table
const query = getDescribeTableQuery(tableName!);
const result = await dbAll(query);
return {
contents: [

View File

@@ -10,12 +10,20 @@ import {
} from "@modelcontextprotocol/sdk/types.js";
// Import database utils
import { initDatabase, closeDatabase } from './db/index.js';
import { initDatabase, closeDatabase, getDatabaseMetadata } from './db/index.js';
// Import handlers
import { handleListResources, handleReadResource } from './handlers/resourceHandlers.js';
import { handleListTools, handleToolCall } from './handlers/toolHandlers.js';
// Setup a logger that uses stderr instead of stdout to avoid interfering with MCP communications
const logger = {
log: (...args: any[]) => console.error('[INFO]', ...args),
error: (...args: any[]) => console.error('[ERROR]', ...args),
warn: (...args: any[]) => console.error('[WARN]', ...args),
info: (...args: any[]) => console.error('[INFO]', ...args),
};
// Configure the server
const server = new Server(
{
@@ -33,11 +41,52 @@ const server = new Server(
// Parse command line arguments
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Please provide a database file path as a command-line argument");
logger.error("Please provide database connection information");
logger.error("Usage for SQLite: node index.js <database_file_path>");
logger.error("Usage for SQL Server: node index.js --sqlserver --server <server> --database <database> [--user <user> --password <password>]");
process.exit(1);
}
const databasePath = args[0];
// Parse arguments to determine database type and connection info
let dbType = 'sqlite';
let connectionInfo: any = null;
// Check if using SQL Server
if (args.includes('--sqlserver')) {
dbType = 'sqlserver';
connectionInfo = {
server: '',
database: '',
user: undefined,
password: undefined
};
// Parse SQL Server connection parameters
for (let i = 0; i < args.length; i++) {
if (args[i] === '--server' && i + 1 < args.length) {
connectionInfo.server = args[i + 1];
} else if (args[i] === '--database' && i + 1 < args.length) {
connectionInfo.database = args[i + 1];
} else if (args[i] === '--user' && i + 1 < args.length) {
connectionInfo.user = args[i + 1];
} else if (args[i] === '--password' && i + 1 < args.length) {
connectionInfo.password = args[i + 1];
} else if (args[i] === '--port' && i + 1 < args.length) {
connectionInfo.port = parseInt(args[i + 1], 10);
}
}
// Validate SQL Server connection info
if (!connectionInfo.server || !connectionInfo.database) {
logger.error("Error: SQL Server requires --server and --database parameters");
process.exit(1);
}
} else {
// SQLite mode (default)
dbType = 'sqlite';
connectionInfo = args[0]; // First argument is the SQLite file path
logger.info(`Using SQLite database at path: ${connectionInfo}`);
}
// Set up request handlers
server.setRequestHandler(ListResourcesRequestSchema, async () => {
@@ -58,35 +107,57 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
// Handle shutdown gracefully
process.on('SIGINT', async () => {
console.log('Shutting down gracefully...');
logger.info('Shutting down gracefully...');
await closeDatabase();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log('Shutting down gracefully...');
logger.info('Shutting down gracefully...');
await closeDatabase();
process.exit(0);
});
// Add global error handler
process.on('uncaughtException', (error) => {
logger.error('Uncaught exception:', error);
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
});
/**
* Start the server
*/
async function runServer() {
try {
console.log(`Initializing database: ${databasePath}`);
await initDatabase(databasePath);
logger.info(`Initializing ${dbType} database...`);
if (dbType === 'sqlite') {
logger.info(`Database path: ${connectionInfo}`);
} else if (dbType === 'sqlserver') {
logger.info(`Server: ${connectionInfo.server}, Database: ${connectionInfo.database}`);
}
console.log('Starting MCP server...');
// Initialize the database
await initDatabase(connectionInfo, dbType);
const dbInfo = getDatabaseMetadata();
logger.info(`Connected to ${dbInfo.name} database`);
logger.info('Starting MCP server...');
const transport = new StdioServerTransport();
await server.connect(transport);
console.log('Server running. Press Ctrl+C to exit.');
logger.info('Server running. Press Ctrl+C to exit.');
} catch (error) {
console.error("Failed to initialize:", error);
logger.error("Failed to initialize:", error);
process.exit(1);
}
}
// Start the server
runServer().catch(console.error);
runServer().catch(error => {
logger.error("Server initialization failed:", error);
process.exit(1);
});

View File

@@ -1,4 +1,4 @@
import { dbAll, dbExec } from '../db/index.js';
import { dbAll, dbExec, getListTablesQuery, getDescribeTableQuery } from '../db/index.js';
import { formatSuccessResponse } from '../utils/formatUtils.js';
/**
@@ -56,13 +56,12 @@ export async function dropTable(tableName: string, confirm: boolean) {
});
}
// Check if table exists
const tableExists = await dbAll(
"SELECT name FROM sqlite_master WHERE type='table' AND name = ?",
[tableName]
);
// First check if table exists by directly querying for tables
const query = getListTablesQuery();
const tables = await dbAll(query);
const tableNames = tables.map(t => t.name);
if (tableExists.length === 0) {
if (!tableNames.includes(tableName)) {
throw new Error(`Table '${tableName}' does not exist`);
}
@@ -84,9 +83,9 @@ export async function dropTable(tableName: string, confirm: boolean) {
*/
export async function listTables() {
try {
const tables = await dbAll(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
);
// Use adapter-specific query for listing tables
const query = getListTablesQuery();
const tables = await dbAll(query);
return formatSuccessResponse(tables.map((t) => t.name));
} catch (error: any) {
throw new Error(`Error listing tables: ${error.message}`);
@@ -104,17 +103,19 @@ export async function describeTable(tableName: string) {
throw new Error("Table name is required");
}
// Check if table exists
const tableExists = await dbAll(
"SELECT name FROM sqlite_master WHERE type='table' AND name = ?",
[tableName]
);
// First check if table exists by directly querying for tables
const query = getListTablesQuery();
const tables = await dbAll(query);
const tableNames = tables.map(t => t.name);
if (tableExists.length === 0) {
if (!tableNames.includes(tableName)) {
throw new Error(`Table '${tableName}' does not exist`);
}
const columns = await dbAll(`PRAGMA table_info("${tableName}")`);
// Use adapter-specific query for describing tables
const descQuery = getDescribeTableQuery(tableName);
const columns = await dbAll(descQuery);
return formatSuccessResponse(columns.map((col) => ({
name: col.name,
type: col.type,