import { z } from "zod"; import { assertSafeSelect, buildWhereClause, qualifyName, quoteIdentifier, validateProcedureDefinition } from "./sql.js"; const JsonValueSchema = z.union([ z.string(), z.number(), z.boolean(), z.null(), z.array(z.unknown()), z.record(z.string(), z.unknown()) ]); const DatabaseNameSchema = z.string().min(1).optional().describe("Optional database name from the configured DATABASES allowlist."); const SchemaNameSchema = z.string().min(1).default("dbo").describe("SQL Server schema name. Defaults to dbo."); const ObjectNameSchema = z.string().min(1).describe("SQL Server object name without schema."); const ResponseFormatSchema = z.enum(["markdown", "json"]).default("markdown"); export function registerSqlServerTools(server, client) { server.registerTool("sqlserver_list_databases", { title: "List Configured SQL Server Databases", description: "List database names this SQL Server MCP is allowed to access. This reads local MCP configuration only.", inputSchema: z.object({ response_format: ResponseFormatSchema }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async ({ response_format }) => toolResult({ defaultDatabase: client.defaultDatabase, databases: client.allowedDatabases }, response_format)); server.registerTool("sqlserver_list_tables", { title: "List SQL Server Tables", description: "List user tables in a configured SQL Server database, optionally filtered by schema.", inputSchema: z.object({ databaseName: DatabaseNameSchema, schemaName: z.string().min(1).optional(), response_format: ResponseFormatSchema }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async ({ databaseName, schemaName, response_format }) => handleTool(async () => { const params = []; let where = "WHERE t.is_ms_shipped = 0"; if (schemaName) { where += " AND s.name = @schemaName"; params.push({ name: "schemaName", value: schemaName }); } const result = await client.query(databaseName, ` SELECT s.name AS schema_name, t.name AS table_name, SUM(p.rows) AS row_count, t.create_date, t.modify_date FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id LEFT JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0, 1) ${where} GROUP BY s.name, t.name, t.create_date, t.modify_date ORDER BY s.name, t.name`, params); return toolResult({ count: result.recordset.length, tables: result.recordset }, response_format); })); server.registerTool("sqlserver_describe_table", { title: "Describe SQL Server Table", description: "Return columns, indexes, and row count metadata for a SQL Server table.", inputSchema: z.object({ databaseName: DatabaseNameSchema, schemaName: SchemaNameSchema, tableName: ObjectNameSchema, response_format: ResponseFormatSchema }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async ({ databaseName, schemaName, tableName, response_format }) => handleTool(async () => { const columns = await client.query(databaseName, ` SELECT c.column_id, c.name AS column_name, ty.name AS data_type, c.max_length, c.precision, c.scale, c.is_nullable, dc.definition AS default_definition, ep.value AS description FROM sys.columns c JOIN sys.types ty ON ty.user_type_id = c.user_type_id LEFT JOIN sys.default_constraints dc ON dc.object_id = c.default_object_id LEFT JOIN sys.extended_properties ep ON ep.major_id = c.object_id AND ep.minor_id = c.column_id AND ep.name = 'MS_Description' WHERE c.object_id = OBJECT_ID(@objectName) ORDER BY c.column_id`, [{ name: "objectName", value: `${schemaName}.${tableName}` }]); const indexes = await client.query(databaseName, ` SELECT i.name AS index_name, i.type_desc, i.is_unique, i.is_primary_key, STRING_AGG(c.name, ', ') WITHIN GROUP (ORDER BY ic.key_ordinal) AS columns FROM sys.indexes i JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id WHERE i.object_id = OBJECT_ID(@objectName) AND i.name IS NOT NULL GROUP BY i.name, i.type_desc, i.is_unique, i.is_primary_key ORDER BY i.is_primary_key DESC, i.name`, [{ name: "objectName", value: `${schemaName}.${tableName}` }]); return toolResult({ table: `${schemaName}.${tableName}`, columns: columns.recordset, indexes: indexes.recordset }, response_format); })); server.registerTool("sqlserver_read_data", { title: "Read SQL Server Data", description: "Execute a single SELECT or CTE query against SQL Server. Mutating statements and multiple statements are blocked.", inputSchema: z.object({ databaseName: DatabaseNameSchema, query: z.string().min(1).describe("A single SELECT statement or CTE query."), response_format: ResponseFormatSchema }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async ({ databaseName, query, response_format }) => handleTool(async () => { assertSafeSelect(query); const result = await client.query(databaseName, query); const rows = result.recordset.slice(0, client.maxRows); return toolResult({ count: rows.length, truncated: result.recordset.length > rows.length, rows }, response_format); })); server.registerTool("sqlserver_insert_rows", { title: "Insert SQL Server Rows", description: "Insert one or more rows into a table. Disabled when READONLY=true.", inputSchema: z.object({ databaseName: DatabaseNameSchema, schemaName: SchemaNameSchema, tableName: ObjectNameSchema, rows: z.array(z.record(z.string(), JsonValueSchema)).min(1).max(100), confirm: z.literal(true).describe("Must be true to execute the insert."), response_format: ResponseFormatSchema }).strict(), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true } }, async ({ databaseName, schemaName, tableName, rows, response_format }) => handleTool(async () => { assertWritable(client); assertWriteLimit(client, rows.length); const result = await insertRows(client, databaseName, schemaName, tableName, rows); return toolResult({ rowsAffected: result.rowsAffected }, response_format); })); server.registerTool("sqlserver_update_rows", { title: "Update SQL Server Rows", description: "Update table rows using equality-based SET values and WHERE criteria. WHERE is required. Disabled when READONLY=true.", inputSchema: z.object({ databaseName: DatabaseNameSchema, schemaName: SchemaNameSchema, tableName: ObjectNameSchema, values: z.record(z.string(), JsonValueSchema).refine((value) => Object.keys(value).length > 0, "values must not be empty"), where: z.record(z.string(), JsonValueSchema).refine((value) => Object.keys(value).length > 0, "where must not be empty"), confirm: z.literal(true).describe("Must be true to execute the update."), response_format: ResponseFormatSchema }).strict(), annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true } }, async ({ databaseName, schemaName, tableName, values, where, response_format }) => handleTool(async () => { assertWritable(client); const setClause = buildAssignments(values, "s"); const whereClause = buildWhereClause(where, "w"); const result = await client.query(databaseName, `UPDATE ${qualifyName(schemaName, tableName)} SET ${setClause.clause} WHERE ${whereClause.clause}`, [...setClause.params, ...whereClause.params]); assertAffectedRowsWithinLimit(client, result.rowsAffected); return toolResult({ rowsAffected: result.rowsAffected }, response_format); })); server.registerTool("sqlserver_delete_rows", { title: "Delete SQL Server Rows", description: "Delete table rows using equality-based WHERE criteria. WHERE is required. Disabled when READONLY=true.", inputSchema: z.object({ databaseName: DatabaseNameSchema, schemaName: SchemaNameSchema, tableName: ObjectNameSchema, where: z.record(z.string(), JsonValueSchema).refine((value) => Object.keys(value).length > 0, "where must not be empty"), confirm: z.literal(true).describe("Must be true to execute the delete."), response_format: ResponseFormatSchema }).strict(), annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true } }, async ({ databaseName, schemaName, tableName, where, response_format }) => handleTool(async () => { assertWritable(client); const whereClause = buildWhereClause(where, "w"); const result = await client.query(databaseName, `DELETE FROM ${qualifyName(schemaName, tableName)} WHERE ${whereClause.clause}`, whereClause.params); assertAffectedRowsWithinLimit(client, result.rowsAffected); return toolResult({ rowsAffected: result.rowsAffected }, response_format); })); server.registerTool("sqlserver_create_table", { title: "Create SQL Server Table", description: "Create a SQL Server table from validated column definitions. Requires ENABLE_DDL=true and READONLY=false.", inputSchema: z.object({ databaseName: DatabaseNameSchema, schemaName: SchemaNameSchema, tableName: ObjectNameSchema, columns: z.array(z.object({ name: z.string().min(1), type: z.string().min(1).describe("SQL Server data type, e.g. int, bigint, nvarchar(100), decimal(18,2), datetime2."), nullable: z.boolean().default(true), primaryKey: z.boolean().default(false), identity: z.boolean().default(false), defaultValue: z.string().optional() }).strict()).min(1), confirm: z.literal(true), response_format: ResponseFormatSchema }).strict(), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true } }, async ({ databaseName, schemaName, tableName, columns, response_format }) => handleTool(async () => { assertDdlAllowed(client); const definitions = columns.map((column) => { assertSafeDataType(column.type); const pieces = [ quoteIdentifier(column.name), column.type, column.identity ? "IDENTITY(1,1)" : "", column.nullable ? "NULL" : "NOT NULL", column.defaultValue ? `DEFAULT ${column.defaultValue}` : "" ].filter(Boolean); return pieces.join(" "); }); const primaryKeys = columns.filter((column) => column.primaryKey).map((column) => quoteIdentifier(column.name)); if (primaryKeys.length > 0) { definitions.push(`CONSTRAINT ${quoteIdentifier(`PK_${tableName}`)} PRIMARY KEY (${primaryKeys.join(", ")})`); } await client.query(databaseName, `CREATE TABLE ${qualifyName(schemaName, tableName)} (\n ${definitions.join(",\n ")}\n)`); return toolResult({ created: true, table: `${schemaName}.${tableName}` }, response_format); })); server.registerTool("sqlserver_drop_table", { title: "Drop SQL Server Table", description: "Drop a SQL Server table if it exists. Requires ENABLE_DDL=true, READONLY=false, and confirm=true.", inputSchema: z.object({ databaseName: DatabaseNameSchema, schemaName: SchemaNameSchema, tableName: ObjectNameSchema, confirm: z.literal(true), response_format: ResponseFormatSchema }).strict(), annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true } }, async ({ databaseName, schemaName, tableName, response_format }) => handleTool(async () => { assertDdlAllowed(client); await client.query(databaseName, `DROP TABLE IF EXISTS ${qualifyName(schemaName, tableName)}`); return toolResult({ dropped: true, table: `${schemaName}.${tableName}` }, response_format); })); server.registerTool("sqlserver_list_procedures", { title: "List SQL Server Stored Procedures", description: "List stored procedures in a configured SQL Server database, optionally filtered by schema.", inputSchema: z.object({ databaseName: DatabaseNameSchema, schemaName: z.string().min(1).optional(), response_format: ResponseFormatSchema }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async ({ databaseName, schemaName, response_format }) => handleTool(async () => { const params = []; let where = "WHERE p.is_ms_shipped = 0"; if (schemaName) { where += " AND s.name = @schemaName"; params.push({ name: "schemaName", value: schemaName }); } const result = await client.query(databaseName, ` SELECT s.name AS schema_name, p.name AS procedure_name, p.create_date, p.modify_date FROM sys.procedures p JOIN sys.schemas s ON s.schema_id = p.schema_id ${where} ORDER BY s.name, p.name`, params); return toolResult({ count: result.recordset.length, procedures: result.recordset }, response_format); })); server.registerTool("sqlserver_describe_procedure", { title: "Describe SQL Server Stored Procedure", description: "Return definition and parameter metadata for a stored procedure.", inputSchema: z.object({ databaseName: DatabaseNameSchema, schemaName: SchemaNameSchema, procedureName: ObjectNameSchema, response_format: ResponseFormatSchema }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async ({ databaseName, schemaName, procedureName, response_format }) => handleTool(async () => { const objectName = `${schemaName}.${procedureName}`; const definition = await client.query(databaseName, ` SELECT s.name AS schema_name, p.name AS procedure_name, p.create_date, p.modify_date, m.definition FROM sys.procedures p JOIN sys.schemas s ON s.schema_id = p.schema_id LEFT JOIN sys.sql_modules m ON m.object_id = p.object_id WHERE p.object_id = OBJECT_ID(@objectName)`, [{ name: "objectName", value: objectName }]); const parameters = await client.query(databaseName, ` SELECT parameter_id, name, TYPE_NAME(user_type_id) AS data_type, max_length, precision, scale, is_output FROM sys.parameters WHERE object_id = OBJECT_ID(@objectName) ORDER BY parameter_id`, [{ name: "objectName", value: objectName }]); return toolResult({ procedure: objectName, metadata: definition.recordset[0] ?? null, parameters: parameters.recordset }, response_format); })); server.registerTool("sqlserver_create_or_alter_procedure", { title: "Create Or Alter SQL Server Stored Procedure", description: "Create or alter a stored procedure. The definition must start with CREATE PROCEDURE, ALTER PROCEDURE, or CREATE OR ALTER PROCEDURE and match schemaName/procedureName. Requires ENABLE_DDL=true and READONLY=false.", inputSchema: z.object({ databaseName: DatabaseNameSchema, schemaName: SchemaNameSchema, procedureName: ObjectNameSchema, definition: z.string().min(1), confirm: z.literal(true), response_format: ResponseFormatSchema }).strict(), annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true } }, async ({ databaseName, schemaName, procedureName, definition, response_format }) => handleTool(async () => { assertDdlAllowed(client); validateProcedureDefinition(definition, schemaName, procedureName); await client.batch(databaseName, definition); return toolResult({ saved: true, procedure: `${schemaName}.${procedureName}` }, response_format); })); server.registerTool("sqlserver_drop_procedure", { title: "Drop SQL Server Stored Procedure", description: "Drop a stored procedure if it exists. Requires ENABLE_DDL=true, READONLY=false, and confirm=true.", inputSchema: z.object({ databaseName: DatabaseNameSchema, schemaName: SchemaNameSchema, procedureName: ObjectNameSchema, confirm: z.literal(true), response_format: ResponseFormatSchema }).strict(), annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true } }, async ({ databaseName, schemaName, procedureName, response_format }) => handleTool(async () => { assertDdlAllowed(client); await client.query(databaseName, `DROP PROCEDURE IF EXISTS ${qualifyName(schemaName, procedureName)}`); return toolResult({ dropped: true, procedure: `${schemaName}.${procedureName}` }, response_format); })); server.registerTool("sqlserver_execute_procedure", { title: "Execute SQL Server Stored Procedure", description: "Execute a stored procedure with named input parameters. Disabled when READONLY=true unless allowInReadOnly=true is explicitly set in the tool call.", inputSchema: z.object({ databaseName: DatabaseNameSchema, schemaName: SchemaNameSchema, procedureName: ObjectNameSchema, parameters: z.record(z.string(), JsonValueSchema).default({}), allowInReadOnly: z.boolean().default(false), confirm: z.literal(true), response_format: ResponseFormatSchema }).strict(), annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true } }, async ({ databaseName, schemaName, procedureName, parameters, allowInReadOnly, response_format }) => handleTool(async () => { if (client.readOnly && !allowInReadOnly) { throw new Error("READONLY=true blocks stored procedure execution. Set allowInReadOnly=true only for procedures known to be read-only."); } const params = Object.entries(parameters).map(([name, value], index) => ({ sourceName: name, param: { name: `p${index}`, value } })); const assignments = params.map(({ sourceName, param }) => `@${sourceName.replace(/^@/u, "")} = @${param.name}`); const result = await client.query(databaseName, `EXEC ${qualifyName(schemaName, procedureName)} ${assignments.join(", ")}`, params.map(({ param }) => param)); return toolResult({ rowsAffected: result.rowsAffected, recordset: result.recordset?.slice(0, client.maxRows) ?? [] }, response_format); })); } function toolResult(data, responseFormat = "markdown") { const text = responseFormat === "json" ? JSON.stringify(data, null, 2) : toMarkdown(data); return { content: [{ type: "text", text }] }; } async function handleTool(action) { try { return await action(); } catch (error) { return { isError: true, content: [{ type: "text", text: formatError(error) }] }; } } function toMarkdown(data) { return ["```json", JSON.stringify(data, null, 2), "```"].join("\n"); } function formatError(error) { if (error instanceof Error) { return `Error: ${error.message}`; } return `Error: ${String(error)}`; } function assertWritable(client) { if (client.readOnly) { throw new Error("READONLY=true blocks write operations."); } } function assertDdlAllowed(client) { assertWritable(client); if (!client.enableDdl) { throw new Error("ENABLE_DDL=true is required for table and stored procedure DDL operations."); } } function assertWriteLimit(client, rows) { if (rows > client.maxWriteRows) { throw new Error(`Write affects ${rows} rows, which exceeds MAX_WRITE_ROWS=${client.maxWriteRows}.`); } } function assertAffectedRowsWithinLimit(client, rowsAffected) { const total = rowsAffected.reduce((sum, count) => sum + count, 0); assertWriteLimit(client, total); } async function insertRows(client, databaseName, schemaName, tableName, rows) { const firstColumns = Object.keys(rows[0] ?? {}); if (firstColumns.length === 0) { throw new Error("Insert rows must include at least one column."); } for (const row of rows) { const columns = Object.keys(row); if (columns.length !== firstColumns.length || columns.some((column) => !firstColumns.includes(column))) { throw new Error("All insert rows must have the same columns."); } } const params = []; const values = rows.map((row, rowIndex) => { const rowParams = firstColumns.map((column, columnIndex) => { const name = `r${rowIndex}_${columnIndex}`; params.push({ name, value: row[column] }); return `@${name}`; }); return `(${rowParams.join(", ")})`; }); return client.query(databaseName, `INSERT INTO ${qualifyName(schemaName, tableName)} (${firstColumns.map(quoteIdentifier).join(", ")}) VALUES ${values.join(", ")}`, params); } function buildAssignments(values, prefix) { const entries = Object.entries(values).filter(([, value]) => value !== undefined); if (entries.length === 0) { throw new Error("SET values must include at least one field."); } const params = entries.map(([key, value], index) => ({ name: `${prefix}${index}`, value, key })); return { clause: params.map((param) => `${quoteIdentifier(param.key)} = @${param.name}`).join(", "), params: params.map(({ name, value }) => ({ name, value })) }; } function assertSafeDataType(type) { const trimmed = type.trim(); if (!/^[a-zA-Z][a-zA-Z0-9\s(),]+$/u.test(trimmed)) { throw new Error(`Unsafe SQL data type '${type}'.`); } if (/\b(drop|alter|create|insert|update|delete|exec|execute|merge|truncate)\b/iu.test(trimmed)) { throw new Error(`Unsafe SQL data type '${type}'.`); } } //# sourceMappingURL=tools.js.map