feat: 打通出库作业链路
This commit is contained in:
39
tools/mcp/sqlserver-mcp-server/README.md
Normal file
39
tools/mcp/sqlserver-mcp-server/README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# SQL Server MCP Server
|
||||
|
||||
Project-local MCP server for Codex. It connects to SQL Server using the existing `.cursorrules-mcp.json` configuration at the repository root.
|
||||
|
||||
## Tools
|
||||
|
||||
- `sqlserver_list_databases`
|
||||
- `sqlserver_list_tables`
|
||||
- `sqlserver_describe_table`
|
||||
- `sqlserver_read_data`
|
||||
- `sqlserver_insert_rows`
|
||||
- `sqlserver_update_rows`
|
||||
- `sqlserver_delete_rows`
|
||||
- `sqlserver_create_table`
|
||||
- `sqlserver_drop_table`
|
||||
- `sqlserver_list_procedures`
|
||||
- `sqlserver_describe_procedure`
|
||||
- `sqlserver_create_or_alter_procedure`
|
||||
- `sqlserver_drop_procedure`
|
||||
- `sqlserver_execute_procedure`
|
||||
|
||||
## Safety
|
||||
|
||||
- Connection secrets stay in `.cursorrules-mcp.json`, which is ignored by git.
|
||||
- `READONLY=true` blocks writes.
|
||||
- `DB_ENCRYPT=true` enables encrypted SQL Server connections when required.
|
||||
- Table/procedure DDL requires `ENABLE_DDL=true`.
|
||||
- Update/delete require object-based `where` criteria.
|
||||
- Destructive tools require `confirm: true`.
|
||||
- Database selection is restricted to the configured `DATABASES` allowlist.
|
||||
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Codex starts the server through `start.ps1`.
|
||||
17
tools/mcp/sqlserver-mcp-server/dist/config.d.ts
vendored
Normal file
17
tools/mcp/sqlserver-mcp-server/dist/config.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
export interface ServerConfig {
|
||||
serverName: string;
|
||||
defaultDatabase: string;
|
||||
allowedDatabases: string[];
|
||||
user?: string;
|
||||
password?: string;
|
||||
readOnly: boolean;
|
||||
enableDdl: boolean;
|
||||
encrypt: boolean;
|
||||
trustServerCertificate: boolean;
|
||||
connectionTimeoutMs: number;
|
||||
requestTimeoutMs: number;
|
||||
maxRows: number;
|
||||
maxWriteRows: number;
|
||||
}
|
||||
export declare function loadConfig(env?: NodeJS.ProcessEnv): ServerConfig;
|
||||
//# sourceMappingURL=config.d.ts.map
|
||||
1
tools/mcp/sqlserver-mcp-server/dist/config.d.ts.map
vendored
Normal file
1
tools/mcp/sqlserver-mcp-server/dist/config.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,sBAAsB,EAAE,OAAO,CAAC;IAChC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,YAAY,CAyB7E"}
|
||||
70
tools/mcp/sqlserver-mcp-server/dist/config.js
vendored
Normal file
70
tools/mcp/sqlserver-mcp-server/dist/config.js
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
export function loadConfig(env = process.env) {
|
||||
const serverName = requireEnv(env, "SERVER_NAME");
|
||||
const databases = parseList(env.DATABASES);
|
||||
const defaultDatabase = (env.DATABASE_NAME?.trim() || databases[0] || "").trim();
|
||||
if (!defaultDatabase) {
|
||||
throw new Error("DATABASE_NAME or DATABASES must be configured.");
|
||||
}
|
||||
const allowedDatabases = mergeUnique([defaultDatabase, ...databases]);
|
||||
return {
|
||||
serverName,
|
||||
defaultDatabase,
|
||||
allowedDatabases,
|
||||
user: optional(env.DB_USER),
|
||||
password: optional(env.DB_PASSWORD),
|
||||
readOnly: parseBoolean(env.READONLY, false),
|
||||
enableDdl: parseBoolean(env.ENABLE_DDL, false),
|
||||
encrypt: parseBoolean(env.DB_ENCRYPT ?? env.ENCRYPT, false),
|
||||
trustServerCertificate: parseBoolean(env.TRUST_SERVER_CERTIFICATE, false),
|
||||
connectionTimeoutMs: parsePositiveInt(env.CONNECTION_TIMEOUT, 30) * 1000,
|
||||
requestTimeoutMs: parsePositiveInt(env.QUERY_TIMEOUT_MS, 30000),
|
||||
maxRows: parsePositiveInt(env.MAX_ROWS, 1000),
|
||||
maxWriteRows: parsePositiveInt(env.MAX_WRITE_ROWS, 100)
|
||||
};
|
||||
}
|
||||
function requireEnv(env, name) {
|
||||
const value = env[name]?.trim();
|
||||
if (!value) {
|
||||
throw new Error(`${name} must be configured.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function optional(value) {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
function parseList(value) {
|
||||
return (value ?? "")
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
function parseBoolean(value, fallback) {
|
||||
if (value === undefined || value.trim() === "") {
|
||||
return fallback;
|
||||
}
|
||||
return ["1", "true", "yes", "y", "on"].includes(value.trim().toLowerCase());
|
||||
}
|
||||
function parsePositiveInt(value, fallback) {
|
||||
if (value === undefined || value.trim() === "") {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
throw new Error(`Expected a positive integer but received '${value}'.`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
function mergeUnique(values) {
|
||||
const seen = new Set();
|
||||
const result = [];
|
||||
for (const value of values) {
|
||||
const key = value.toLowerCase();
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
result.push(value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
//# sourceMappingURL=config.js.map
|
||||
1
tools/mcp/sqlserver-mcp-server/dist/config.js.map
vendored
Normal file
1
tools/mcp/sqlserver-mcp-server/dist/config.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
||||
21
tools/mcp/sqlserver-mcp-server/dist/db.d.ts
vendored
Normal file
21
tools/mcp/sqlserver-mcp-server/dist/db.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import { type IResult } from "mssql";
|
||||
import type { ServerConfig } from "./config.js";
|
||||
import { type SqlParam } from "./sql.js";
|
||||
export type QueryResult = IResult<Record<string, unknown>>;
|
||||
export declare class SqlServerClient {
|
||||
private readonly config;
|
||||
private readonly pools;
|
||||
constructor(config: ServerConfig);
|
||||
get allowedDatabases(): string[];
|
||||
get defaultDatabase(): string;
|
||||
get readOnly(): boolean;
|
||||
get enableDdl(): boolean;
|
||||
get maxRows(): number;
|
||||
get maxWriteRows(): number;
|
||||
query(databaseName: string | undefined, statement: string, params?: SqlParam[]): Promise<QueryResult>;
|
||||
batch(databaseName: string | undefined, statement: string): Promise<QueryResult>;
|
||||
close(): Promise<void>;
|
||||
private getPool;
|
||||
}
|
||||
export declare function useDatabaseStatement(databaseName: string): string;
|
||||
//# sourceMappingURL=db.d.ts.map
|
||||
1
tools/mcp/sqlserver-mcp-server/dist/db.d.ts.map
vendored
Normal file
1
tools/mcp/sqlserver-mcp-server/dist/db.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAY,EAAiD,KAAK,OAAO,EAAgB,MAAM,OAAO,CAAC;AACvG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAwC,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AAE/E,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAE3D,qBAAa,eAAe;IAGd,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8C;gBAEvC,MAAM,EAAE,YAAY;IAEjD,IAAI,gBAAgB,IAAI,MAAM,EAAE,CAE/B;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAEK,KAAK,CACT,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,SAAS,EAAE,MAAM,EACjB,MAAM,GAAE,QAAQ,EAAO,GACtB,OAAO,CAAC,WAAW,CAAC;IAOjB,KAAK,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAKhF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAMd,OAAO;CAgBtB;AAED,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEjE"}
|
||||
81
tools/mcp/sqlserver-mcp-server/dist/db.js
vendored
Normal file
81
tools/mcp/sqlserver-mcp-server/dist/db.js
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
import sql from "mssql";
|
||||
import { quoteIdentifier, resolveDatabaseName } from "./sql.js";
|
||||
export class SqlServerClient {
|
||||
config;
|
||||
pools = new Map();
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
}
|
||||
get allowedDatabases() {
|
||||
return this.config.allowedDatabases;
|
||||
}
|
||||
get defaultDatabase() {
|
||||
return this.config.defaultDatabase;
|
||||
}
|
||||
get readOnly() {
|
||||
return this.config.readOnly;
|
||||
}
|
||||
get enableDdl() {
|
||||
return this.config.enableDdl;
|
||||
}
|
||||
get maxRows() {
|
||||
return this.config.maxRows;
|
||||
}
|
||||
get maxWriteRows() {
|
||||
return this.config.maxWriteRows;
|
||||
}
|
||||
async query(databaseName, statement, params = []) {
|
||||
const pool = await this.getPool(databaseName);
|
||||
const request = pool.request();
|
||||
bindParams(request, params);
|
||||
return request.query(statement);
|
||||
}
|
||||
async batch(databaseName, statement) {
|
||||
const pool = await this.getPool(databaseName);
|
||||
return pool.request().batch(statement);
|
||||
}
|
||||
async close() {
|
||||
const pools = await Promise.all([...this.pools.values()]);
|
||||
await Promise.all(pools.map((pool) => pool.close()));
|
||||
this.pools.clear();
|
||||
}
|
||||
async getPool(requestedDatabase) {
|
||||
const database = resolveDatabaseName(requestedDatabase, this.config.defaultDatabase, this.config.allowedDatabases);
|
||||
const key = database.toLowerCase();
|
||||
const existing = this.pools.get(key);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const poolPromise = new sql.ConnectionPool(buildSqlConfig(this.config, database)).connect();
|
||||
this.pools.set(key, poolPromise);
|
||||
return poolPromise;
|
||||
}
|
||||
}
|
||||
export function useDatabaseStatement(databaseName) {
|
||||
return `USE ${quoteIdentifier(databaseName)};`;
|
||||
}
|
||||
function buildSqlConfig(config, database) {
|
||||
return {
|
||||
server: config.serverName,
|
||||
database,
|
||||
user: config.user,
|
||||
password: config.password,
|
||||
connectionTimeout: config.connectionTimeoutMs,
|
||||
requestTimeout: config.requestTimeoutMs,
|
||||
options: {
|
||||
encrypt: config.encrypt,
|
||||
trustServerCertificate: config.trustServerCertificate
|
||||
},
|
||||
pool: {
|
||||
max: 5,
|
||||
min: 0,
|
||||
idleTimeoutMillis: 30000
|
||||
}
|
||||
};
|
||||
}
|
||||
function bindParams(request, params) {
|
||||
for (const param of params) {
|
||||
request.input(param.name, param.value);
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=db.js.map
|
||||
1
tools/mcp/sqlserver-mcp-server/dist/db.js.map
vendored
Normal file
1
tools/mcp/sqlserver-mcp-server/dist/db.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"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"}
|
||||
3
tools/mcp/sqlserver-mcp-server/dist/index.d.ts
vendored
Normal file
3
tools/mcp/sqlserver-mcp-server/dist/index.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
export {};
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
tools/mcp/sqlserver-mcp-server/dist/index.d.ts.map
vendored
Normal file
1
tools/mcp/sqlserver-mcp-server/dist/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
||||
52
tools/mcp/sqlserver-mcp-server/dist/index.js
vendored
Normal file
52
tools/mcp/sqlserver-mcp-server/dist/index.js
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env node
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { loadConfig } from "./config.js";
|
||||
import { SqlServerClient } from "./db.js";
|
||||
import { registerSqlServerTools } from "./tools.js";
|
||||
const VERSION = "1.0.0";
|
||||
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
||||
console.log(`sqlserver-mcp-server ${VERSION}
|
||||
|
||||
Local stdio MCP server for Microsoft SQL Server.
|
||||
|
||||
Required environment:
|
||||
SERVER_NAME
|
||||
DATABASE_NAME or DATABASES
|
||||
|
||||
Optional environment:
|
||||
DB_USER, DB_PASSWORD, READONLY, ENABLE_DDL, TRUST_SERVER_CERTIFICATE,
|
||||
DB_ENCRYPT, CONNECTION_TIMEOUT, QUERY_TIMEOUT_MS, MAX_ROWS, MAX_WRITE_ROWS`);
|
||||
process.exit(0);
|
||||
}
|
||||
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
||||
console.log(VERSION);
|
||||
process.exit(0);
|
||||
}
|
||||
async function main() {
|
||||
const config = loadConfig();
|
||||
const client = new SqlServerClient(config);
|
||||
const server = new McpServer({
|
||||
name: "sqlserver-mcp-server",
|
||||
version: VERSION
|
||||
});
|
||||
registerSqlServerTools(server, client);
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("sqlserver-mcp-server running via stdio");
|
||||
const shutdown = async () => {
|
||||
await client.close();
|
||||
await server.close();
|
||||
};
|
||||
process.once("SIGINT", () => {
|
||||
shutdown().finally(() => process.exit(0));
|
||||
});
|
||||
process.once("SIGTERM", () => {
|
||||
shutdown().finally(() => process.exit(0));
|
||||
});
|
||||
}
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
tools/mcp/sqlserver-mcp-server/dist/index.js.map
vendored
Normal file
1
tools/mcp/sqlserver-mcp-server/dist/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,OAAO,GAAG,OAAO,CAAC;AAExB,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IACnE,OAAO,CAAC,GAAG,CAAC,wBAAwB,OAAO;;;;;;;;;;6EAUgC,CAAC,CAAC;IAC7E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IACtE,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACrB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,sBAAsB;QAC5B,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IAEH,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEvC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,OAAO,CAAC,KAAK,CAAC,wCAAwC,CAAC,CAAC;IAExD,MAAM,QAAQ,GAAG,KAAK,IAAI,EAAE;QAC1B,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE;QAC1B,QAAQ,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE;QAC3B,QAAQ,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACtE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
||||
15
tools/mcp/sqlserver-mcp-server/dist/sql.d.ts
vendored
Normal file
15
tools/mcp/sqlserver-mcp-server/dist/sql.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface SqlParam {
|
||||
name: string;
|
||||
value: unknown;
|
||||
}
|
||||
export interface WhereClause {
|
||||
clause: string;
|
||||
params: SqlParam[];
|
||||
}
|
||||
export declare function quoteIdentifier(identifier: string): string;
|
||||
export declare function qualifyName(schemaName: string, objectName: string): string;
|
||||
export declare function assertSafeSelect(query: string): void;
|
||||
export declare function buildWhereClause(criteria: Record<string, unknown>, prefix: string): WhereClause;
|
||||
export declare function resolveDatabaseName(requested: string | undefined, defaultDatabase: string, allowedDatabases: string[]): string;
|
||||
export declare function validateProcedureDefinition(definition: string, schemaName: string, procedureName: string): void;
|
||||
//# sourceMappingURL=sql.d.ts.map
|
||||
1
tools/mcp/sqlserver-mcp-server/dist/sql.d.ts.map
vendored
Normal file
1
tools/mcp/sqlserver-mcp-server/dist/sql.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../src/sql.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,QAAQ,EAAE,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAO1D;AAED,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAkBpD;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CAsB/F;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,eAAe,EAAE,MAAM,EACvB,gBAAgB,EAAE,MAAM,EAAE,GACzB,MAAM,CAgBR;AAED,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,GACpB,IAAI,CAgBN"}
|
||||
86
tools/mcp/sqlserver-mcp-server/dist/sql.js
vendored
Normal file
86
tools/mcp/sqlserver-mcp-server/dist/sql.js
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
export function quoteIdentifier(identifier) {
|
||||
const trimmed = identifier.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error("SQL identifier must not be empty.");
|
||||
}
|
||||
return `[${trimmed.replaceAll("]", "]]")}]`;
|
||||
}
|
||||
export function qualifyName(schemaName, objectName) {
|
||||
return `${quoteIdentifier(schemaName)}.${quoteIdentifier(objectName)}`;
|
||||
}
|
||||
export function assertSafeSelect(query) {
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error("SELECT query must not be empty.");
|
||||
}
|
||||
const withoutTrailingSemicolon = trimmed.replace(/;+\s*$/u, "");
|
||||
if (withoutTrailingSemicolon.includes(";")) {
|
||||
throw new Error("Only a single SELECT statement is allowed.");
|
||||
}
|
||||
if (!/^(select|with)\b/iu.test(withoutTrailingSemicolon)) {
|
||||
throw new Error("Only SELECT queries or CTE queries starting with WITH are allowed.");
|
||||
}
|
||||
if (/\b(insert|update|delete|merge|drop|alter|create|truncate|exec|execute|grant|revoke)\b/iu.test(withoutTrailingSemicolon)) {
|
||||
throw new Error("SELECT query contains a disallowed mutating keyword.");
|
||||
}
|
||||
}
|
||||
export function buildWhereClause(criteria, prefix) {
|
||||
const entries = Object.entries(criteria).filter(([, value]) => value !== undefined);
|
||||
if (entries.length === 0) {
|
||||
throw new Error("WHERE criteria must include at least one field.");
|
||||
}
|
||||
const params = [];
|
||||
const parts = entries.map(([key, value], index) => {
|
||||
const column = quoteIdentifier(key);
|
||||
if (value === null) {
|
||||
return `${column} IS NULL`;
|
||||
}
|
||||
const name = `${prefix}${index}`;
|
||||
params.push({ name, value });
|
||||
return `${column} = @${name}`;
|
||||
});
|
||||
return {
|
||||
clause: parts.join(" AND "),
|
||||
params
|
||||
};
|
||||
}
|
||||
export function resolveDatabaseName(requested, defaultDatabase, allowedDatabases) {
|
||||
const databaseName = (requested?.trim() || defaultDatabase.trim());
|
||||
if (!databaseName) {
|
||||
throw new Error("Database name is required.");
|
||||
}
|
||||
if (allowedDatabases.length === 0) {
|
||||
return databaseName;
|
||||
}
|
||||
const match = allowedDatabases.find((allowed) => allowed.toLowerCase() === databaseName.toLowerCase());
|
||||
if (!match) {
|
||||
throw new Error(`Database '${databaseName}' is not in the configured allowed database list.`);
|
||||
}
|
||||
return match;
|
||||
}
|
||||
export function validateProcedureDefinition(definition, schemaName, procedureName) {
|
||||
const trimmed = definition.trim();
|
||||
if (!/^(create\s+(or\s+alter\s+)?|alter\s+)proc(edure)?\b/iu.test(trimmed)) {
|
||||
throw new Error("Stored procedure definition must start with CREATE PROCEDURE, ALTER PROCEDURE, or CREATE OR ALTER PROCEDURE.");
|
||||
}
|
||||
const nameMatch = trimmed.match(/^(?:create\s+(?:or\s+alter\s+)?|alter\s+)proc(?:edure)?\s+([^\s(@;]+)/iu);
|
||||
if (!nameMatch) {
|
||||
throw new Error("Stored procedure definition must include a procedure name.");
|
||||
}
|
||||
const normalizedExpected = normalizeObjectName(`${schemaName}.${procedureName}`);
|
||||
const normalizedActual = normalizeObjectName(nameMatch[1]);
|
||||
if (normalizedActual !== normalizedExpected) {
|
||||
throw new Error(`Stored procedure definition name '${nameMatch[1]}' does not match requested name '${schemaName}.${procedureName}'.`);
|
||||
}
|
||||
}
|
||||
function normalizeObjectName(name) {
|
||||
return name
|
||||
.replaceAll("[", "")
|
||||
.replaceAll("]", "")
|
||||
.replaceAll("\"", "")
|
||||
.split(".")
|
||||
.map((part) => part.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
.join(".");
|
||||
}
|
||||
//# sourceMappingURL=sql.js.map
|
||||
1
tools/mcp/sqlserver-mcp-server/dist/sql.js.map
vendored
Normal file
1
tools/mcp/sqlserver-mcp-server/dist/sql.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"sql.js","sourceRoot":"","sources":["../src/sql.ts"],"names":[],"mappings":"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"}
|
||||
4
tools/mcp/sqlserver-mcp-server/dist/tools.d.ts
vendored
Normal file
4
tools/mcp/sqlserver-mcp-server/dist/tools.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import type { SqlServerClient } from "./db.js";
|
||||
export declare function registerSqlServerTools(server: McpServer, client: SqlServerClient): void;
|
||||
//# sourceMappingURL=tools.d.ts.map
|
||||
1
tools/mcp/sqlserver-mcp-server/dist/tools.d.ts.map
vendored
Normal file
1
tools/mcp/sqlserver-mcp-server/dist/tools.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAwB/C,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAogBvF"}
|
||||
533
tools/mcp/sqlserver-mcp-server/dist/tools.js
vendored
Normal file
533
tools/mcp/sqlserver-mcp-server/dist/tools.js
vendored
Normal file
@@ -0,0 +1,533 @@
|
||||
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
|
||||
1
tools/mcp/sqlserver-mcp-server/dist/tools.js.map
vendored
Normal file
1
tools/mcp/sqlserver-mcp-server/dist/tools.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3434
tools/mcp/sqlserver-mcp-server/package-lock.json
generated
Normal file
3434
tools/mcp/sqlserver-mcp-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
tools/mcp/sqlserver-mcp-server/package.json
Normal file
26
tools/mcp/sqlserver-mcp-server/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "sqlserver-mcp-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Project-local MCP server for Microsoft SQL Server table and stored procedure operations.",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start": "node dist/index.js",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.28.0",
|
||||
"mssql": "^11.0.1",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mssql": "^12.3.0",
|
||||
"@types/node": "^22.10.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vitest": "^2.1.0"
|
||||
}
|
||||
}
|
||||
95
tools/mcp/sqlserver-mcp-server/src/config.ts
Normal file
95
tools/mcp/sqlserver-mcp-server/src/config.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
export interface ServerConfig {
|
||||
serverName: string;
|
||||
defaultDatabase: string;
|
||||
allowedDatabases: string[];
|
||||
user?: string;
|
||||
password?: string;
|
||||
readOnly: boolean;
|
||||
enableDdl: boolean;
|
||||
encrypt: boolean;
|
||||
trustServerCertificate: boolean;
|
||||
connectionTimeoutMs: number;
|
||||
requestTimeoutMs: number;
|
||||
maxRows: number;
|
||||
maxWriteRows: number;
|
||||
}
|
||||
|
||||
export function loadConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig {
|
||||
const serverName = requireEnv(env, "SERVER_NAME");
|
||||
const databases = parseList(env.DATABASES);
|
||||
const defaultDatabase = (env.DATABASE_NAME?.trim() || databases[0] || "").trim();
|
||||
if (!defaultDatabase) {
|
||||
throw new Error("DATABASE_NAME or DATABASES must be configured.");
|
||||
}
|
||||
|
||||
const allowedDatabases = mergeUnique([defaultDatabase, ...databases]);
|
||||
|
||||
return {
|
||||
serverName,
|
||||
defaultDatabase,
|
||||
allowedDatabases,
|
||||
user: optional(env.DB_USER),
|
||||
password: optional(env.DB_PASSWORD),
|
||||
readOnly: parseBoolean(env.READONLY, false),
|
||||
enableDdl: parseBoolean(env.ENABLE_DDL, false),
|
||||
encrypt: parseBoolean(env.DB_ENCRYPT ?? env.ENCRYPT, false),
|
||||
trustServerCertificate: parseBoolean(env.TRUST_SERVER_CERTIFICATE, false),
|
||||
connectionTimeoutMs: parsePositiveInt(env.CONNECTION_TIMEOUT, 30) * 1000,
|
||||
requestTimeoutMs: parsePositiveInt(env.QUERY_TIMEOUT_MS, 30000),
|
||||
maxRows: parsePositiveInt(env.MAX_ROWS, 1000),
|
||||
maxWriteRows: parsePositiveInt(env.MAX_WRITE_ROWS, 100)
|
||||
};
|
||||
}
|
||||
|
||||
function requireEnv(env: NodeJS.ProcessEnv, name: string): string {
|
||||
const value = env[name]?.trim();
|
||||
if (!value) {
|
||||
throw new Error(`${name} must be configured.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function optional(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function parseList(value: string | undefined): string[] {
|
||||
return (value ?? "")
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function parseBoolean(value: string | undefined, fallback: boolean): boolean {
|
||||
if (value === undefined || value.trim() === "") {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return ["1", "true", "yes", "y", "on"].includes(value.trim().toLowerCase());
|
||||
}
|
||||
|
||||
function parsePositiveInt(value: string | undefined, fallback: number): number {
|
||||
if (value === undefined || value.trim() === "") {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
throw new Error(`Expected a positive integer but received '${value}'.`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function mergeUnique(values: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const value of values) {
|
||||
const key = value.toLowerCase();
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
result.push(value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
104
tools/mcp/sqlserver-mcp-server/src/db.ts
Normal file
104
tools/mcp/sqlserver-mcp-server/src/db.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import sql, { type config as SqlConfig, type ConnectionPool, type IResult, type Request } from "mssql";
|
||||
import type { ServerConfig } from "./config.js";
|
||||
import { quoteIdentifier, resolveDatabaseName, type SqlParam } from "./sql.js";
|
||||
|
||||
export type QueryResult = IResult<Record<string, unknown>>;
|
||||
|
||||
export class SqlServerClient {
|
||||
private readonly pools = new Map<string, Promise<ConnectionPool>>();
|
||||
|
||||
constructor(private readonly config: ServerConfig) {}
|
||||
|
||||
get allowedDatabases(): string[] {
|
||||
return this.config.allowedDatabases;
|
||||
}
|
||||
|
||||
get defaultDatabase(): string {
|
||||
return this.config.defaultDatabase;
|
||||
}
|
||||
|
||||
get readOnly(): boolean {
|
||||
return this.config.readOnly;
|
||||
}
|
||||
|
||||
get enableDdl(): boolean {
|
||||
return this.config.enableDdl;
|
||||
}
|
||||
|
||||
get maxRows(): number {
|
||||
return this.config.maxRows;
|
||||
}
|
||||
|
||||
get maxWriteRows(): number {
|
||||
return this.config.maxWriteRows;
|
||||
}
|
||||
|
||||
async query(
|
||||
databaseName: string | undefined,
|
||||
statement: string,
|
||||
params: SqlParam[] = []
|
||||
): Promise<QueryResult> {
|
||||
const pool = await this.getPool(databaseName);
|
||||
const request = pool.request();
|
||||
bindParams(request, params);
|
||||
return request.query(statement) as Promise<QueryResult>;
|
||||
}
|
||||
|
||||
async batch(databaseName: string | undefined, statement: string): Promise<QueryResult> {
|
||||
const pool = await this.getPool(databaseName);
|
||||
return pool.request().batch(statement) as Promise<QueryResult>;
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
const pools = await Promise.all([...this.pools.values()]);
|
||||
await Promise.all(pools.map((pool) => pool.close()));
|
||||
this.pools.clear();
|
||||
}
|
||||
|
||||
private async getPool(requestedDatabase: string | undefined): Promise<ConnectionPool> {
|
||||
const database = resolveDatabaseName(
|
||||
requestedDatabase,
|
||||
this.config.defaultDatabase,
|
||||
this.config.allowedDatabases
|
||||
);
|
||||
const key = database.toLowerCase();
|
||||
const existing = this.pools.get(key);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const poolPromise = new sql.ConnectionPool(buildSqlConfig(this.config, database)).connect();
|
||||
this.pools.set(key, poolPromise);
|
||||
return poolPromise;
|
||||
}
|
||||
}
|
||||
|
||||
export function useDatabaseStatement(databaseName: string): string {
|
||||
return `USE ${quoteIdentifier(databaseName)};`;
|
||||
}
|
||||
|
||||
function buildSqlConfig(config: ServerConfig, database: string): SqlConfig {
|
||||
return {
|
||||
server: config.serverName,
|
||||
database,
|
||||
user: config.user,
|
||||
password: config.password,
|
||||
connectionTimeout: config.connectionTimeoutMs,
|
||||
requestTimeout: config.requestTimeoutMs,
|
||||
options: {
|
||||
encrypt: config.encrypt,
|
||||
trustServerCertificate: config.trustServerCertificate
|
||||
},
|
||||
pool: {
|
||||
max: 5,
|
||||
min: 0,
|
||||
idleTimeoutMillis: 30000
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function bindParams(request: Request, params: SqlParam[]): void {
|
||||
for (const param of params) {
|
||||
request.input(param.name, param.value);
|
||||
}
|
||||
}
|
||||
59
tools/mcp/sqlserver-mcp-server/src/index.ts
Normal file
59
tools/mcp/sqlserver-mcp-server/src/index.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env node
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { loadConfig } from "./config.js";
|
||||
import { SqlServerClient } from "./db.js";
|
||||
import { registerSqlServerTools } from "./tools.js";
|
||||
|
||||
const VERSION = "1.0.0";
|
||||
|
||||
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
||||
console.log(`sqlserver-mcp-server ${VERSION}
|
||||
|
||||
Local stdio MCP server for Microsoft SQL Server.
|
||||
|
||||
Required environment:
|
||||
SERVER_NAME
|
||||
DATABASE_NAME or DATABASES
|
||||
|
||||
Optional environment:
|
||||
DB_USER, DB_PASSWORD, READONLY, ENABLE_DDL, TRUST_SERVER_CERTIFICATE,
|
||||
DB_ENCRYPT, CONNECTION_TIMEOUT, QUERY_TIMEOUT_MS, MAX_ROWS, MAX_WRITE_ROWS`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
||||
console.log(VERSION);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const config = loadConfig();
|
||||
const client = new SqlServerClient(config);
|
||||
const server = new McpServer({
|
||||
name: "sqlserver-mcp-server",
|
||||
version: VERSION
|
||||
});
|
||||
|
||||
registerSqlServerTools(server, client);
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("sqlserver-mcp-server running via stdio");
|
||||
|
||||
const shutdown = async () => {
|
||||
await client.close();
|
||||
await server.close();
|
||||
};
|
||||
process.once("SIGINT", () => {
|
||||
shutdown().finally(() => process.exit(0));
|
||||
});
|
||||
process.once("SIGTERM", () => {
|
||||
shutdown().finally(() => process.exit(0));
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
121
tools/mcp/sqlserver-mcp-server/src/sql.ts
Normal file
121
tools/mcp/sqlserver-mcp-server/src/sql.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
export interface SqlParam {
|
||||
name: string;
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
export interface WhereClause {
|
||||
clause: string;
|
||||
params: SqlParam[];
|
||||
}
|
||||
|
||||
export function quoteIdentifier(identifier: string): string {
|
||||
const trimmed = identifier.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error("SQL identifier must not be empty.");
|
||||
}
|
||||
|
||||
return `[${trimmed.replaceAll("]", "]]")}]`;
|
||||
}
|
||||
|
||||
export function qualifyName(schemaName: string, objectName: string): string {
|
||||
return `${quoteIdentifier(schemaName)}.${quoteIdentifier(objectName)}`;
|
||||
}
|
||||
|
||||
export function assertSafeSelect(query: string): void {
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error("SELECT query must not be empty.");
|
||||
}
|
||||
|
||||
const withoutTrailingSemicolon = trimmed.replace(/;+\s*$/u, "");
|
||||
if (withoutTrailingSemicolon.includes(";")) {
|
||||
throw new Error("Only a single SELECT statement is allowed.");
|
||||
}
|
||||
|
||||
if (!/^(select|with)\b/iu.test(withoutTrailingSemicolon)) {
|
||||
throw new Error("Only SELECT queries or CTE queries starting with WITH are allowed.");
|
||||
}
|
||||
|
||||
if (/\b(insert|update|delete|merge|drop|alter|create|truncate|exec|execute|grant|revoke)\b/iu.test(withoutTrailingSemicolon)) {
|
||||
throw new Error("SELECT query contains a disallowed mutating keyword.");
|
||||
}
|
||||
}
|
||||
|
||||
export function buildWhereClause(criteria: Record<string, unknown>, prefix: string): WhereClause {
|
||||
const entries = Object.entries(criteria).filter(([, value]) => value !== undefined);
|
||||
if (entries.length === 0) {
|
||||
throw new Error("WHERE criteria must include at least one field.");
|
||||
}
|
||||
|
||||
const params: SqlParam[] = [];
|
||||
const parts = entries.map(([key, value], index) => {
|
||||
const column = quoteIdentifier(key);
|
||||
if (value === null) {
|
||||
return `${column} IS NULL`;
|
||||
}
|
||||
|
||||
const name = `${prefix}${index}`;
|
||||
params.push({ name, value });
|
||||
return `${column} = @${name}`;
|
||||
});
|
||||
|
||||
return {
|
||||
clause: parts.join(" AND "),
|
||||
params
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveDatabaseName(
|
||||
requested: string | undefined,
|
||||
defaultDatabase: string,
|
||||
allowedDatabases: string[]
|
||||
): string {
|
||||
const databaseName = (requested?.trim() || defaultDatabase.trim());
|
||||
if (!databaseName) {
|
||||
throw new Error("Database name is required.");
|
||||
}
|
||||
|
||||
if (allowedDatabases.length === 0) {
|
||||
return databaseName;
|
||||
}
|
||||
|
||||
const match = allowedDatabases.find((allowed) => allowed.toLowerCase() === databaseName.toLowerCase());
|
||||
if (!match) {
|
||||
throw new Error(`Database '${databaseName}' is not in the configured allowed database list.`);
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
export function validateProcedureDefinition(
|
||||
definition: string,
|
||||
schemaName: string,
|
||||
procedureName: string
|
||||
): void {
|
||||
const trimmed = definition.trim();
|
||||
if (!/^(create\s+(or\s+alter\s+)?|alter\s+)proc(edure)?\b/iu.test(trimmed)) {
|
||||
throw new Error("Stored procedure definition must start with CREATE PROCEDURE, ALTER PROCEDURE, or CREATE OR ALTER PROCEDURE.");
|
||||
}
|
||||
|
||||
const nameMatch = trimmed.match(/^(?:create\s+(?:or\s+alter\s+)?|alter\s+)proc(?:edure)?\s+([^\s(@;]+)/iu);
|
||||
if (!nameMatch) {
|
||||
throw new Error("Stored procedure definition must include a procedure name.");
|
||||
}
|
||||
|
||||
const normalizedExpected = normalizeObjectName(`${schemaName}.${procedureName}`);
|
||||
const normalizedActual = normalizeObjectName(nameMatch[1]);
|
||||
if (normalizedActual !== normalizedExpected) {
|
||||
throw new Error(`Stored procedure definition name '${nameMatch[1]}' does not match requested name '${schemaName}.${procedureName}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeObjectName(name: string): string {
|
||||
return name
|
||||
.replaceAll("[", "")
|
||||
.replaceAll("]", "")
|
||||
.replaceAll("\"", "")
|
||||
.split(".")
|
||||
.map((part) => part.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
.join(".");
|
||||
}
|
||||
661
tools/mcp/sqlserver-mcp-server/src/tools.ts
Normal file
661
tools/mcp/sqlserver-mcp-server/src/tools.ts
Normal file
@@ -0,0 +1,661 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import type { SqlServerClient } from "./db.js";
|
||||
import {
|
||||
assertSafeSelect,
|
||||
buildWhereClause,
|
||||
qualifyName,
|
||||
quoteIdentifier,
|
||||
validateProcedureDefinition,
|
||||
type SqlParam
|
||||
} from "./sql.js";
|
||||
|
||||
const JsonValueSchema: z.ZodType<unknown> = 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: McpServer, client: SqlServerClient): void {
|
||||
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: SqlParam[] = [];
|
||||
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: SqlParam[] = [];
|
||||
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: unknown, responseFormat: "markdown" | "json" = "markdown") {
|
||||
const text = responseFormat === "json" ? JSON.stringify(data, null, 2) : toMarkdown(data);
|
||||
return {
|
||||
content: [{ type: "text" as const, text }]
|
||||
};
|
||||
}
|
||||
|
||||
async function handleTool(action: () => Promise<ReturnType<typeof toolResult>>) {
|
||||
try {
|
||||
return await action();
|
||||
} catch (error) {
|
||||
return {
|
||||
isError: true as const,
|
||||
content: [{ type: "text" as const, text: formatError(error) }]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function toMarkdown(data: unknown): string {
|
||||
return ["```json", JSON.stringify(data, null, 2), "```"].join("\n");
|
||||
}
|
||||
|
||||
function formatError(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return `Error: ${error.message}`;
|
||||
}
|
||||
return `Error: ${String(error)}`;
|
||||
}
|
||||
|
||||
function assertWritable(client: SqlServerClient): void {
|
||||
if (client.readOnly) {
|
||||
throw new Error("READONLY=true blocks write operations.");
|
||||
}
|
||||
}
|
||||
|
||||
function assertDdlAllowed(client: SqlServerClient): void {
|
||||
assertWritable(client);
|
||||
if (!client.enableDdl) {
|
||||
throw new Error("ENABLE_DDL=true is required for table and stored procedure DDL operations.");
|
||||
}
|
||||
}
|
||||
|
||||
function assertWriteLimit(client: SqlServerClient, rows: number): void {
|
||||
if (rows > client.maxWriteRows) {
|
||||
throw new Error(`Write affects ${rows} rows, which exceeds MAX_WRITE_ROWS=${client.maxWriteRows}.`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertAffectedRowsWithinLimit(client: SqlServerClient, rowsAffected: number[]): void {
|
||||
const total = rowsAffected.reduce((sum, count) => sum + count, 0);
|
||||
assertWriteLimit(client, total);
|
||||
}
|
||||
|
||||
async function insertRows(
|
||||
client: SqlServerClient,
|
||||
databaseName: string | undefined,
|
||||
schemaName: string,
|
||||
tableName: string,
|
||||
rows: Record<string, unknown>[]
|
||||
) {
|
||||
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: SqlParam[] = [];
|
||||
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: Record<string, unknown>, prefix: string) {
|
||||
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: string): void {
|
||||
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}'.`);
|
||||
}
|
||||
}
|
||||
42
tools/mcp/sqlserver-mcp-server/start.ps1
Normal file
42
tools/mcp/sqlserver-mcp-server/start.ps1
Normal file
@@ -0,0 +1,42 @@
|
||||
param(
|
||||
[string]$ConfigPath = "$PSScriptRoot\..\..\..\.cursorrules-mcp.json"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$resolvedConfig = Resolve-Path -LiteralPath $ConfigPath
|
||||
$config = Get-Content -Raw -LiteralPath $resolvedConfig | ConvertFrom-Json
|
||||
$serverConfig = $config.mcpServers.mssql
|
||||
|
||||
if (-not $serverConfig) {
|
||||
throw "Cannot find mcpServers.mssql in $resolvedConfig"
|
||||
}
|
||||
|
||||
foreach ($property in $serverConfig.env.PSObject.Properties) {
|
||||
[Environment]::SetEnvironmentVariable($property.Name, [string]$property.Value, "Process")
|
||||
}
|
||||
|
||||
if (-not $env:ENABLE_DDL) {
|
||||
$env:ENABLE_DDL = "true"
|
||||
}
|
||||
|
||||
if (-not $env:MAX_ROWS) {
|
||||
$env:MAX_ROWS = "1000"
|
||||
}
|
||||
|
||||
if (-not $env:MAX_WRITE_ROWS) {
|
||||
$env:MAX_WRITE_ROWS = "100"
|
||||
}
|
||||
|
||||
$entry = Join-Path $PSScriptRoot "dist\index.js"
|
||||
if (-not (Test-Path -LiteralPath $entry)) {
|
||||
throw "MCP server is not built. Run npm install and npm run build in $PSScriptRoot first."
|
||||
}
|
||||
|
||||
$nodeExe = $env:NODE_EXE
|
||||
if (-not $nodeExe) {
|
||||
$nodeExe = (Get-Command node -ErrorAction Stop).Source
|
||||
}
|
||||
|
||||
& $nodeExe $entry @args
|
||||
exit $LASTEXITCODE
|
||||
65
tools/mcp/sqlserver-mcp-server/tests/sql.test.ts
Normal file
65
tools/mcp/sqlserver-mcp-server/tests/sql.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
assertSafeSelect,
|
||||
buildWhereClause,
|
||||
quoteIdentifier,
|
||||
resolveDatabaseName,
|
||||
validateProcedureDefinition
|
||||
} from "../src/sql.js";
|
||||
|
||||
describe("SQL helper safety", () => {
|
||||
it("quotes SQL Server identifiers and escapes closing brackets", () => {
|
||||
expect(quoteIdentifier("dbo")).toBe("[dbo]");
|
||||
expect(quoteIdentifier("stock]audit")).toBe("[stock]]audit]");
|
||||
});
|
||||
|
||||
it("rejects empty identifiers", () => {
|
||||
expect(() => quoteIdentifier("")).toThrow(/identifier/i);
|
||||
});
|
||||
|
||||
it("requires safe single-statement SELECT queries", () => {
|
||||
expect(() => assertSafeSelect("select top 1 * from dbo.Users")).not.toThrow();
|
||||
expect(() => assertSafeSelect("with cte as (select 1 as id) select * from cte")).not.toThrow();
|
||||
expect(() => assertSafeSelect("update dbo.Users set name = 'x'")).toThrow(/select/i);
|
||||
expect(() => assertSafeSelect("select * from dbo.Users; drop table dbo.Users")).toThrow(/single/i);
|
||||
});
|
||||
|
||||
it("builds parameterized WHERE clauses from object criteria", () => {
|
||||
const where = buildWhereClause({ id: 1, status: "pending" }, "w");
|
||||
|
||||
expect(where.clause).toBe("[id] = @w0 AND [status] = @w1");
|
||||
expect(where.params).toEqual([
|
||||
{ name: "w0", value: 1 },
|
||||
{ name: "w1", value: "pending" }
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects empty WHERE criteria", () => {
|
||||
expect(() => buildWhereClause({}, "w")).toThrow(/where/i);
|
||||
});
|
||||
|
||||
it("restricts database selection to the configured allowlist", () => {
|
||||
expect(resolveDatabaseName(undefined, "MainDb", ["MainDb", "ReportDb"])).toBe("MainDb");
|
||||
expect(resolveDatabaseName("ReportDb", "MainDb", ["MainDb", "ReportDb"])).toBe("ReportDb");
|
||||
expect(() => resolveDatabaseName("OtherDb", "MainDb", ["MainDb", "ReportDb"])).toThrow(/allowed/i);
|
||||
});
|
||||
|
||||
it("validates stored procedure definitions before execution", () => {
|
||||
expect(() =>
|
||||
validateProcedureDefinition(
|
||||
"CREATE OR ALTER PROCEDURE [dbo].[p_test] AS BEGIN SELECT 1 END",
|
||||
"dbo",
|
||||
"p_test"
|
||||
)
|
||||
).not.toThrow();
|
||||
|
||||
expect(() => validateProcedureDefinition("SELECT 1", "dbo", "p_test")).toThrow(/procedure/i);
|
||||
expect(() =>
|
||||
validateProcedureDefinition(
|
||||
"CREATE OR ALTER PROCEDURE [dbo].[other_proc] AS BEGIN SELECT 1 END",
|
||||
"dbo",
|
||||
"p_test"
|
||||
)
|
||||
).toThrow(/name/i);
|
||||
});
|
||||
});
|
||||
20
tools/mcp/sqlserver-mcp-server/tsconfig.json
Normal file
20
tools/mcp/sqlserver-mcp-server/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "tests"]
|
||||
}
|
||||
8
tools/mcp/sqlserver-mcp-server/vitest.config.ts
Normal file
8
tools/mcp/sqlserver-mcp-server/vitest.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: false,
|
||||
include: ["tests/**/*.test.ts"]
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user