@adriancy/mcp-mssql 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +21 -0
- package/README.md +98 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +74 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +127 -0
- package/dist/readonly-sql.d.ts +8 -0
- package/dist/readonly-sql.js +66 -0
- package/dist/schemas.d.ts +16 -0
- package/dist/schemas.js +18 -0
- package/package.json +32 -0
package/.env.example
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Required
|
|
2
|
+
MSSQL_SERVER=localhost
|
|
3
|
+
MSSQL_USER=sa
|
|
4
|
+
MSSQL_PASSWORD=
|
|
5
|
+
MSSQL_DATABASE=master
|
|
6
|
+
|
|
7
|
+
# Optional (default 1433)
|
|
8
|
+
# MSSQL_PORT=1433
|
|
9
|
+
|
|
10
|
+
# TLS (defaults: encrypt true, trust cert false)
|
|
11
|
+
# MSSQL_ENCRYPT=true
|
|
12
|
+
# MSSQL_TRUST_SERVER_CERTIFICATE=false
|
|
13
|
+
|
|
14
|
+
# Safety: when false/ unset, blocks common write/DDL/exec patterns (heuristic only)
|
|
15
|
+
# MSSQL_ALLOW_WRITES=false
|
|
16
|
+
|
|
17
|
+
# Cap rows returned (SET ROWCOUNT); unset = no cap
|
|
18
|
+
# MSSQL_MAX_ROWS=5000
|
|
19
|
+
|
|
20
|
+
# Request timeout in ms (driver)
|
|
21
|
+
# MSSQL_QUERY_TIMEOUT_MS=30000
|
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# mssql-mcp
|
|
2
|
+
|
|
3
|
+
MCP server (stdio) that connects to Microsoft SQL Server using the `mssql` driver. Tool arguments are defined and validated with [Valibot](https://valibot.dev); JSON Schema for clients is generated with `@valibot/to-json-schema`.
|
|
4
|
+
|
|
5
|
+
**Implementation note:** This project uses the low-level `@modelcontextprotocol/sdk` `Server` class rather than `McpServer`, because the current SDK’s `McpServer.registerTool` path is built around Zod for schema export and validation. Tools are registered with `ListTools` / `tools/call` handlers and Valibot parsing inside the handlers.
|
|
6
|
+
|
|
7
|
+
## Tools
|
|
8
|
+
|
|
9
|
+
- **`mssql_query`** — Run a T-SQL batch; returns `recordsets` and `rowsAffected`. Honors `MSSQL_MAX_ROWS` via `SET ROWCOUNT` when set.
|
|
10
|
+
- **`mssql_list_tables`** — Base tables from `INFORMATION_SCHEMA.TABLES`, optional schema filter.
|
|
11
|
+
- **`mssql_describe_table`** — Column metadata from `INFORMATION_SCHEMA.COLUMNS`.
|
|
12
|
+
|
|
13
|
+
## Environment variables
|
|
14
|
+
|
|
15
|
+
See [`.env.example`](.env.example). Required: `MSSQL_SERVER`, `MSSQL_USER`, `MSSQL_PASSWORD`, `MSSQL_DATABASE`.
|
|
16
|
+
|
|
17
|
+
- **`MSSQL_ALLOW_WRITES`** — Default off. When off, a heuristic blocks common write/DDL/exec keywords (not a substitute for DB permissions).
|
|
18
|
+
- **`MSSQL_MAX_ROWS`** — When set, wraps batches in `SET ROWCOUNT` for `mssql_query`.
|
|
19
|
+
- **`MSSQL_ENCRYPT`** / **`MSSQL_TRUST_SERVER_CERTIFICATE`** — Passed through to the driver (`encrypt` defaults to true).
|
|
20
|
+
|
|
21
|
+
## Build and run
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm install
|
|
25
|
+
pnpm run build
|
|
26
|
+
pnpm start
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Development (no separate build):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm dev
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Do not write logs to **stdout** when running under MCP; the protocol uses stdout. Errors on startup go to stderr via `console.error`.
|
|
36
|
+
|
|
37
|
+
## Cursor MCP configuration
|
|
38
|
+
|
|
39
|
+
**Published package (`npx`):** from `@adriancy/mssql-mcp` **1.0.1+** you can run the CLI without a local path:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"mcpServers": {
|
|
44
|
+
"mssql": {
|
|
45
|
+
"command": "npx",
|
|
46
|
+
"args": ["-y", "@adriancy/mssql-mcp"],
|
|
47
|
+
"env": {
|
|
48
|
+
"MSSQL_SERVER": "localhost",
|
|
49
|
+
"MSSQL_USER": "your_user",
|
|
50
|
+
"MSSQL_PASSWORD": "your_password",
|
|
51
|
+
"MSSQL_DATABASE": "your_database",
|
|
52
|
+
"MSSQL_TRUST_SERVER_CERTIFICATE": "true"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Local checkout:** use the absolute path to `dist/index.js`:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"mcpServers": {
|
|
64
|
+
"mssql": {
|
|
65
|
+
"command": "node",
|
|
66
|
+
"args": ["/home/adrian/code/mcp/mssql-mcp/dist/index.js"],
|
|
67
|
+
"env": {
|
|
68
|
+
"MSSQL_SERVER": "localhost",
|
|
69
|
+
"MSSQL_USER": "your_user",
|
|
70
|
+
"MSSQL_PASSWORD": "your_password",
|
|
71
|
+
"MSSQL_DATABASE": "your_database",
|
|
72
|
+
"MSSQL_TRUST_SERVER_CERTIFICATE": "true"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Use **`node`** for local paths. Do **not** use **`pnpm`** as `command` for MCP: if pnpm prints errors to stdout, Cursor can show JSON parse errors because the protocol uses stdout. **`npx`** is fine for the published package: it execs the `mssql-mcp` bin (Node) after install.
|
|
80
|
+
|
|
81
|
+
### Dev mode without a build (still use `node`)
|
|
82
|
+
|
|
83
|
+
Set **`cwd`** to this repo so `node` can resolve `tsx` from `node_modules`:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"mcpServers": {
|
|
88
|
+
"mssql": {
|
|
89
|
+
"command": "node",
|
|
90
|
+
"args": ["--import", "tsx", "/home/adrian/code/mcp/src/index.ts"],
|
|
91
|
+
"cwd": "/home/adrian/code/mcp",
|
|
92
|
+
"env": { }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Fill `env` the same as in the example above. Run `pnpm install` locally first so `tsx` exists.
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type AppConfig = {
|
|
2
|
+
server: string;
|
|
3
|
+
port: number;
|
|
4
|
+
user: string;
|
|
5
|
+
password: string;
|
|
6
|
+
database: string;
|
|
7
|
+
encrypt: boolean;
|
|
8
|
+
trustServerCertificate: boolean;
|
|
9
|
+
allowWrites: boolean;
|
|
10
|
+
maxRows: number | undefined;
|
|
11
|
+
queryTimeoutMs: number | undefined;
|
|
12
|
+
};
|
|
13
|
+
export declare function loadConfig(): AppConfig;
|
|
14
|
+
export declare function mssqlDriverConfig(cfg: AppConfig): import('mssql').config;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as v from 'valibot';
|
|
2
|
+
function parseBool(raw, defaultValue) {
|
|
3
|
+
if (raw === undefined || raw === '')
|
|
4
|
+
return defaultValue;
|
|
5
|
+
return /^(1|true|yes|on)$/i.test(raw);
|
|
6
|
+
}
|
|
7
|
+
function parseIntEnv(raw, defaultValue) {
|
|
8
|
+
if (raw === undefined || raw === '')
|
|
9
|
+
return defaultValue;
|
|
10
|
+
const n = Number.parseInt(raw, 10);
|
|
11
|
+
return Number.isFinite(n) ? n : defaultValue;
|
|
12
|
+
}
|
|
13
|
+
const envSchema = v.object({
|
|
14
|
+
MSSQL_SERVER: v.pipe(v.string(), v.nonEmpty()),
|
|
15
|
+
MSSQL_USER: v.pipe(v.string(), v.nonEmpty()),
|
|
16
|
+
MSSQL_PASSWORD: v.string(),
|
|
17
|
+
MSSQL_DATABASE: v.pipe(v.string(), v.nonEmpty()),
|
|
18
|
+
MSSQL_PORT: v.optional(v.string()),
|
|
19
|
+
MSSQL_ENCRYPT: v.optional(v.string()),
|
|
20
|
+
MSSQL_TRUST_SERVER_CERTIFICATE: v.optional(v.string()),
|
|
21
|
+
MSSQL_ALLOW_WRITES: v.optional(v.string()),
|
|
22
|
+
MSSQL_MAX_ROWS: v.optional(v.string()),
|
|
23
|
+
MSSQL_QUERY_TIMEOUT_MS: v.optional(v.string()),
|
|
24
|
+
});
|
|
25
|
+
export function loadConfig() {
|
|
26
|
+
const e = v.parse(envSchema, {
|
|
27
|
+
MSSQL_SERVER: process.env.MSSQL_SERVER,
|
|
28
|
+
MSSQL_USER: process.env.MSSQL_USER,
|
|
29
|
+
MSSQL_PASSWORD: process.env.MSSQL_PASSWORD,
|
|
30
|
+
MSSQL_DATABASE: process.env.MSSQL_DATABASE,
|
|
31
|
+
MSSQL_PORT: process.env.MSSQL_PORT,
|
|
32
|
+
MSSQL_ENCRYPT: process.env.MSSQL_ENCRYPT,
|
|
33
|
+
MSSQL_TRUST_SERVER_CERTIFICATE: process.env.MSSQL_TRUST_SERVER_CERTIFICATE,
|
|
34
|
+
MSSQL_ALLOW_WRITES: process.env.MSSQL_ALLOW_WRITES,
|
|
35
|
+
MSSQL_MAX_ROWS: process.env.MSSQL_MAX_ROWS,
|
|
36
|
+
MSSQL_QUERY_TIMEOUT_MS: process.env.MSSQL_QUERY_TIMEOUT_MS,
|
|
37
|
+
});
|
|
38
|
+
const port = parseIntEnv(e.MSSQL_PORT, 1433);
|
|
39
|
+
const maxRowsRaw = e.MSSQL_MAX_ROWS;
|
|
40
|
+
const maxRows = maxRowsRaw === undefined || maxRowsRaw === ''
|
|
41
|
+
? undefined
|
|
42
|
+
: Math.max(0, parseIntEnv(maxRowsRaw, 0));
|
|
43
|
+
const timeoutRaw = e.MSSQL_QUERY_TIMEOUT_MS;
|
|
44
|
+
const queryTimeoutMs = timeoutRaw === undefined || timeoutRaw === ''
|
|
45
|
+
? undefined
|
|
46
|
+
: Math.max(0, parseIntEnv(timeoutRaw, 0));
|
|
47
|
+
return {
|
|
48
|
+
server: e.MSSQL_SERVER,
|
|
49
|
+
port,
|
|
50
|
+
user: e.MSSQL_USER,
|
|
51
|
+
password: e.MSSQL_PASSWORD,
|
|
52
|
+
database: e.MSSQL_DATABASE,
|
|
53
|
+
encrypt: parseBool(e.MSSQL_ENCRYPT, true),
|
|
54
|
+
trustServerCertificate: parseBool(e.MSSQL_TRUST_SERVER_CERTIFICATE, false),
|
|
55
|
+
allowWrites: parseBool(e.MSSQL_ALLOW_WRITES, false),
|
|
56
|
+
maxRows: maxRows === 0 ? undefined : maxRows,
|
|
57
|
+
queryTimeoutMs: queryTimeoutMs === 0 ? undefined : queryTimeoutMs,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function mssqlDriverConfig(cfg) {
|
|
61
|
+
return {
|
|
62
|
+
user: cfg.user,
|
|
63
|
+
password: cfg.password,
|
|
64
|
+
server: cfg.server,
|
|
65
|
+
port: cfg.port,
|
|
66
|
+
database: cfg.database,
|
|
67
|
+
pool: { max: 10, min: 0, idleTimeoutMillis: 30_000 },
|
|
68
|
+
options: {
|
|
69
|
+
encrypt: cfg.encrypt,
|
|
70
|
+
trustServerCertificate: cfg.trustServerCertificate,
|
|
71
|
+
},
|
|
72
|
+
requestTimeout: cfg.queryTimeoutMs,
|
|
73
|
+
};
|
|
74
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import sql from 'mssql';
|
|
3
|
+
import * as v from 'valibot';
|
|
4
|
+
import { Server } from '@modelcontextprotocol/sdk/server';
|
|
5
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { loadConfig, mssqlDriverConfig } from './config.js';
|
|
8
|
+
import { assertReadOnlySql, wrapWithRowCount } from './readonly-sql.js';
|
|
9
|
+
import { mssqlDescribeTableSchema, mssqlListTablesSchema, mssqlQuerySchema, valibotToJsonSchema, } from './schemas.js';
|
|
10
|
+
const SHARED_TOOL_PREAMBLE = 'Connection uses MSSQL_* environment variables on the MCP server process. Arbitrary SQL is dangerous; prefer a read-only or narrowly scoped database user.';
|
|
11
|
+
function toolError(message) {
|
|
12
|
+
return {
|
|
13
|
+
content: [{ type: 'text', text: message }],
|
|
14
|
+
isError: true,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function jsonResult(data) {
|
|
18
|
+
return {
|
|
19
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async function runQuery(pool, cfg, batch) {
|
|
23
|
+
assertReadOnlySql(batch, cfg.allowWrites);
|
|
24
|
+
const wrapped = wrapWithRowCount(batch, cfg.maxRows);
|
|
25
|
+
const result = await pool.request().query(wrapped);
|
|
26
|
+
const recordsets = result.recordsets;
|
|
27
|
+
return jsonResult({
|
|
28
|
+
rowsAffected: result.rowsAffected,
|
|
29
|
+
recordsets,
|
|
30
|
+
rowCountNote: cfg.maxRows !== undefined
|
|
31
|
+
? `ROWCOUNT capped at ${cfg.maxRows} per batch (SET ROWCOUNT).`
|
|
32
|
+
: undefined,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async function runListTables(pool, schemaFilter) {
|
|
36
|
+
const req = pool.request();
|
|
37
|
+
req.input('schema', sql.NVarChar(128), schemaFilter ?? null);
|
|
38
|
+
const q = `
|
|
39
|
+
SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE
|
|
40
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
41
|
+
WHERE TABLE_TYPE = 'BASE TABLE'
|
|
42
|
+
AND (@schema IS NULL OR TABLE_SCHEMA = @schema)
|
|
43
|
+
ORDER BY TABLE_SCHEMA, TABLE_NAME`;
|
|
44
|
+
const result = await req.query(q);
|
|
45
|
+
return jsonResult({ tables: result.recordset });
|
|
46
|
+
}
|
|
47
|
+
async function runDescribeTable(pool, schema, table) {
|
|
48
|
+
const req = pool.request();
|
|
49
|
+
req.input('s', sql.NVarChar(128), schema);
|
|
50
|
+
req.input('t', sql.NVarChar(128), table);
|
|
51
|
+
const q = `
|
|
52
|
+
SELECT
|
|
53
|
+
COLUMN_NAME,
|
|
54
|
+
DATA_TYPE,
|
|
55
|
+
IS_NULLABLE,
|
|
56
|
+
CHARACTER_MAXIMUM_LENGTH,
|
|
57
|
+
NUMERIC_PRECISION,
|
|
58
|
+
NUMERIC_SCALE,
|
|
59
|
+
ORDINAL_POSITION
|
|
60
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
61
|
+
WHERE TABLE_SCHEMA = @s AND TABLE_NAME = @t
|
|
62
|
+
ORDER BY ORDINAL_POSITION`;
|
|
63
|
+
const result = await req.query(q);
|
|
64
|
+
return jsonResult({ columns: result.recordset });
|
|
65
|
+
}
|
|
66
|
+
function buildTools() {
|
|
67
|
+
return [
|
|
68
|
+
{
|
|
69
|
+
name: 'mssql_query',
|
|
70
|
+
description: `${SHARED_TOOL_PREAMBLE} Runs a T-SQL batch; returns recordsets as JSON.`,
|
|
71
|
+
inputSchema: valibotToJsonSchema(mssqlQuerySchema),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'mssql_list_tables',
|
|
75
|
+
description: `${SHARED_TOOL_PREAMBLE} Lists BASE TABLE rows from INFORMATION_SCHEMA.TABLES.`,
|
|
76
|
+
inputSchema: valibotToJsonSchema(mssqlListTablesSchema),
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'mssql_describe_table',
|
|
80
|
+
description: `${SHARED_TOOL_PREAMBLE} Returns column metadata from INFORMATION_SCHEMA.COLUMNS.`,
|
|
81
|
+
inputSchema: valibotToJsonSchema(mssqlDescribeTableSchema),
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
async function main() {
|
|
86
|
+
const cfg = loadConfig();
|
|
87
|
+
const pool = await sql.connect(mssqlDriverConfig(cfg));
|
|
88
|
+
const server = new Server({ name: 'mssql-mcp', version: '1.0.0' });
|
|
89
|
+
server.registerCapabilities({ tools: { listChanged: false } });
|
|
90
|
+
const tools = buildTools();
|
|
91
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools }));
|
|
92
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
93
|
+
const name = request.params.name;
|
|
94
|
+
const rawArgs = request.params.arguments ?? {};
|
|
95
|
+
try {
|
|
96
|
+
switch (name) {
|
|
97
|
+
case 'mssql_query': {
|
|
98
|
+
const args = v.parse(mssqlQuerySchema, rawArgs);
|
|
99
|
+
return await runQuery(pool, cfg, args.sql);
|
|
100
|
+
}
|
|
101
|
+
case 'mssql_list_tables': {
|
|
102
|
+
const args = v.parse(mssqlListTablesSchema, rawArgs);
|
|
103
|
+
return await runListTables(pool, args.schema);
|
|
104
|
+
}
|
|
105
|
+
case 'mssql_describe_table': {
|
|
106
|
+
const args = v.parse(mssqlDescribeTableSchema, rawArgs);
|
|
107
|
+
return await runDescribeTable(pool, args.schema, args.table);
|
|
108
|
+
}
|
|
109
|
+
default:
|
|
110
|
+
return toolError(`Unknown tool: ${name}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
if (v.isValiError(err)) {
|
|
115
|
+
return toolError(`Invalid arguments: ${err.message}`);
|
|
116
|
+
}
|
|
117
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
118
|
+
return toolError(msg);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
const transport = new StdioServerTransport();
|
|
122
|
+
await server.connect(transport);
|
|
123
|
+
}
|
|
124
|
+
main().catch((err) => {
|
|
125
|
+
console.error(err);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort read-only gate. Not a substitute for database permissions.
|
|
3
|
+
*/
|
|
4
|
+
export declare function stripSqlComments(text: string): string;
|
|
5
|
+
/** Masks single-quoted and N'...' string literals so keywords inside literals are ignored. */
|
|
6
|
+
export declare function maskSqlStringLiterals(text: string): string;
|
|
7
|
+
export declare function assertReadOnlySql(sql: string, allowWrites: boolean): void;
|
|
8
|
+
export declare function wrapWithRowCount(sql: string, maxRows: number | undefined): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort read-only gate. Not a substitute for database permissions.
|
|
3
|
+
*/
|
|
4
|
+
const FORBIDDEN = /\b(INSERT|UPDATE|DELETE|MERGE|DROP|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE|GRANT|REVOKE|DENY|BULK)\b/i;
|
|
5
|
+
export function stripSqlComments(text) {
|
|
6
|
+
let s = text.replace(/\/\*[\s\S]*?\*\//g, ' ');
|
|
7
|
+
s = s.replace(/--[^\n\r]*/g, ' ');
|
|
8
|
+
return s;
|
|
9
|
+
}
|
|
10
|
+
/** Masks single-quoted and N'...' string literals so keywords inside literals are ignored. */
|
|
11
|
+
export function maskSqlStringLiterals(text) {
|
|
12
|
+
let out = '';
|
|
13
|
+
let i = 0;
|
|
14
|
+
while (i < text.length) {
|
|
15
|
+
const ch = text[i];
|
|
16
|
+
if (ch === "'") {
|
|
17
|
+
let j = i + 1;
|
|
18
|
+
while (j < text.length) {
|
|
19
|
+
if (text[j] === "'" && text[j + 1] === "'") {
|
|
20
|
+
j += 2;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (text[j] === "'")
|
|
24
|
+
break;
|
|
25
|
+
j++;
|
|
26
|
+
}
|
|
27
|
+
out += ' ';
|
|
28
|
+
i = j < text.length ? j + 1 : text.length;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (ch === 'N' || ch === 'n') {
|
|
32
|
+
if (text[i + 1] === "'") {
|
|
33
|
+
let j = i + 2;
|
|
34
|
+
while (j < text.length) {
|
|
35
|
+
if (text[j] === "'" && text[j + 1] === "'") {
|
|
36
|
+
j += 2;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (text[j] === "'")
|
|
40
|
+
break;
|
|
41
|
+
j++;
|
|
42
|
+
}
|
|
43
|
+
out += ' ';
|
|
44
|
+
i = j < text.length ? j + 1 : text.length;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
out += ch;
|
|
49
|
+
i++;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
export function assertReadOnlySql(sql, allowWrites) {
|
|
54
|
+
if (allowWrites)
|
|
55
|
+
return;
|
|
56
|
+
const probe = maskSqlStringLiterals(stripSqlComments(sql));
|
|
57
|
+
if (FORBIDDEN.test(probe)) {
|
|
58
|
+
throw new Error('Read-only mode: this batch contains a blocked keyword (INSERT, UPDATE, DELETE, MERGE, DDL, EXEC, etc.). Set MSSQL_ALLOW_WRITES=true to allow writes (still use a least-privilege login).');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function wrapWithRowCount(sql, maxRows) {
|
|
62
|
+
if (maxRows === undefined || maxRows <= 0)
|
|
63
|
+
return sql;
|
|
64
|
+
const n = Math.floor(maxRows);
|
|
65
|
+
return `SET ROWCOUNT ${n}; ${sql}; SET ROWCOUNT 0;`;
|
|
66
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as v from 'valibot';
|
|
2
|
+
/** MCP tool inputSchema: JSON Schema derived from Valibot (descriptions flow to clients). */
|
|
3
|
+
export declare function valibotToJsonSchema(schema: v.GenericSchema): Record<string, unknown>;
|
|
4
|
+
export declare const mssqlQuerySchema: v.ObjectSchema<{
|
|
5
|
+
readonly sql: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.NonEmptyAction<string, undefined>, v.DescriptionAction<string, "T-SQL batch to run on the connected database. Credentials and safety flags come from server environment variables. Read-only mode blocks common write/DDL/exec patterns heuristically; use MSSQL_ALLOW_WRITES=true to lift that gate (still use a least-privilege SQL login).">]>;
|
|
6
|
+
}, undefined>;
|
|
7
|
+
export declare const mssqlListTablesSchema: v.ObjectSchema<{
|
|
8
|
+
readonly schema: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.DescriptionAction<string, "When set, restrict to this schema (e.g. dbo). When omitted, list base tables from all schemas.">]>, undefined>;
|
|
9
|
+
}, undefined>;
|
|
10
|
+
export declare const mssqlDescribeTableSchema: v.ObjectSchema<{
|
|
11
|
+
readonly schema: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.NonEmptyAction<string, undefined>, v.DescriptionAction<string, "Schema name, typically dbo.">]>;
|
|
12
|
+
readonly table: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.NonEmptyAction<string, undefined>, v.DescriptionAction<string, "Table name (unquoted identifier).">]>;
|
|
13
|
+
}, undefined>;
|
|
14
|
+
export type MssqlQueryInput = v.InferOutput<typeof mssqlQuerySchema>;
|
|
15
|
+
export type MssqlListTablesInput = v.InferOutput<typeof mssqlListTablesSchema>;
|
|
16
|
+
export type MssqlDescribeTableInput = v.InferOutput<typeof mssqlDescribeTableSchema>;
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as v from 'valibot';
|
|
2
|
+
import { toJsonSchema } from '@valibot/to-json-schema';
|
|
3
|
+
/** MCP tool inputSchema: JSON Schema derived from Valibot (descriptions flow to clients). */
|
|
4
|
+
export function valibotToJsonSchema(schema) {
|
|
5
|
+
const js = toJsonSchema(schema);
|
|
6
|
+
delete js.$schema;
|
|
7
|
+
return js;
|
|
8
|
+
}
|
|
9
|
+
export const mssqlQuerySchema = v.object({
|
|
10
|
+
sql: v.pipe(v.string(), v.nonEmpty(), v.description('T-SQL batch to run on the connected database. Credentials and safety flags come from server environment variables. Read-only mode blocks common write/DDL/exec patterns heuristically; use MSSQL_ALLOW_WRITES=true to lift that gate (still use a least-privilege SQL login).')),
|
|
11
|
+
});
|
|
12
|
+
export const mssqlListTablesSchema = v.object({
|
|
13
|
+
schema: v.optional(v.pipe(v.string(), v.description('When set, restrict to this schema (e.g. dbo). When omitted, list base tables from all schemas.'))),
|
|
14
|
+
});
|
|
15
|
+
export const mssqlDescribeTableSchema = v.object({
|
|
16
|
+
schema: v.pipe(v.string(), v.nonEmpty(), v.description('Schema name, typically dbo.')),
|
|
17
|
+
table: v.pipe(v.string(), v.nonEmpty(), v.description('Table name (unquoted identifier).')),
|
|
18
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adriancy/mcp-mssql",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mssql-mcp": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
".env.example"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@modelcontextprotocol/sdk": "^1.25.0",
|
|
17
|
+
"@valibot/to-json-schema": "^1.6.0",
|
|
18
|
+
"mssql": "^11.0.1",
|
|
19
|
+
"valibot": "^1.1.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/mssql": "^9.1.8",
|
|
23
|
+
"@types/node": "^22.10.0",
|
|
24
|
+
"tsx": "^4.19.2",
|
|
25
|
+
"typescript": "^5.7.2"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"start": "node dist/index.js",
|
|
30
|
+
"dev": "node --import tsx src/index.ts"
|
|
31
|
+
}
|
|
32
|
+
}
|