UNPKG

@bluspace/mssql-mcp-server

Version:

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

203 lines (202 loc) 8.83 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 parse table reference (can include alias) function parseTableRef(tableRef) { if (typeof tableRef === 'string') { const parts = tableRef.split(/\s+/); if (parts.length === 2) { return { table: parts[0], alias: parts[1] }; } return { table: tableRef }; } return tableRef; } // 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)}`; } export function registerQueryBuilderTools(server, db) { server.tool("queryBuilder", "Build and execute complex SQL queries", { select: z.union([ z.array(z.string()), z.literal("*") ]).describe("Columns to select or '*' for all"), from: z.union([ z.string(), z.object({ table: z.string(), alias: z.string().optional() }) ]).describe("Table to query from with optional alias"), joins: z.array(z.object({ type: z.enum(["INNER", "LEFT", "RIGHT", "FULL"]).optional().default("INNER"), table: z.string().describe("Table to join (can include alias like 'Orders o')"), on: z.union([ z.string(), z.object({ left: z.string(), right: z.string() }) ]).describe("Join condition") })).optional().describe("Join clauses"), where: z.union([ z.record(z.any()), z.array(z.object({ column: z.string(), operator: z.enum(["=", "!=", ">", "<", ">=", "<=", "LIKE", "IN", "BETWEEN", "IS NULL", "IS NOT NULL"]), value: z.any().optional(), values: z.array(z.any()).optional() })) ]).optional().describe("WHERE conditions"), groupBy: z.array(z.string()).optional().describe("GROUP BY columns"), having: z.string().optional().describe("HAVING clause"), orderBy: z.array(z.object({ column: z.string(), direction: z.enum(["ASC", "DESC"]).optional().default("ASC") })).optional().describe("ORDER BY columns"), limit: z.number().optional().describe("Maximum rows to return"), offset: z.number().optional().describe("Number of rows to skip") }, async ({ select, from, joins, where, groupBy, having, orderBy, limit, offset }) => { try { const request = new sql.Request(db); let query = ""; let paramIndex = 1; // Build SELECT clause if (select === "*") { query += "SELECT *"; } else { query += `SELECT ${select.join(", ")}`; } // Build FROM clause const fromTable = parseTableRef(from); query += ` FROM ${formatTableName(fromTable.table)}`; if (fromTable.alias) { query += ` AS ${fromTable.alias}`; } // Build JOIN clauses if (joins && joins.length > 0) { for (const join of joins) { const joinTable = parseTableRef(join.table); query += ` ${join.type || 'INNER'} JOIN ${formatTableName(joinTable.table)}`; if (joinTable.alias) { query += ` AS ${joinTable.alias}`; } if (typeof join.on === 'string') { query += ` ON ${join.on}`; } else { query += ` ON ${join.on.left} = ${join.on.right}`; } } } // Build WHERE clause if (where) { const conditions = []; if (Array.isArray(where)) { // Advanced where conditions for (const condition of where) { const paramName = `param${paramIndex++}`; switch (condition.operator) { case "IS NULL": conditions.push(`${condition.column} IS NULL`); break; case "IS NOT NULL": conditions.push(`${condition.column} IS NOT NULL`); break; case "IN": if (condition.values && condition.values.length > 0) { const inParams = condition.values.map((_, i) => `@param${paramIndex + i}`); condition.values.forEach((val, i) => { request.input(`param${paramIndex + i}`, val); }); paramIndex += condition.values.length; conditions.push(`${condition.column} IN (${inParams.join(", ")})`); } break; case "BETWEEN": if (condition.values && condition.values.length === 2) { request.input(`param${paramIndex}`, condition.values[0]); request.input(`param${paramIndex + 1}`, condition.values[1]); conditions.push(`${condition.column} BETWEEN @param${paramIndex} AND @param${paramIndex + 1}`); paramIndex += 2; } break; default: request.input(paramName, condition.value); conditions.push(`${condition.column} ${condition.operator} @${paramName}`); } } } else { // Simple where conditions (key-value pairs) for (const [column, value] of Object.entries(where)) { const paramName = `param${paramIndex++}`; request.input(paramName, value); conditions.push(`${column} = @${paramName}`); } } if (conditions.length > 0) { query += ` WHERE ${conditions.join(" AND ")}`; } } // Build GROUP BY clause if (groupBy && groupBy.length > 0) { query += ` GROUP BY ${groupBy.join(", ")}`; } // Build HAVING clause if (having) { query += ` HAVING ${having}`; } // Build ORDER BY clause if (orderBy && orderBy.length > 0) { const orderClauses = orderBy.map(o => `${o.column} ${o.direction || 'ASC'}`); query += ` ORDER BY ${orderClauses.join(", ")}`; } // Handle LIMIT/OFFSET (SQL Server style) if (offset || limit) { if (!orderBy || orderBy.length === 0) { // SQL Server requires ORDER BY for OFFSET/FETCH query += " ORDER BY (SELECT NULL)"; } if (offset) { query += ` OFFSET ${offset} ROWS`; } else { query += " OFFSET 0 ROWS"; } if (limit) { query += ` FETCH NEXT ${limit} ROWS ONLY`; } } else if (limit && !offset) { // Use TOP if only limit is specified query = query.replace(/^SELECT/, `SELECT TOP (${limit})`); } // Execute query const result = await request.query(query); return { content: [{ type: "text", text: JSON.stringify({ query: query, rows: result.recordset, rowCount: result.recordset.length }) }] }; } catch (error) { console.error("Error in queryBuilder:", error); throw new Error(`Query builder failed: ${error.message}`); } }); }