@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
JavaScript
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)}`);
}
});
}