UNPKG

@bilims/mcp-sqlserver

Version:

MCP Server for Microsoft SQL Server with CRUD operations and data analysis capabilities

636 lines 26 kB
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { DatabaseConnection } from './database/connection.js'; import { CrudTools, SelectToolSchema, InsertToolSchema, UpdateToolSchema, DeleteToolSchema, CustomQuerySchema } from './tools/crud-tools.js'; import { SchemaTools, GetTablesSchema, GetColumnsSchema } from './tools/schema-tools.js'; import { StoredProcedureTools, ExecuteStoredProcedureSchema, GetStoredProceduresSchema, GetStoredProcedureInfoSchema } from './tools/stored-procedure-tools.js'; import { TransactionTools, BeginTransactionSchema, CommitTransactionSchema, RollbackTransactionSchema, CreateSavepointSchema } from './tools/transaction-tools.js'; import { BulkTools, BulkInsertSchema, BatchUpdateSchema, BatchDeleteSchema, ImportDataSchema } from './tools/bulk-tools.js'; import { loadDatabaseConfig } from './config/database.js'; import { zodToJsonSchema } from './utils/schema-converter.js'; export class SqlServerMcpServer { server; db; crudTools; schemaTools; storedProcedureTools; transactionTools; bulkTools; isDbConnected = false; constructor() { try { process.stderr.write('Initializing MCP Server instance...\n'); this.server = new McpServer({ name: 'mcp-sqlserver', version: '1.1.0', description: 'MCP Server for Microsoft SQL Server with CRUD operations and schema introspection' }); process.stderr.write('Loading database configuration...\n'); let config; try { config = loadDatabaseConfig(); process.stderr.write('Database configuration loaded successfully.\n'); } catch (configError) { process.stderr.write(`Warning: Failed to load database configuration: ${configError}\n`); process.stderr.write('Server will start without database connection.\n'); // Create a dummy config to allow server to start config = { host: 'localhost', port: 1433, database: '', username: '', password: '', encrypt: false, trustServerCertificate: false, pool: { min: 0, max: 10 }, connectionTimeout: 15000, requestTimeout: 15000, enableArithAbort: true }; } process.stderr.write('Creating database connection...\n'); this.db = new DatabaseConnection(config); this.crudTools = new CrudTools(this.db); this.schemaTools = new SchemaTools(this.db); this.storedProcedureTools = new StoredProcedureTools(this.db); this.transactionTools = new TransactionTools(this.db); this.bulkTools = new BulkTools(this.db); process.stderr.write('Setting up tools and error handling...\n'); this.setupTools(); this.setupErrorHandling(); process.stderr.write('Server initialization complete.\n'); } catch (error) { process.stderr.write(`Error during server construction: ${error}\n`); if (error instanceof Error && error.stack) { process.stderr.write(`Stack trace: ${error.stack}\n`); } throw error; } } async wrapWithDbConnection(fn) { await this.ensureDbConnection(); return fn(); } setupTools() { // SELECT tool this.server.registerTool('sql_select', { title: 'Execute SELECT Query', description: 'Execute a SELECT query with optional WHERE conditions, JOINs, and ordering', inputSchema: zodToJsonSchema(SelectToolSchema) }, async (args) => { try { await this.ensureDbConnection(); const params = SelectToolSchema.parse(args); const result = await this.crudTools.executeSelect(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_select'); } }); // INSERT tool this.server.registerTool('sql_insert', { title: 'Execute INSERT Query', description: 'Insert new records into a table', inputSchema: zodToJsonSchema(InsertToolSchema) }, async (args) => { try { const params = InsertToolSchema.parse(args); const result = await this.crudTools.executeInsert(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_insert'); } }); // UPDATE tool this.server.registerTool('sql_update', { title: 'Execute UPDATE Query', description: 'Update existing records in a table', inputSchema: zodToJsonSchema(UpdateToolSchema) }, async (args) => { try { const params = UpdateToolSchema.parse(args); const result = await this.crudTools.executeUpdate(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_update'); } }); // DELETE tool this.server.registerTool('sql_delete', { title: 'Execute DELETE Query', description: 'Delete records from a table', inputSchema: zodToJsonSchema(DeleteToolSchema) }, async (args) => { try { const params = DeleteToolSchema.parse(args); const result = await this.crudTools.executeDelete(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_delete'); } }); // Custom Query tool this.server.registerTool('sql_query', { title: 'Execute Custom SQL Query', description: 'Execute a custom SQL query (SELECT statements only for security)', inputSchema: zodToJsonSchema(CustomQuerySchema) }, async (args) => { try { const params = CustomQuerySchema.parse(args); const result = await this.crudTools.executeCustomQuery(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_query'); } }); // Schema introspection tools this.server.registerTool('sql_get_tables', { title: 'Get Database Tables', description: 'List all tables in the database or specific schema', inputSchema: zodToJsonSchema(GetTablesSchema) }, async (args) => { try { const params = GetTablesSchema.parse(args); const result = await this.schemaTools.getTables(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_get_tables'); } }); this.server.registerTool('sql_get_columns', { title: 'Get Table Columns', description: 'Get detailed information about columns in a specific table', inputSchema: zodToJsonSchema(GetColumnsSchema) }, async (args) => { try { const params = GetColumnsSchema.parse(args); const result = await this.schemaTools.getColumns(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_get_columns'); } }); this.server.registerTool('sql_get_table_structure', { title: 'Get Complete Table Structure', description: 'Get complete structure of a table including columns, indexes, and foreign keys', inputSchema: zodToJsonSchema(GetColumnsSchema) }, async (args) => { try { const params = GetColumnsSchema.parse(args); const result = await this.schemaTools.getTableStructure(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_get_table_structure'); } }); this.server.registerTool('sql_get_schemas', { title: 'Get Database Schemas', description: 'List all available schemas in the database', inputSchema: {} }, async () => { try { const result = await this.schemaTools.getSchemas(); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_get_schemas'); } }); // Connection test tool this.server.registerTool('sql_test_connection', { title: 'Test Database Connection', description: 'Test the database connection and get server information', inputSchema: {} }, async () => { try { const isConnected = await this.db.testConnection(); if (isConnected) { const serverInfo = await this.db.getServerInfo(); return { content: [{ type: 'text', text: JSON.stringify({ success: true, connected: true, serverInfo }, null, 2) }] }; } else { return { content: [{ type: 'text', text: JSON.stringify({ success: false, connected: false, error: 'Connection test failed' }, null, 2) }] }; } } catch (error) { return this.handleError(error, 'sql_test_connection'); } }); // Stored Procedure tools this.server.registerTool('sql_execute_stored_procedure', { title: 'Execute Stored Procedure', description: 'Execute a stored procedure with parameters and handle multiple result sets', inputSchema: zodToJsonSchema(ExecuteStoredProcedureSchema) }, async (args) => { try { const params = ExecuteStoredProcedureSchema.parse(args); const result = await this.storedProcedureTools.executeStoredProcedure(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_execute_stored_procedure'); } }); this.server.registerTool('sql_get_stored_procedures', { title: 'Get Stored Procedures', description: 'List all stored procedures in the database or specific schema', inputSchema: zodToJsonSchema(GetStoredProceduresSchema) }, async (args) => { try { const params = GetStoredProceduresSchema.parse(args); const result = await this.storedProcedureTools.getStoredProcedures(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_get_stored_procedures'); } }); this.server.registerTool('sql_get_stored_procedure_info', { title: 'Get Stored Procedure Info', description: 'Get detailed information about a specific stored procedure including parameters', inputSchema: zodToJsonSchema(GetStoredProcedureInfoSchema) }, async (args) => { try { const params = GetStoredProcedureInfoSchema.parse(args); const result = await this.storedProcedureTools.getStoredProcedureInfo(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_get_stored_procedure_info'); } }); // Transaction Management tools this.server.registerTool('sql_begin_transaction', { title: 'Begin Transaction', description: 'Start a new database transaction with optional isolation level', inputSchema: zodToJsonSchema(BeginTransactionSchema) }, async (args) => { try { const params = BeginTransactionSchema.parse(args); const result = await this.transactionTools.beginTransaction(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_begin_transaction'); } }); this.server.registerTool('sql_commit_transaction', { title: 'Commit Transaction', description: 'Commit the current database transaction', inputSchema: zodToJsonSchema(CommitTransactionSchema) }, async (args) => { try { const params = CommitTransactionSchema.parse(args); const result = await this.transactionTools.commitTransaction(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_commit_transaction'); } }); this.server.registerTool('sql_rollback_transaction', { title: 'Rollback Transaction', description: 'Rollback the current database transaction or to a savepoint', inputSchema: zodToJsonSchema(RollbackTransactionSchema) }, async (args) => { try { const params = RollbackTransactionSchema.parse(args); const result = await this.transactionTools.rollbackTransaction(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_rollback_transaction'); } }); this.server.registerTool('sql_create_savepoint', { title: 'Create Savepoint', description: 'Create a savepoint within the current transaction', inputSchema: zodToJsonSchema(CreateSavepointSchema) }, async (args) => { try { const params = CreateSavepointSchema.parse(args); const result = await this.transactionTools.createSavepoint(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_create_savepoint'); } }); this.server.registerTool('sql_get_transaction_status', { title: 'Get Transaction Status', description: 'Get current transaction status and connection options', inputSchema: {} }, async () => { try { const result = await this.transactionTools.getTransactionStatus(); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_get_transaction_status'); } }); // Bulk Operations tools this.server.registerTool('sql_bulk_insert', { title: 'Bulk Insert', description: 'Insert multiple records in batches for better performance', inputSchema: zodToJsonSchema(BulkInsertSchema) }, async (args) => { try { const params = BulkInsertSchema.parse(args); const result = await this.bulkTools.bulkInsert(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_bulk_insert'); } }); this.server.registerTool('sql_batch_update', { title: 'Batch Update', description: 'Update multiple records in batches with different conditions', inputSchema: zodToJsonSchema(BatchUpdateSchema) }, async (args) => { try { const params = BatchUpdateSchema.parse(args); const result = await this.bulkTools.batchUpdate(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_batch_update'); } }); this.server.registerTool('sql_batch_delete', { title: 'Batch Delete', description: 'Delete multiple records in batches with different conditions', inputSchema: zodToJsonSchema(BatchDeleteSchema) }, async (args) => { try { const params = BatchDeleteSchema.parse(args); const result = await this.bulkTools.batchDelete(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_batch_delete'); } }); this.server.registerTool('sql_import_data', { title: 'Import Data', description: 'Import data from CSV or JSON format into a table', inputSchema: zodToJsonSchema(ImportDataSchema) }, async (args) => { try { const params = ImportDataSchema.parse(args); const result = await this.bulkTools.importData(params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { return this.handleError(error, 'sql_import_data'); } }); } setupErrorHandling() { // Handle graceful shutdown process.on('SIGINT', async () => { await this.cleanup(); process.exit(0); }); process.on('SIGTERM', async () => { await this.cleanup(); process.exit(0); }); // Handle unhandled errors process.on('uncaughtException', async (error) => { // Log to stderr to avoid contaminating stdio transport process.stderr.write(`Uncaught exception: ${error.message}\n`); await this.cleanup(); process.exit(1); }); process.on('unhandledRejection', async (reason, promise) => { // Log to stderr to avoid contaminating stdio transport process.stderr.write(`Unhandled rejection: ${reason}\n`); await this.cleanup(); process.exit(1); }); // Handle EPIPE errors process.on('SIGPIPE', () => { // Silent - don't log anything to avoid pipe issues }); } handleError(error, toolName) { // Log to stderr to avoid contaminating stdio transport process.stderr.write(`Error in ${toolName}: ${error.message || error}\n`); let errorMessage = 'An unknown error occurred'; if (error instanceof Error) { errorMessage = error.message; } else if (typeof error === 'string') { errorMessage = error; } return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: errorMessage, tool: toolName }, null, 2) }], isError: true }; } async start() { try { process.stderr.write('Creating stdio transport...\n'); const transport = new StdioServerTransport(); // Handle transport errors silently to avoid contaminating stdio transport.onclose = () => { process.stderr.write('Transport closed.\n'); }; transport.onerror = (error) => { process.stderr.write(`Transport error: ${error.message || error}\n`); }; process.stderr.write('Connecting MCP server to transport...\n'); await this.server.connect(transport); process.stderr.write('MCP server connected successfully.\n'); // Delay database connection until after initialization process.stderr.write('Delaying database connection until after initialization...\n'); // Don't log to stdout - it contaminates the stdio transport } catch (error) { process.stderr.write(`Failed to start server: ${error}\n`); if (error instanceof Error && error.stack) { process.stderr.write(`Stack trace: ${error.stack}\n`); } await this.cleanup(); process.exit(1); } } async connectToDatabase() { if (this.isDbConnected) { return true; } try { process.stderr.write('Attempting to connect to database...\n'); await this.db.connect(); process.stderr.write('Database connection established.\n'); this.isDbConnected = true; return true; } catch (error) { process.stderr.write(`Failed to connect to database: ${error}\n`); return false; } } async ensureDbConnection() { if (!this.isDbConnected) { const connected = await this.connectToDatabase(); if (!connected) { throw new Error('Database connection required but not available'); } } } async cleanup() { try { if (this.isDbConnected && this.db) { await this.db.disconnect(); } // Silent cleanup - don't contaminate stdio } catch (error) { process.stderr.write(`Error during cleanup: ${error}\n`); } } } //# sourceMappingURL=server.js.map