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