UNPKG

@bluspace/mssql-mcp-server

Version:

Microsoft SQL Server MCP (Model Context Protocol) Server - AI-powered database interaction tool

686 lines (685 loc) 33.9 kB
import { z } from "zod"; import sql from "mssql"; // Helper function to safely quote identifiers function quoteIdentifier(identifier) { return `[${identifier.replace(/\]/g, ']]')}]`; } // Helper function to format table name with schema function formatTableName(tableName) { const parts = tableName.split('.'); if (parts.length === 2) { return `${quoteIdentifier(parts[0])}.${quoteIdentifier(parts[1])}`; } return `[dbo].${quoteIdentifier(tableName)}`; } // SQL Server data types const sqlServerDataTypes = [ // Exact numeric "bigint", "int", "smallint", "tinyint", "bit", "decimal", "numeric", "money", "smallmoney", // Approximate numeric "float", "real", // Date and time "date", "datetime", "datetime2", "datetimeoffset", "smalldatetime", "time", // Character strings "char", "varchar", "text", // Unicode character strings "nchar", "nvarchar", "ntext", // Binary strings "binary", "varbinary", "image", // Other data types "uniqueidentifier", "xml", "sql_variant", "geometry", "geography", "hierarchyid" ]; export function registerDdlTools(server, db) { // Create Table Tool server.tool("createTable", "Create a new table with columns and constraints", { table: z.string().describe("Table name (can include schema like 'schema.table')"), columns: z.array(z.object({ name: z.string().describe("Column name"), type: z.string().describe("SQL Server data type (e.g., 'int', 'varchar(50)', 'decimal(10,2)')"), nullable: z.boolean().optional().default(true).describe("Whether the column allows NULL values"), default: z.string().optional().describe("Default value or expression (e.g., '0', 'GETDATE()', 'NEWID()')"), identity: z.object({ seed: z.number().optional().default(1), increment: z.number().optional().default(1) }).optional().describe("Identity specification for auto-incrementing columns"), computed: z.string().optional().describe("Computed column expression"), primaryKey: z.boolean().optional().describe("Whether this column is the primary key"), unique: z.boolean().optional().describe("Whether this column has a unique constraint"), check: z.string().optional().describe("Check constraint expression for this column"), references: z.object({ table: z.string().describe("Referenced table name"), column: z.string().describe("Referenced column name"), onDelete: z.enum(["NO ACTION", "CASCADE", "SET NULL", "SET DEFAULT"]).optional().default("NO ACTION"), onUpdate: z.enum(["NO ACTION", "CASCADE", "SET NULL", "SET DEFAULT"]).optional().default("NO ACTION") }).optional().describe("Foreign key reference") })).describe("Column definitions"), constraints: z.object({ primaryKey: z.object({ name: z.string().optional().describe("Constraint name"), columns: z.array(z.string()).describe("Column names that form the primary key") }).optional().describe("Primary key constraint (if not defined on individual columns)"), uniqueKeys: z.array(z.object({ name: z.string().optional().describe("Constraint name"), columns: z.array(z.string()).describe("Column names that form the unique key") })).optional().describe("Unique constraints"), foreignKeys: z.array(z.object({ name: z.string().optional().describe("Constraint name"), columns: z.array(z.string()).describe("Local column names"), referencedTable: z.string().describe("Referenced table name"), referencedColumns: z.array(z.string()).describe("Referenced column names"), onDelete: z.enum(["NO ACTION", "CASCADE", "SET NULL", "SET DEFAULT"]).optional().default("NO ACTION"), onUpdate: z.enum(["NO ACTION", "CASCADE", "SET NULL", "SET DEFAULT"]).optional().default("NO ACTION") })).optional().describe("Foreign key constraints"), checks: z.array(z.object({ name: z.string().optional().describe("Constraint name"), expression: z.string().describe("Check constraint expression") })).optional().describe("Check constraints") }).optional().describe("Table-level constraints") }, async ({ table, columns, constraints = {} }) => { try { const request = new sql.Request(db); let query = `CREATE TABLE ${formatTableName(table)} (\n`; // Column definitions const columnDefs = []; const primaryKeyColumns = []; for (const col of columns) { let colDef = ` ${quoteIdentifier(col.name)} ${col.type}`; // Identity if (col.identity) { colDef += ` IDENTITY(${col.identity.seed}, ${col.identity.increment})`; } // Computed column if (col.computed) { colDef = ` ${quoteIdentifier(col.name)} AS ${col.computed}`; } else { // NULL/NOT NULL (not applicable to computed columns) colDef += col.nullable ? ' NULL' : ' NOT NULL'; // Default value if (col.default !== undefined) { colDef += ` DEFAULT ${col.default}`; } } // Column-level constraints if (col.primaryKey) { primaryKeyColumns.push(col.name); } if (col.unique) { colDef += ' UNIQUE'; } if (col.check) { colDef += ` CHECK (${col.check})`; } if (col.references) { colDef += ` REFERENCES ${formatTableName(col.references.table)}(${quoteIdentifier(col.references.column)})`; if (col.references.onDelete !== 'NO ACTION') { colDef += ` ON DELETE ${col.references.onDelete}`; } if (col.references.onUpdate !== 'NO ACTION') { colDef += ` ON UPDATE ${col.references.onUpdate}`; } } columnDefs.push(colDef); } // Add primary key constraint if columns were marked as primary key if (primaryKeyColumns.length > 0 && !constraints.primaryKey) { constraints.primaryKey = { columns: primaryKeyColumns }; } // Table-level constraints const constraintDefs = []; // Primary key if (constraints.primaryKey) { const pkName = constraints.primaryKey.name || `PK_${table.replace('.', '_')}`; const pkColumns = constraints.primaryKey.columns.map(c => quoteIdentifier(c)).join(', '); constraintDefs.push(` CONSTRAINT ${quoteIdentifier(pkName)} PRIMARY KEY (${pkColumns})`); } // Unique constraints if (constraints.uniqueKeys) { for (const uk of constraints.uniqueKeys) { const ukName = uk.name || `UQ_${table.replace('.', '_')}_${uk.columns.join('_')}`; const ukColumns = uk.columns.map(c => quoteIdentifier(c)).join(', '); constraintDefs.push(` CONSTRAINT ${quoteIdentifier(ukName)} UNIQUE (${ukColumns})`); } } // Foreign key constraints if (constraints.foreignKeys) { for (const fk of constraints.foreignKeys) { const fkName = fk.name || `FK_${table.replace('.', '_')}_${fk.referencedTable.replace('.', '_')}`; const fkColumns = fk.columns.map(c => quoteIdentifier(c)).join(', '); const refColumns = fk.referencedColumns.map(c => quoteIdentifier(c)).join(', '); let fkDef = ` CONSTRAINT ${quoteIdentifier(fkName)} FOREIGN KEY (${fkColumns}) REFERENCES ${formatTableName(fk.referencedTable)}(${refColumns})`; if (fk.onDelete !== 'NO ACTION') { fkDef += ` ON DELETE ${fk.onDelete}`; } if (fk.onUpdate !== 'NO ACTION') { fkDef += ` ON UPDATE ${fk.onUpdate}`; } constraintDefs.push(fkDef); } } // Check constraints if (constraints.checks) { for (const chk of constraints.checks) { const chkName = chk.name || `CHK_${table.replace('.', '_')}_${Math.random().toString(36).substr(2, 9)}`; constraintDefs.push(` CONSTRAINT ${quoteIdentifier(chkName)} CHECK (${chk.expression})`); } } // Combine all definitions const allDefs = [...columnDefs, ...constraintDefs]; query += allDefs.join(',\n') + '\n)'; // Execute the CREATE TABLE statement await request.query(query); return { content: [{ type: "text", text: JSON.stringify({ success: true, table: table, message: `Table ${table} created successfully`, query: query }) }] }; } catch (error) { console.error(`Error creating table ${table}:`, error); throw new Error(`Failed to create table ${table}: ${error.message}`); } }); // Alter Table Tool server.tool("alterTable", "Alter an existing table structure", { table: z.string().describe("Table name to alter"), operations: z.array(z.union([ // Add column z.object({ type: z.literal("ADD_COLUMN"), column: z.object({ name: z.string(), type: z.string(), nullable: z.boolean().optional().default(true), default: z.string().optional(), check: z.string().optional() }) }), // Drop column z.object({ type: z.literal("DROP_COLUMN"), columnName: z.string() }), // Alter column z.object({ type: z.literal("ALTER_COLUMN"), columnName: z.string(), newType: z.string().optional(), nullable: z.boolean().optional() }), // Add constraint z.object({ type: z.literal("ADD_CONSTRAINT"), constraint: z.union([ z.object({ type: z.literal("PRIMARY_KEY"), name: z.string().optional(), columns: z.array(z.string()) }), z.object({ type: z.literal("UNIQUE"), name: z.string().optional(), columns: z.array(z.string()) }), z.object({ type: z.literal("FOREIGN_KEY"), name: z.string().optional(), columns: z.array(z.string()), referencedTable: z.string(), referencedColumns: z.array(z.string()), onDelete: z.enum(["NO ACTION", "CASCADE", "SET NULL", "SET DEFAULT"]).optional(), onUpdate: z.enum(["NO ACTION", "CASCADE", "SET NULL", "SET DEFAULT"]).optional() }), z.object({ type: z.literal("CHECK"), name: z.string().optional(), expression: z.string() }), z.object({ type: z.literal("DEFAULT"), columnName: z.string(), expression: z.string() }) ]) }), // Drop constraint z.object({ type: z.literal("DROP_CONSTRAINT"), constraintName: z.string() }) ])).describe("List of alter operations to perform") }, async ({ table, operations }) => { try { const request = new sql.Request(db); const results = []; for (const op of operations) { let query = `ALTER TABLE ${formatTableName(table)} `; switch (op.type) { case "ADD_COLUMN": { let colDef = `ADD ${quoteIdentifier(op.column.name)} ${op.column.type}`; colDef += op.column.nullable ? ' NULL' : ' NOT NULL'; if (op.column.default !== undefined) { colDef += ` DEFAULT ${op.column.default}`; } if (op.column.check) { colDef += ` CHECK (${op.column.check})`; } query += colDef; break; } case "DROP_COLUMN": { query += `DROP COLUMN ${quoteIdentifier(op.columnName)}`; break; } case "ALTER_COLUMN": { if (op.newType) { query += `ALTER COLUMN ${quoteIdentifier(op.columnName)} ${op.newType}`; if (op.nullable !== undefined) { query += op.nullable ? ' NULL' : ' NOT NULL'; } } else if (op.nullable !== undefined) { // Just changing nullability - need to get current type const typeQuery = ` SELECT TYPE_NAME(c.user_type_id) as type_name, c.max_length, c.precision, c.scale FROM sys.columns c JOIN sys.tables t ON c.object_id = t.object_id JOIN sys.schemas s ON t.schema_id = s.schema_id WHERE t.name = @tableName AND c.name = @columnName AND (@schemaName IS NULL OR s.name = @schemaName) `; const typeRequest = new sql.Request(db); const parts = table.split('.'); typeRequest.input('tableName', parts.length === 2 ? parts[1] : table); typeRequest.input('schemaName', parts.length === 2 ? parts[0] : 'dbo'); typeRequest.input('columnName', op.columnName); const typeResult = await typeRequest.query(typeQuery); if (typeResult.recordset.length === 0) { throw new Error(`Column ${op.columnName} not found in table ${table}`); } const colInfo = typeResult.recordset[0]; let dataType = colInfo.type_name; // Add length/precision/scale as needed if (['char', 'varchar', 'nchar', 'nvarchar', 'binary', 'varbinary'].includes(colInfo.type_name)) { dataType += `(${colInfo.max_length === -1 ? 'max' : colInfo.max_length})`; } else if (['decimal', 'numeric'].includes(colInfo.type_name)) { dataType += `(${colInfo.precision}, ${colInfo.scale})`; } else if (['float'].includes(colInfo.type_name) && colInfo.precision !== 53) { dataType += `(${colInfo.precision})`; } query += `ALTER COLUMN ${quoteIdentifier(op.columnName)} ${dataType}`; query += op.nullable ? ' NULL' : ' NOT NULL'; } break; } case "ADD_CONSTRAINT": { const constraint = op.constraint; switch (constraint.type) { case "PRIMARY_KEY": { const pkName = constraint.name || `PK_${table.replace('.', '_')}`; const pkColumns = constraint.columns.map(c => quoteIdentifier(c)).join(', '); query += `ADD CONSTRAINT ${quoteIdentifier(pkName)} PRIMARY KEY (${pkColumns})`; break; } case "UNIQUE": { const ukName = constraint.name || `UQ_${table.replace('.', '_')}_${constraint.columns.join('_')}`; const ukColumns = constraint.columns.map(c => quoteIdentifier(c)).join(', '); query += `ADD CONSTRAINT ${quoteIdentifier(ukName)} UNIQUE (${ukColumns})`; break; } case "FOREIGN_KEY": { const fkName = constraint.name || `FK_${table.replace('.', '_')}_${constraint.referencedTable.replace('.', '_')}`; const fkColumns = constraint.columns.map(c => quoteIdentifier(c)).join(', '); const refColumns = constraint.referencedColumns.map(c => quoteIdentifier(c)).join(', '); query += `ADD CONSTRAINT ${quoteIdentifier(fkName)} FOREIGN KEY (${fkColumns}) REFERENCES ${formatTableName(constraint.referencedTable)}(${refColumns})`; if (constraint.onDelete && constraint.onDelete !== 'NO ACTION') { query += ` ON DELETE ${constraint.onDelete}`; } if (constraint.onUpdate && constraint.onUpdate !== 'NO ACTION') { query += ` ON UPDATE ${constraint.onUpdate}`; } break; } case "CHECK": { const chkName = constraint.name || `CHK_${table.replace('.', '_')}_${Math.random().toString(36).substr(2, 9)}`; query += `ADD CONSTRAINT ${quoteIdentifier(chkName)} CHECK (${constraint.expression})`; break; } case "DEFAULT": { const defName = `DF_${table.replace('.', '_')}_${constraint.columnName}`; query += `ADD CONSTRAINT ${quoteIdentifier(defName)} DEFAULT ${constraint.expression} FOR ${quoteIdentifier(constraint.columnName)}`; break; } } break; } case "DROP_CONSTRAINT": { query += `DROP CONSTRAINT ${quoteIdentifier(op.constraintName)}`; break; } } await request.query(query); results.push({ operation: op.type, success: true, query: query }); } return { content: [{ type: "text", text: JSON.stringify({ success: true, table: table, operations: results, message: `Successfully performed ${operations.length} alter operations on table ${table}` }) }] }; } catch (error) { console.error(`Error altering table ${table}:`, error); throw new Error(`Failed to alter table ${table}: ${error.message}`); } }); // Drop Table Tool server.tool("dropTable", "Drop an existing table", { table: z.string().describe("Table name to drop"), ifExists: z.boolean().optional().default(false).describe("Only drop if the table exists") }, async ({ table, ifExists }) => { try { const request = new sql.Request(db); let query = `DROP TABLE ${ifExists ? 'IF EXISTS ' : ''}${formatTableName(table)}`; await request.query(query); return { content: [{ type: "text", text: JSON.stringify({ success: true, table: table, message: `Table ${table} dropped successfully` }) }] }; } catch (error) { console.error(`Error dropping table ${table}:`, error); throw new Error(`Failed to drop table ${table}: ${error.message}`); } }); // Create Index Tool server.tool("createIndex", "Create an index on a table", { indexName: z.string().describe("Name of the index"), table: z.string().describe("Table name"), columns: z.array(z.object({ name: z.string().describe("Column name"), direction: z.enum(["ASC", "DESC"]).optional().default("ASC") })).describe("Columns to index with optional sort direction"), unique: z.boolean().optional().default(false).describe("Whether this is a unique index"), clustered: z.boolean().optional().default(false).describe("Whether this is a clustered index"), include: z.array(z.string()).optional().describe("Non-key columns to include in the index"), where: z.string().optional().describe("Filter predicate for a filtered index"), fillFactor: z.number().min(0).max(100).optional().describe("Fill factor percentage (0-100)"), online: z.boolean().optional().describe("Create index online (Enterprise Edition only)") }, async ({ indexName, table, columns, unique, clustered, include, where, fillFactor, online }) => { try { const request = new sql.Request(db); let query = 'CREATE '; if (unique) query += 'UNIQUE '; if (clustered) query += 'CLUSTERED '; else query += 'NONCLUSTERED '; query += `INDEX ${quoteIdentifier(indexName)} ON ${formatTableName(table)} (`; // Key columns const keyColumns = columns.map(c => `${quoteIdentifier(c.name)} ${c.direction || 'ASC'}`); query += keyColumns.join(', ') + ')'; // Include columns if (include && include.length > 0) { query += ' INCLUDE ('; query += include.map(c => quoteIdentifier(c)).join(', ') + ')'; } // Where clause (filtered index) if (where) { query += ` WHERE ${where}`; } // WITH options const withOptions = []; if (fillFactor !== undefined) { withOptions.push(`FILLFACTOR = ${fillFactor}`); } if (online) { withOptions.push('ONLINE = ON'); } if (withOptions.length > 0) { query += ` WITH (${withOptions.join(', ')})`; } await request.query(query); return { content: [{ type: "text", text: JSON.stringify({ success: true, indexName: indexName, table: table, message: `Index ${indexName} created successfully on table ${table}`, query: query }) }] }; } catch (error) { console.error(`Error creating index ${indexName}:`, error); throw new Error(`Failed to create index ${indexName}: ${error.message}`); } }); // Drop Index Tool server.tool("dropIndex", "Drop an index from a table", { indexName: z.string().describe("Name of the index to drop"), table: z.string().describe("Table name the index is on"), ifExists: z.boolean().optional().default(false).describe("Only drop if the index exists") }, async ({ indexName, table, ifExists }) => { try { const request = new sql.Request(db); let query; if (ifExists) { // Use conditional drop query = ` IF EXISTS ( SELECT * FROM sys.indexes WHERE name = @indexName AND object_id = OBJECT_ID(@tableName) ) DROP INDEX ${quoteIdentifier(indexName)} ON ${formatTableName(table)} `; request.input('indexName', indexName); request.input('tableName', table); } else { query = `DROP INDEX ${quoteIdentifier(indexName)} ON ${formatTableName(table)}`; } await request.query(query); return { content: [{ type: "text", text: JSON.stringify({ success: true, indexName: indexName, table: table, message: `Index ${indexName} dropped successfully from table ${table}` }) }] }; } catch (error) { console.error(`Error dropping index ${indexName}:`, error); throw new Error(`Failed to drop index ${indexName}: ${error.message}`); } }); // Execute Query Tool - General purpose query execution server.tool("executeQuery", "Execute a custom SQL query with optional parameters", { query: z.string().describe("SQL query to execute"), parameters: z.record(z.any()).optional().describe("Named parameters for the query (e.g., {'name': 'John', 'age': 30})") }, async ({ query, parameters = {} }) => { try { const request = new sql.Request(db); // Add parameters to the request Object.entries(parameters).forEach(([name, value]) => { request.input(name, value); }); // Execute the query const result = await request.query(query); // Format response based on query type const queryType = query.trim().split(/\s+/)[0].toUpperCase(); if (['SELECT', 'WITH'].includes(queryType)) { return { content: [{ type: "text", text: JSON.stringify({ success: true, rows: result.recordset, rowCount: result.recordset.length, query: query }) }] }; } else { return { content: [{ type: "text", text: JSON.stringify({ success: true, rowsAffected: result.rowsAffected, message: `Query executed successfully. Rows affected: ${result.rowsAffected}`, query: query }) }] }; } } catch (error) { console.error("Error executing query:", error); throw new Error(`Failed to execute query: ${error.message}`); } }); // Execute SQL Batch Tool - Execute multiple SQL statements with optional transaction server.tool("executeSqlBatch", "Execute multiple SQL statements in sequence, optionally wrapped in a transaction", { statements: z.array(z.union([ z.string().describe("SQL statement to execute"), z.object({ sql: z.string().describe("SQL statement to execute"), parameters: z.record(z.any()).optional().describe("Named parameters for this statement") }) ])).describe("Array of SQL statements to execute in order"), useTransaction: z.boolean().optional().default(true).describe("Whether to wrap all statements in a transaction (default: true)"), stopOnError: z.boolean().optional().default(true).describe("Whether to stop execution on first error (default: true)"), isolationLevel: z.enum([ "READ_UNCOMMITTED", "READ_COMMITTED", "REPEATABLE_READ", "SERIALIZABLE", "SNAPSHOT" ]).optional().default("READ_COMMITTED").describe("Transaction isolation level (only used if useTransaction is true)") }, async ({ statements, useTransaction = true, stopOnError = true, isolationLevel = "READ_COMMITTED" }) => { const results = []; let transaction = null; try { // Start transaction if requested if (useTransaction) { transaction = new sql.Transaction(db); const isolationLevelMap = { "READ_UNCOMMITTED": sql.ISOLATION_LEVEL.READ_UNCOMMITTED, "READ_COMMITTED": sql.ISOLATION_LEVEL.READ_COMMITTED, "REPEATABLE_READ": sql.ISOLATION_LEVEL.REPEATABLE_READ, "SERIALIZABLE": sql.ISOLATION_LEVEL.SERIALIZABLE, "SNAPSHOT": sql.ISOLATION_LEVEL.SNAPSHOT }; await transaction.begin(isolationLevelMap[isolationLevel]); } // Execute each statement for (let i = 0; i < statements.length; i++) { const stmt = statements[i]; const statementInfo = typeof stmt === 'string' ? { sql: stmt, parameters: {} } : { sql: stmt.sql, parameters: stmt.parameters || {} }; try { const request = transaction ? new sql.Request(transaction) : new sql.Request(db); // Add parameters Object.entries(statementInfo.parameters).forEach(([name, value]) => { request.input(name, value); }); // Execute statement const result = await request.query(statementInfo.sql); // Determine result type const queryType = statementInfo.sql.trim().split(/\s+/)[0].toUpperCase(); const isSelect = ['SELECT', 'WITH'].includes(queryType); results.push({ index: i, success: true, sql: statementInfo.sql, rowsAffected: result.rowsAffected, ...(isSelect ? { rows: result.recordset, rowCount: result.recordset.length } : {}) }); } catch (error) { const errorResult = { index: i, success: false, sql: statementInfo.sql, error: error.message }; results.push(errorResult); if (stopOnError) { throw new Error(`Statement ${i} failed: ${error.message}`); } } } // Commit transaction if used if (transaction) { await transaction.commit(); } return { content: [{ type: "text", text: JSON.stringify({ success: true, totalStatements: statements.length, successfulStatements: results.filter(r => r.success).length, failedStatements: results.filter(r => !r.success).length, usedTransaction: useTransaction, results: results }) }] }; } catch (error) { // Rollback transaction if it exists if (transaction) { try { await transaction.rollback(); } catch (rollbackError) { console.error("Rollback failed:", rollbackError); } } console.error("SQL batch execution failed:", error); throw new Error(`SQL batch failed: ${error.message}. Results: ${JSON.stringify(results)}`); } }); }