UNPKG

@bluspace/mssql-mcp-server

Version:

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

679 lines (678 loc) 29.9 kB
import { z } from "zod"; import sql from "mssql"; export function registerSchemaTools(server, db) { // listSchemas: returns all schema names server.tool("listSchemas", "Lists all available schemas in the database", async () => { try { const result = await db.request().query(`SELECT SCHEMA_NAME AS schema_name FROM INFORMATION_SCHEMA.SCHEMATA`); const schemas = result.recordset.map((r) => r.schema_name); return { content: [ { type: "text", text: JSON.stringify({ schemas }) } ] }; } catch (error) { console.error("Error listing schemas:", error); throw new Error(`Failed to list schemas: ${error.message}`); } }); // readSchema: tables + columns in a schema (or all schemas if not specified) server.tool("readSchema", "Read Schema Information", { schema: z.string().optional().describe("Name of the schema to inspect (defaults to all schemas)") }, async ({ schema }) => { try { let tablesQuery; let columnsQuery; if (schema) { // Get tables in the specific schema const tablesResult = await db.request() .input('schema', sql.NVarChar, schema) .query(`SELECT TABLE_SCHEMA AS schema_name, TABLE_NAME AS table_name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @schema`); // Get columns for all tables in the specific schema const columnsResult = await db.request() .input('schema', sql.NVarChar, schema) .query(`SELECT TABLE_SCHEMA AS schema_name, TABLE_NAME AS table_name, COLUMN_NAME AS column_name, DATA_TYPE AS data_type FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @schema`); const result = { schema: schema, tables: tablesResult.recordset.map((r) => r.table_name), columns: columnsResult.recordset.map((r) => ({ table: r.table_name, name: r.column_name, type: r.data_type })) }; return { content: [ { type: "text", text: JSON.stringify(result) } ] }; } else { // Get tables from all schemas const tablesResult = await db.request() .query(`SELECT TABLE_SCHEMA AS schema_name, TABLE_NAME AS table_name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_SCHEMA, TABLE_NAME`); // Get columns for all tables in all schemas const columnsResult = await db.request() .query(`SELECT TABLE_SCHEMA AS schema_name, TABLE_NAME AS table_name, COLUMN_NAME AS column_name, DATA_TYPE AS data_type FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA IN (SELECT DISTINCT TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE') ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION`); // Group by schema const schemaMap = new Map(); tablesResult.recordset.forEach((r) => { if (!schemaMap.has(r.schema_name)) { schemaMap.set(r.schema_name, { tables: [], columns: [] }); } schemaMap.get(r.schema_name).tables.push(r.table_name); }); columnsResult.recordset.forEach((r) => { if (schemaMap.has(r.schema_name)) { schemaMap.get(r.schema_name).columns.push({ table: r.table_name, name: r.column_name, type: r.data_type }); } }); const result = { schemas: Array.from(schemaMap.entries()).map(([schemaName, data]) => ({ schema: schemaName, ...data })) }; return { content: [ { type: "text", text: JSON.stringify(result) } ] }; } } catch (error) { console.error(`Error reading schema${schema ? ' ' + schema : 's'}:`, error); throw new Error(`Failed to read schema${schema ? ' ' + schema : 's'}: ${error.message}`); } }); // listTables: List all tables with metadata server.tool("listTables", "List all tables in the database with metadata", { schema: z.string().optional().describe("Filter by schema name"), pattern: z.string().optional().describe("Filter by table name pattern (supports SQL wildcards)") }, async ({ schema, pattern }) => { try { const request = db.request(); let query = ` SELECT t.TABLE_SCHEMA as schema_name, t.TABLE_NAME as table_name, t.TABLE_TYPE as table_type, (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS c WHERE c.TABLE_SCHEMA = t.TABLE_SCHEMA AND c.TABLE_NAME = t.TABLE_NAME) as column_count, ISNULL(p.rows, 0) as row_count FROM INFORMATION_SCHEMA.TABLES t LEFT JOIN sys.tables st ON st.name = t.TABLE_NAME LEFT JOIN sys.schemas ss ON ss.schema_id = st.schema_id AND ss.name = t.TABLE_SCHEMA LEFT JOIN sys.partitions p ON p.object_id = st.object_id AND p.index_id IN (0,1) WHERE t.TABLE_TYPE = 'BASE TABLE' `; const conditions = []; let paramIndex = 1; if (schema) { conditions.push(`t.TABLE_SCHEMA = @param${paramIndex}`); request.input(`param${paramIndex}`, sql.NVarChar, schema); paramIndex++; } if (pattern) { conditions.push(`t.TABLE_NAME LIKE @param${paramIndex}`); request.input(`param${paramIndex}`, sql.NVarChar, pattern.replace('*', '%')); paramIndex++; } if (conditions.length > 0) { query += ` AND ${conditions.join(' AND ')}`; } query += ' ORDER BY t.TABLE_SCHEMA, t.TABLE_NAME'; const result = await request.query(query); return { content: [{ type: "text", text: JSON.stringify({ tables: result.recordset, total: result.recordset.length }) }] }; } catch (error) { console.error("Error listing tables:", error); throw new Error(`Failed to list tables: ${error.message}`); } }); // listViews: List all views server.tool("listViews", "List all views in the database", { schema: z.string().optional().describe("Filter by schema name"), pattern: z.string().optional().describe("Filter by view name pattern (supports SQL wildcards)") }, async ({ schema, pattern }) => { try { const request = db.request(); let query = ` SELECT TABLE_SCHEMA as schema_name, TABLE_NAME as view_name, (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS c WHERE c.TABLE_SCHEMA = v.TABLE_SCHEMA AND c.TABLE_NAME = v.TABLE_NAME) as column_count FROM INFORMATION_SCHEMA.VIEWS v WHERE 1=1 `; const conditions = []; let paramIndex = 1; if (schema) { conditions.push(`TABLE_SCHEMA = @param${paramIndex}`); request.input(`param${paramIndex}`, sql.NVarChar, schema); paramIndex++; } if (pattern) { conditions.push(`TABLE_NAME LIKE @param${paramIndex}`); request.input(`param${paramIndex}`, sql.NVarChar, pattern.replace('*', '%')); paramIndex++; } if (conditions.length > 0) { query += ` AND ${conditions.join(' AND ')}`; } query += ' ORDER BY TABLE_SCHEMA, TABLE_NAME'; const result = await request.query(query); return { content: [{ type: "text", text: JSON.stringify({ views: result.recordset, total: result.recordset.length }) }] }; } catch (error) { console.error("Error listing views:", error); throw new Error(`Failed to list views: ${error.message}`); } }); // listFunctions: List all functions server.tool("listFunctions", "List all user-defined functions in the database", { schema: z.string().optional().describe("Filter by schema name"), pattern: z.string().optional().describe("Filter by function name pattern (supports SQL wildcards)"), type: z.enum(["SCALAR", "TABLE", "ALL"]).optional().default("ALL").describe("Function type filter") }, async ({ schema, pattern, type = "ALL" }) => { try { const request = db.request(); let query = ` SELECT ROUTINE_SCHEMA as schema_name, ROUTINE_NAME as function_name, ROUTINE_TYPE as routine_type, DATA_TYPE as return_type, CREATED as created_date, LAST_ALTERED as modified_date FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_TYPE = 'FUNCTION' `; const conditions = []; let paramIndex = 1; if (schema) { conditions.push(`ROUTINE_SCHEMA = @param${paramIndex}`); request.input(`param${paramIndex}`, sql.NVarChar, schema); paramIndex++; } if (pattern) { conditions.push(`ROUTINE_NAME LIKE @param${paramIndex}`); request.input(`param${paramIndex}`, sql.NVarChar, pattern.replace('*', '%')); paramIndex++; } // Additional filter based on function type if (type !== "ALL") { const typeQuery = ` EXISTS ( SELECT 1 FROM sys.objects o JOIN sys.schemas s ON o.schema_id = s.schema_id WHERE s.name = ROUTINE_SCHEMA AND o.name = ROUTINE_NAME AND o.type ${type === "SCALAR" ? "IN ('FN', 'FS')" : "IN ('TF', 'IF')"} ) `; conditions.push(typeQuery); } if (conditions.length > 0) { query += ` AND ${conditions.join(' AND ')}`; } query += ' ORDER BY ROUTINE_SCHEMA, ROUTINE_NAME'; const result = await request.query(query); return { content: [{ type: "text", text: JSON.stringify({ functions: result.recordset, total: result.recordset.length }) }] }; } catch (error) { console.error("Error listing functions:", error); throw new Error(`Failed to list functions: ${error.message}`); } }); // listIndexes: List indexes for a table server.tool("listIndexes", "List all indexes for a specific table", { table: z.string().describe("Table name (can include schema.table)"), includeColumns: z.boolean().optional().default(true).describe("Include column information") }, async ({ table, includeColumns = true }) => { try { const request = db.request(); // Parse table name const parts = table.split('.'); const tableName = parts.length === 2 ? parts[1] : table; const schemaName = parts.length === 2 ? parts[0] : 'dbo'; request.input('schema', sql.NVarChar, schemaName); request.input('table', sql.NVarChar, tableName); let query = ` SELECT s.name as schema_name, t.name as table_name, i.name as index_name, i.type_desc as index_type, i.is_primary_key, i.is_unique, i.is_unique_constraint, i.fill_factor, (SELECT SUM(p.rows) FROM sys.partitions p WHERE p.object_id = t.object_id AND p.index_id = i.index_id) as row_count `; if (includeColumns) { query += `, STUFF(( SELECT ',' + c.name + CASE WHEN ic.is_descending_key = 1 THEN ' DESC' ELSE ' ASC' END FROM sys.index_columns ic JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id WHERE ic.object_id = i.object_id AND ic.index_id = i.index_id ORDER BY ic.key_ordinal FOR XML PATH('') ), 1, 1, '') as columns `; } query += ` FROM sys.indexes i JOIN sys.tables t ON i.object_id = t.object_id JOIN sys.schemas s ON t.schema_id = s.schema_id WHERE s.name = @schema AND t.name = @table ORDER BY i.name `; const result = await request.query(query); return { content: [{ type: "text", text: JSON.stringify({ table: `${schemaName}.${tableName}`, indexes: result.recordset, total: result.recordset.length }) }] }; } catch (error) { console.error(`Error listing indexes for ${table}:`, error); throw new Error(`Failed to list indexes for ${table}: ${error.message}`); } }); // listConstraints: List constraints server.tool("listConstraints", "List all constraints for a table or schema", { table: z.string().optional().describe("Table name (can include schema.table)"), schema: z.string().optional().describe("Schema name (if table not specified)"), type: z.enum(["PRIMARY KEY", "FOREIGN KEY", "CHECK", "UNIQUE", "DEFAULT", "ALL"]).optional().default("ALL") }, async ({ table, schema, type = "ALL" }) => { try { const request = db.request(); let query = ` SELECT tc.TABLE_SCHEMA as schema_name, tc.TABLE_NAME as table_name, tc.CONSTRAINT_NAME as constraint_name, tc.CONSTRAINT_TYPE as constraint_type, CASE WHEN tc.CONSTRAINT_TYPE = 'FOREIGN KEY' THEN (SELECT STUFF(( SELECT ',' + COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE CONSTRAINT_NAME = tc.CONSTRAINT_NAME ORDER BY ORDINAL_POSITION FOR XML PATH('') ), 1, 1, '') + ' -> ' + rc.UNIQUE_CONSTRAINT_SCHEMA + '.' + (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME) + '(' + STUFF(( SELECT ',' + kcu2.COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu2 WHERE kcu2.CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME ORDER BY kcu2.ORDINAL_POSITION FOR XML PATH('') ), 1, 1, '') + ')' FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc WHERE rc.CONSTRAINT_NAME = tc.CONSTRAINT_NAME) WHEN tc.CONSTRAINT_TYPE IN ('PRIMARY KEY', 'UNIQUE') THEN STUFF(( SELECT ',' + COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE CONSTRAINT_NAME = tc.CONSTRAINT_NAME ORDER BY ORDINAL_POSITION FOR XML PATH('') ), 1, 1, '') ELSE '' END as constraint_definition FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc WHERE 1=1 `; const conditions = []; let paramIndex = 1; if (table) { const parts = table.split('.'); const tableName = parts.length === 2 ? parts[1] : table; const schemaName = parts.length === 2 ? parts[0] : 'dbo'; conditions.push(`tc.TABLE_SCHEMA = @param${paramIndex}`); request.input(`param${paramIndex}`, sql.NVarChar, schemaName); paramIndex++; conditions.push(`tc.TABLE_NAME = @param${paramIndex}`); request.input(`param${paramIndex}`, sql.NVarChar, tableName); paramIndex++; } else if (schema) { conditions.push(`tc.TABLE_SCHEMA = @param${paramIndex}`); request.input(`param${paramIndex}`, sql.NVarChar, schema); paramIndex++; } if (type !== "ALL") { conditions.push(`tc.CONSTRAINT_TYPE = @param${paramIndex}`); request.input(`param${paramIndex}`, sql.NVarChar, type); paramIndex++; } if (conditions.length > 0) { query += ` AND ${conditions.join(' AND ')}`; } query += ' ORDER BY tc.TABLE_SCHEMA, tc.TABLE_NAME, tc.CONSTRAINT_TYPE, tc.CONSTRAINT_NAME'; const result = await request.query(query); return { content: [{ type: "text", text: JSON.stringify({ constraints: result.recordset, total: result.recordset.length }) }] }; } catch (error) { console.error("Error listing constraints:", error); throw new Error(`Failed to list constraints: ${error.message}`); } }); // listTriggers: List triggers server.tool("listTriggers", "List all triggers in the database", { table: z.string().optional().describe("Table name (can include schema.table)"), schema: z.string().optional().describe("Schema name"), pattern: z.string().optional().describe("Trigger name pattern (supports SQL wildcards)") }, async ({ table, schema, pattern }) => { try { const request = db.request(); let query = ` SELECT s.name as schema_name, OBJECT_NAME(t.parent_id) as table_name, t.name as trigger_name, t.is_disabled, t.is_instead_of_trigger, CASE WHEN t.is_instead_of_trigger = 1 THEN 'INSTEAD OF' ELSE 'AFTER' END as trigger_timing, STUFF(( SELECT ',' + CASE te.type_desc WHEN 'INSERT' THEN 'INSERT' WHEN 'UPDATE' THEN 'UPDATE' WHEN 'DELETE' THEN 'DELETE' END FROM sys.trigger_events te WHERE te.object_id = t.object_id FOR XML PATH('') ), 1, 1, '') as trigger_events, t.create_date, t.modify_date FROM sys.triggers t JOIN sys.objects o ON t.parent_id = o.object_id JOIN sys.schemas s ON o.schema_id = s.schema_id WHERE t.parent_class = 1 `; const conditions = []; let paramIndex = 1; if (table) { const parts = table.split('.'); const tableName = parts.length === 2 ? parts[1] : table; const schemaName = parts.length === 2 ? parts[0] : schema || 'dbo'; conditions.push(`s.name = @param${paramIndex}`); request.input(`param${paramIndex}`, sql.NVarChar, schemaName); paramIndex++; conditions.push(`OBJECT_NAME(t.parent_id) = @param${paramIndex}`); request.input(`param${paramIndex}`, sql.NVarChar, tableName); paramIndex++; } else if (schema) { conditions.push(`s.name = @param${paramIndex}`); request.input(`param${paramIndex}`, sql.NVarChar, schema); paramIndex++; } if (pattern) { conditions.push(`t.name LIKE @param${paramIndex}`); request.input(`param${paramIndex}`, sql.NVarChar, pattern.replace('*', '%')); paramIndex++; } if (conditions.length > 0) { query += ` AND ${conditions.join(' AND ')}`; } query += ' ORDER BY s.name, OBJECT_NAME(t.parent_id), t.name'; const result = await request.query(query); return { content: [{ type: "text", text: JSON.stringify({ triggers: result.recordset, total: result.recordset.length }) }] }; } catch (error) { console.error("Error listing triggers:", error); throw new Error(`Failed to list triggers: ${error.message}`); } }); // getTableSchema: Get detailed schema information for a specific table server.tool("getTableSchema", "Get detailed schema/structure information for a specific table", { table: z.string().describe("Table name (can include schema.table)") }, async ({ table }) => { try { const request = db.request(); // Parse table name const parts = table.split('.'); const tableName = parts.length === 2 ? parts[1] : table; const schemaName = parts.length === 2 ? parts[0] : 'dbo'; request.input('schema', sql.NVarChar, schemaName); request.input('table', sql.NVarChar, tableName); // Get detailed column information const columnsResult = await request.query(` SELECT c.COLUMN_NAME as column_name, c.DATA_TYPE as data_type, c.CHARACTER_MAXIMUM_LENGTH as max_length, c.NUMERIC_PRECISION as precision, c.NUMERIC_SCALE as scale, c.IS_NULLABLE as is_nullable, c.COLUMN_DEFAULT as default_value, c.ORDINAL_POSITION as ordinal_position, CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 'YES' ELSE 'NO' END as is_primary_key, CASE WHEN fk.COLUMN_NAME IS NOT NULL THEN 'YES' ELSE 'NO' END as is_foreign_key, CASE WHEN EXISTS ( SELECT 1 FROM sys.identity_columns ic JOIN sys.columns sc ON ic.object_id = sc.object_id AND ic.column_id = sc.column_id JOIN sys.tables st ON sc.object_id = st.object_id JOIN sys.schemas ss ON st.schema_id = ss.schema_id WHERE ss.name = @schema AND st.name = @table AND sc.name = c.COLUMN_NAME ) THEN 'YES' ELSE 'NO' END as is_identity, CASE WHEN EXISTS ( SELECT 1 FROM sys.computed_columns cc JOIN sys.columns sc ON cc.object_id = sc.object_id AND cc.column_id = sc.column_id JOIN sys.tables st ON sc.object_id = st.object_id JOIN sys.schemas ss ON st.schema_id = ss.schema_id WHERE ss.name = @schema AND st.name = @table AND sc.name = c.COLUMN_NAME ) THEN 'YES' ELSE 'NO' END as is_computed FROM INFORMATION_SCHEMA.COLUMNS c LEFT JOIN ( SELECT DISTINCT kcu.TABLE_SCHEMA, kcu.TABLE_NAME, kcu.COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME AND kcu.TABLE_SCHEMA = tc.TABLE_SCHEMA AND kcu.TABLE_NAME = tc.TABLE_NAME WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' ) pk ON c.TABLE_SCHEMA = pk.TABLE_SCHEMA AND c.TABLE_NAME = pk.TABLE_NAME AND c.COLUMN_NAME = pk.COLUMN_NAME LEFT JOIN ( SELECT DISTINCT kcu.TABLE_SCHEMA, kcu.TABLE_NAME, kcu.COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME AND kcu.TABLE_SCHEMA = tc.TABLE_SCHEMA AND kcu.TABLE_NAME = tc.TABLE_NAME WHERE tc.CONSTRAINT_TYPE = 'FOREIGN KEY' ) fk ON c.TABLE_SCHEMA = fk.TABLE_SCHEMA AND c.TABLE_NAME = fk.TABLE_NAME AND c.COLUMN_NAME = fk.COLUMN_NAME WHERE c.TABLE_SCHEMA = @schema AND c.TABLE_NAME = @table ORDER BY c.ORDINAL_POSITION `); // Get table metadata const tableInfoResult = await request.query(` SELECT s.name as schema_name, t.name as table_name, t.create_date, t.modify_date, p.rows as row_count, CAST(ROUND(((SUM(a.total_pages) * 8) / 1024.00), 2) AS NUMERIC(36, 2)) as size_mb FROM sys.tables t JOIN sys.schemas s ON t.schema_id = s.schema_id JOIN sys.indexes i ON t.object_id = i.object_id JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id JOIN sys.allocation_units a ON p.partition_id = a.container_id WHERE s.name = @schema AND t.name = @table AND i.index_id <= 1 GROUP BY s.name, t.name, t.create_date, t.modify_date, p.rows `); // Get primary key information const primaryKeyResult = await request.query(` SELECT tc.CONSTRAINT_NAME as constraint_name, STUFF(( SELECT ',' + COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu WHERE kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME ORDER BY ORDINAL_POSITION FOR XML PATH('') ), 1, 1, '') as columns FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc WHERE tc.TABLE_SCHEMA = @schema AND tc.TABLE_NAME = @table AND tc.CONSTRAINT_TYPE = 'PRIMARY KEY' `); // Get foreign keys const foreignKeysResult = await request.query(` SELECT fk.CONSTRAINT_NAME as constraint_name, STUFF(( SELECT ',' + COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE CONSTRAINT_NAME = fk.CONSTRAINT_NAME ORDER BY ORDINAL_POSITION FOR XML PATH('') ), 1, 1, '') as columns, rc.UNIQUE_CONSTRAINT_SCHEMA + '.' + (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME) as referenced_table, STUFF(( SELECT ',' + kcu2.COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu2 WHERE kcu2.CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME ORDER BY kcu2.ORDINAL_POSITION FOR XML PATH('') ), 1, 1, '') as referenced_columns FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc ON fk.CONSTRAINT_NAME = rc.CONSTRAINT_NAME WHERE fk.TABLE_SCHEMA = @schema AND fk.TABLE_NAME = @table AND fk.CONSTRAINT_TYPE = 'FOREIGN KEY' `); if (columnsResult.recordset.length === 0) { throw new Error(`Table ${schemaName}.${tableName} not found`); } const result = { table: { schema: schemaName, name: tableName, ...tableInfoResult.recordset[0] }, columns: columnsResult.recordset, primaryKey: primaryKeyResult.recordset[0] || null, foreignKeys: foreignKeysResult.recordset, columnCount: columnsResult.recordset.length }; return { content: [{ type: "text", text: JSON.stringify(result) }] }; } catch (error) { console.error(`Error getting table schema for ${table}:`, error); throw new Error(`Failed to get table schema for ${table}: ${error.message}`); } }); }