mssql-mcp 2.1.1 → 2.3.1
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/LICENSE +21 -0
- package/README.md +161 -89
- package/dist/src/config.d.ts +39 -0
- package/dist/src/config.js +37 -0
- package/dist/src/constants.d.ts +15 -0
- package/dist/src/constants.js +15 -0
- package/dist/src/db/connection.d.ts +8 -0
- package/dist/src/db/connection.js +80 -0
- package/dist/src/db/query-builders.d.ts +3 -0
- package/dist/src/db/query-builders.js +58 -0
- package/dist/src/db/validators.d.ts +5 -0
- package/dist/src/db/validators.js +25 -0
- package/dist/src/index.js +26 -0
- package/dist/src/resources/connection.d.ts +2 -0
- package/dist/src/resources/connection.js +19 -0
- package/dist/src/resources/metadata.d.ts +2 -0
- package/dist/src/resources/metadata.js +58 -0
- package/dist/src/schemas/outputs.d.ts +153 -0
- package/dist/src/schemas/outputs.js +54 -0
- package/dist/src/server.d.ts +2 -0
- package/dist/src/server.js +27 -0
- package/dist/src/tools/connect.d.ts +2 -0
- package/dist/src/tools/connect.js +45 -0
- package/dist/src/tools/databases.d.ts +2 -0
- package/dist/src/tools/databases.js +53 -0
- package/dist/src/tools/procedure.d.ts +2 -0
- package/dist/src/tools/procedure.js +106 -0
- package/dist/src/tools/query.d.ts +2 -0
- package/dist/src/tools/query.js +92 -0
- package/dist/src/tools/schema.d.ts +2 -0
- package/dist/src/tools/schema.js +96 -0
- package/dist/src/tools/status.d.ts +2 -0
- package/dist/src/tools/status.js +17 -0
- package/dist/src/tools/table.d.ts +2 -0
- package/dist/src/tools/table.js +261 -0
- package/dist/src/transports/http.d.ts +3 -0
- package/dist/src/transports/http.js +54 -0
- package/dist/src/transports/stdio.d.ts +2 -0
- package/dist/src/transports/stdio.js +23 -0
- package/dist/src/types.d.ts +37 -0
- package/dist/src/types.js +1 -0
- package/dist/src/utils/errors.d.ts +19 -0
- package/dist/src/utils/errors.js +29 -0
- package/dist/src/utils/format.d.ts +6 -0
- package/dist/src/utils/format.js +27 -0
- package/dist/src/utils/markdown.d.ts +3 -0
- package/dist/src/utils/markdown.js +33 -0
- package/dist/src/utils/pagination.d.ts +3 -0
- package/dist/src/utils/pagination.js +18 -0
- package/dist/tests/unit/markdown.test.d.ts +1 -0
- package/dist/tests/unit/markdown.test.js +70 -0
- package/dist/tests/unit/query-builders.test.d.ts +1 -0
- package/dist/tests/unit/query-builders.test.js +63 -0
- package/dist/tests/unit/tool-contracts.test.d.ts +1 -0
- package/dist/tests/unit/tool-contracts.test.js +62 -0
- package/dist/tests/unit/validators.test.d.ts +1 -0
- package/dist/tests/unit/validators.test.js +51 -0
- package/package.json +10 -6
- package/dist/index.js +0 -648
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import sql from "mssql";
|
|
3
|
+
import { requirePool } from "../db/connection.js";
|
|
4
|
+
import { buildSchemaObjectsQuery } from "../db/query-builders.js";
|
|
5
|
+
import { toActionableError, toolError, toolSuccess } from "../utils/errors.js";
|
|
6
|
+
import { formatJson } from "../utils/format.js";
|
|
7
|
+
import { formatMarkdownTable } from "../utils/markdown.js";
|
|
8
|
+
import { buildPaginationMeta, clampLimit } from "../utils/pagination.js";
|
|
9
|
+
import { PaginationSchema, SchemaObjectSchema } from "../schemas/outputs.js";
|
|
10
|
+
export function registerSchemaTools(server) {
|
|
11
|
+
server.registerTool("mssql_list_schema_objects", {
|
|
12
|
+
title: "List Schema Objects",
|
|
13
|
+
description: "Lists tables, views, stored procedures, or functions in the connected database. " +
|
|
14
|
+
"Read-only. Supports filtering by schema name and pagination. " +
|
|
15
|
+
"Example: objectType='tables', schemaName='dbo', limit=20",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
objectType: z
|
|
18
|
+
.enum(["tables", "views", "procedures", "functions", "all"])
|
|
19
|
+
.optional()
|
|
20
|
+
.default("tables")
|
|
21
|
+
.describe("Type of objects to list (default: tables)"),
|
|
22
|
+
schemaName: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Filter to a specific schema (e.g. 'dbo')"),
|
|
26
|
+
limit: z.number().int().min(1).max(200).optional().default(20).describe("Max objects (default 20)"),
|
|
27
|
+
offset: z.number().int().min(0).optional().default(0).describe("Skip N objects"),
|
|
28
|
+
response_format: z
|
|
29
|
+
.enum(["json", "markdown"])
|
|
30
|
+
.optional()
|
|
31
|
+
.default("json")
|
|
32
|
+
.describe("Output format: 'json' for structured data, 'markdown' for human-readable table"),
|
|
33
|
+
},
|
|
34
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
35
|
+
outputSchema: { objects: z.array(SchemaObjectSchema), pagination: PaginationSchema },
|
|
36
|
+
}, async ({ objectType, schemaName, limit: rawLimit, offset, response_format }) => {
|
|
37
|
+
try {
|
|
38
|
+
const pool = requirePool();
|
|
39
|
+
const limit = clampLimit(rawLimit);
|
|
40
|
+
const query = buildSchemaObjectsQuery(objectType, schemaName);
|
|
41
|
+
const request = pool.request();
|
|
42
|
+
if (schemaName) {
|
|
43
|
+
request.input("schemaName", sql.VarChar, schemaName);
|
|
44
|
+
}
|
|
45
|
+
const result = await request.query(query);
|
|
46
|
+
const allRows = result.recordset ?? [];
|
|
47
|
+
const page = allRows.slice(offset, offset + limit);
|
|
48
|
+
const pagination = buildPaginationMeta(page.length, limit, offset, allRows.length);
|
|
49
|
+
const structured = { objects: page, pagination };
|
|
50
|
+
if (response_format === "markdown") {
|
|
51
|
+
const rows = page;
|
|
52
|
+
let text = formatMarkdownTable(rows, `Schema Objects (${objectType})`);
|
|
53
|
+
text += `\n\n*Showing ${page.length} of ${allRows.length} · offset ${offset}*`;
|
|
54
|
+
return toolSuccess(text, structured);
|
|
55
|
+
}
|
|
56
|
+
return toolSuccess(formatJson(structured), structured);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
const msg = toActionableError(err);
|
|
60
|
+
console.error("mssql_list_schema_objects failed:", msg);
|
|
61
|
+
return toolError(`Schema query failed: ${msg}`);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// Backward-compatible alias
|
|
65
|
+
server.registerTool("mssql_get_schema", {
|
|
66
|
+
title: "Get Schema (deprecated — use mssql_list_schema_objects)",
|
|
67
|
+
description: "Deprecated alias for mssql_list_schema_objects. Use mssql_list_schema_objects instead.",
|
|
68
|
+
inputSchema: {
|
|
69
|
+
objectType: z
|
|
70
|
+
.enum(["tables", "views", "procedures", "functions", "all"])
|
|
71
|
+
.optional()
|
|
72
|
+
.default("tables"),
|
|
73
|
+
schemaName: z.string().optional(),
|
|
74
|
+
response_format: z.enum(["json", "markdown"]).optional().default("json"),
|
|
75
|
+
},
|
|
76
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
77
|
+
}, async ({ objectType, schemaName, response_format }) => {
|
|
78
|
+
try {
|
|
79
|
+
const pool = requirePool();
|
|
80
|
+
const query = buildSchemaObjectsQuery(objectType, schemaName);
|
|
81
|
+
const request = pool.request();
|
|
82
|
+
if (schemaName) {
|
|
83
|
+
request.input("schemaName", sql.VarChar, schemaName);
|
|
84
|
+
}
|
|
85
|
+
const result = await request.query(query);
|
|
86
|
+
const rows = result.recordset ?? [];
|
|
87
|
+
if (response_format === "markdown") {
|
|
88
|
+
return toolSuccess(formatMarkdownTable(rows));
|
|
89
|
+
}
|
|
90
|
+
return toolSuccess(formatJson(rows));
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
return toolError(`Schema query failed: ${toActionableError(err)}`);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getConnectionState } from "../db/connection.js";
|
|
2
|
+
import { toolSuccess } from "../utils/errors.js";
|
|
3
|
+
import { formatJson } from "../utils/format.js";
|
|
4
|
+
import { ConnectionStateSchema } from "../schemas/outputs.js";
|
|
5
|
+
export function registerStatusTool(server) {
|
|
6
|
+
server.registerTool("mssql_connection_status", {
|
|
7
|
+
title: "Connection Status",
|
|
8
|
+
description: "Returns the current connection status, server address, database name, and connection pool metrics. " +
|
|
9
|
+
"Read-only. Safe to call at any time.",
|
|
10
|
+
inputSchema: {},
|
|
11
|
+
outputSchema: ConnectionStateSchema,
|
|
12
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
13
|
+
}, async () => {
|
|
14
|
+
const state = getConnectionState();
|
|
15
|
+
return toolSuccess(formatJson(state), state);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import sql from "mssql";
|
|
3
|
+
import { requirePool } from "../db/connection.js";
|
|
4
|
+
import { validateIdentifier, validateOrderBy, SAFE_IDENTIFIER_RE } from "../db/validators.js";
|
|
5
|
+
import { buildSelectQuery, buildSelectWithWhereQuery } from "../db/query-builders.js";
|
|
6
|
+
import { toActionableError, toolError, toolSuccess } from "../utils/errors.js";
|
|
7
|
+
import { formatJson, truncatePayload } from "../utils/format.js";
|
|
8
|
+
import { formatMarkdownTable } from "../utils/markdown.js";
|
|
9
|
+
import { buildPaginationMeta, clampLimit } from "../utils/pagination.js";
|
|
10
|
+
import { PaginationSchema, ColumnInfoSchema } from "../schemas/outputs.js";
|
|
11
|
+
export function registerTableTools(server) {
|
|
12
|
+
server.registerTool("mssql_describe_table_columns", {
|
|
13
|
+
title: "Describe Table Columns",
|
|
14
|
+
description: "Returns column definitions for a table: name, data type, length, nullability, default, and ordinal position. " +
|
|
15
|
+
"Read-only. Uses parameterized queries to prevent injection.",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
tableName: z.string().min(1).describe("Table name"),
|
|
18
|
+
schemaName: z.string().optional().default("dbo").describe("Schema name (default: dbo)"),
|
|
19
|
+
response_format: z
|
|
20
|
+
.enum(["json", "markdown"])
|
|
21
|
+
.optional()
|
|
22
|
+
.default("json")
|
|
23
|
+
.describe("Output format: 'json' for structured data, 'markdown' for human-readable table"),
|
|
24
|
+
},
|
|
25
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
26
|
+
outputSchema: { columns: z.array(ColumnInfoSchema), table: z.string() },
|
|
27
|
+
}, async ({ tableName, schemaName, response_format }) => {
|
|
28
|
+
try {
|
|
29
|
+
const pool = requirePool();
|
|
30
|
+
validateIdentifier(tableName, "table name");
|
|
31
|
+
validateIdentifier(schemaName, "schema name");
|
|
32
|
+
const result = await pool
|
|
33
|
+
.request()
|
|
34
|
+
.input("tableName", sql.VarChar, tableName)
|
|
35
|
+
.input("schemaName", sql.VarChar, schemaName)
|
|
36
|
+
.query(`
|
|
37
|
+
SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH,
|
|
38
|
+
NUMERIC_PRECISION, NUMERIC_SCALE, IS_NULLABLE,
|
|
39
|
+
COLUMN_DEFAULT, ORDINAL_POSITION
|
|
40
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
41
|
+
WHERE TABLE_NAME = @tableName AND TABLE_SCHEMA = @schemaName
|
|
42
|
+
ORDER BY ORDINAL_POSITION
|
|
43
|
+
`);
|
|
44
|
+
const structured = {
|
|
45
|
+
columns: result.recordset,
|
|
46
|
+
table: `${schemaName}.${tableName}`,
|
|
47
|
+
};
|
|
48
|
+
if (response_format === "markdown") {
|
|
49
|
+
const rows = result.recordset;
|
|
50
|
+
return toolSuccess(formatMarkdownTable(rows, `Columns: ${schemaName}.${tableName}`), structured);
|
|
51
|
+
}
|
|
52
|
+
return toolSuccess(formatJson(structured), structured);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
const msg = toActionableError(err);
|
|
56
|
+
console.error("mssql_describe_table_columns failed:", msg);
|
|
57
|
+
return toolError(`Table description failed: ${msg}`);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// Backward-compatible alias
|
|
61
|
+
server.registerTool("mssql_describe_table", {
|
|
62
|
+
title: "Describe Table (deprecated — use mssql_describe_table_columns)",
|
|
63
|
+
description: "Deprecated alias for mssql_describe_table_columns.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
tableName: z.string().min(1),
|
|
66
|
+
schemaName: z.string().optional().default("dbo"),
|
|
67
|
+
response_format: z.enum(["json", "markdown"]).optional().default("json"),
|
|
68
|
+
},
|
|
69
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
70
|
+
}, async ({ tableName, schemaName, response_format }) => {
|
|
71
|
+
try {
|
|
72
|
+
const pool = requirePool();
|
|
73
|
+
validateIdentifier(tableName, "table name");
|
|
74
|
+
validateIdentifier(schemaName, "schema name");
|
|
75
|
+
const result = await pool
|
|
76
|
+
.request()
|
|
77
|
+
.input("tableName", sql.VarChar, tableName)
|
|
78
|
+
.input("schemaName", sql.VarChar, schemaName)
|
|
79
|
+
.query(`
|
|
80
|
+
SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH,
|
|
81
|
+
NUMERIC_PRECISION, NUMERIC_SCALE, IS_NULLABLE,
|
|
82
|
+
COLUMN_DEFAULT, ORDINAL_POSITION
|
|
83
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
84
|
+
WHERE TABLE_NAME = @tableName AND TABLE_SCHEMA = @schemaName
|
|
85
|
+
ORDER BY ORDINAL_POSITION
|
|
86
|
+
`);
|
|
87
|
+
const rows = result.recordset ?? [];
|
|
88
|
+
if (response_format === "markdown") {
|
|
89
|
+
return toolSuccess(formatMarkdownTable(rows));
|
|
90
|
+
}
|
|
91
|
+
return toolSuccess(formatJson(rows));
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
return toolError(`Table description failed: ${toActionableError(err)}`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
const safeIdentifierPattern = SAFE_IDENTIFIER_RE;
|
|
98
|
+
server.registerTool("mssql_read_table_rows", {
|
|
99
|
+
title: "Read Table Rows",
|
|
100
|
+
description: "Returns rows from a table with optional column projection, WHERE filtering, ORDER BY, and pagination. " +
|
|
101
|
+
"Read-only. Table and schema names are validated as safe identifiers. " +
|
|
102
|
+
"WHERE clause values MUST be passed via 'parameters' using @paramName placeholders. " +
|
|
103
|
+
"Pagination: limit (1-200, default 20) and offset. " +
|
|
104
|
+
"Example: tableName='Orders', schemaName='dbo', columns=['OrderId','Total'], limit=50",
|
|
105
|
+
inputSchema: {
|
|
106
|
+
tableName: z
|
|
107
|
+
.string()
|
|
108
|
+
.min(1)
|
|
109
|
+
.regex(safeIdentifierPattern, "Must be a valid SQL identifier")
|
|
110
|
+
.describe("Table name"),
|
|
111
|
+
schemaName: z
|
|
112
|
+
.string()
|
|
113
|
+
.regex(safeIdentifierPattern, "Must be a valid SQL identifier")
|
|
114
|
+
.optional()
|
|
115
|
+
.default("dbo")
|
|
116
|
+
.describe("Schema name (default: dbo)"),
|
|
117
|
+
columns: z
|
|
118
|
+
.array(z.string().regex(safeIdentifierPattern))
|
|
119
|
+
.optional()
|
|
120
|
+
.describe("Columns to return (default: all)"),
|
|
121
|
+
limit: z.number().int().min(1).max(200).optional().default(20).describe("Max rows (1-200)"),
|
|
122
|
+
offset: z.number().int().min(0).optional().default(0).describe("Rows to skip"),
|
|
123
|
+
whereClause: z
|
|
124
|
+
.string()
|
|
125
|
+
.optional()
|
|
126
|
+
.describe("WHERE predicate without WHERE keyword. Use @paramName for all values. " +
|
|
127
|
+
"Example: 'Status = @status AND Amount > @minAmount'"),
|
|
128
|
+
orderBy: z
|
|
129
|
+
.string()
|
|
130
|
+
.optional()
|
|
131
|
+
.describe("ORDER BY expression. Example: 'CreatedAt DESC, Id ASC'"),
|
|
132
|
+
parameters: z
|
|
133
|
+
.record(z.union([z.string(), z.number(), z.boolean(), z.null()]))
|
|
134
|
+
.optional()
|
|
135
|
+
.describe("Values for WHERE clause @paramName placeholders"),
|
|
136
|
+
response_format: z
|
|
137
|
+
.enum(["json", "markdown"])
|
|
138
|
+
.optional()
|
|
139
|
+
.default("json")
|
|
140
|
+
.describe("Output format: 'json' for structured data, 'markdown' for human-readable table"),
|
|
141
|
+
},
|
|
142
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
143
|
+
outputSchema: {
|
|
144
|
+
data: z.array(z.record(z.unknown())),
|
|
145
|
+
pagination: PaginationSchema,
|
|
146
|
+
table: z.string(),
|
|
147
|
+
execution_time_ms: z.number(),
|
|
148
|
+
truncated: z.boolean().optional(),
|
|
149
|
+
truncation_message: z.string().optional(),
|
|
150
|
+
},
|
|
151
|
+
}, async ({ tableName, schemaName, columns, limit: rawLimit, offset, whereClause, orderBy, parameters, response_format }) => {
|
|
152
|
+
try {
|
|
153
|
+
const pool = requirePool();
|
|
154
|
+
validateIdentifier(tableName, "table name");
|
|
155
|
+
validateIdentifier(schemaName, "schema name");
|
|
156
|
+
const limit = clampLimit(rawLimit);
|
|
157
|
+
const safeOrderBy = orderBy ? validateOrderBy(orderBy) : null;
|
|
158
|
+
const query = whereClause
|
|
159
|
+
? buildSelectWithWhereQuery(schemaName, tableName, columns ?? null, whereClause, safeOrderBy, offset, limit)
|
|
160
|
+
: buildSelectQuery(schemaName, tableName, columns ?? null, safeOrderBy, offset, limit);
|
|
161
|
+
const request = pool.request();
|
|
162
|
+
if (parameters) {
|
|
163
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
164
|
+
request.input(key, value);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const start = Date.now();
|
|
168
|
+
const result = await request.query(query);
|
|
169
|
+
const elapsed = Date.now() - start;
|
|
170
|
+
const { data, truncated, truncation_message } = truncatePayload(result.recordset ?? []);
|
|
171
|
+
const pagination = buildPaginationMeta(data.length, limit, offset);
|
|
172
|
+
const structured = {
|
|
173
|
+
data,
|
|
174
|
+
pagination,
|
|
175
|
+
table: `${schemaName}.${tableName}`,
|
|
176
|
+
execution_time_ms: elapsed,
|
|
177
|
+
};
|
|
178
|
+
if (truncated) {
|
|
179
|
+
structured.truncated = truncated;
|
|
180
|
+
structured.truncation_message = truncation_message;
|
|
181
|
+
}
|
|
182
|
+
if (response_format === "markdown") {
|
|
183
|
+
const rows = data;
|
|
184
|
+
let text = formatMarkdownTable(rows, `${schemaName}.${tableName}`);
|
|
185
|
+
if (truncated)
|
|
186
|
+
text += `\n\n> ⚠️ ${truncation_message}`;
|
|
187
|
+
text += `\n\n*${rows.length} rows · offset ${offset} · ${elapsed}ms*`;
|
|
188
|
+
return toolSuccess(text, structured);
|
|
189
|
+
}
|
|
190
|
+
return toolSuccess(formatJson(structured), structured);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
const msg = toActionableError(err);
|
|
194
|
+
console.error("mssql_read_table_rows failed:", msg);
|
|
195
|
+
return toolError(`Read table rows failed: ${msg}`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
// Backward-compatible alias
|
|
199
|
+
server.registerTool("mssql_get_table_data", {
|
|
200
|
+
title: "Get Table Data (deprecated — use mssql_read_table_rows)",
|
|
201
|
+
description: "Deprecated alias for mssql_read_table_rows. Use mssql_read_table_rows instead. " +
|
|
202
|
+
"⚠️ whereClause must use @paramName placeholders for all values.",
|
|
203
|
+
inputSchema: {
|
|
204
|
+
tableName: z
|
|
205
|
+
.string()
|
|
206
|
+
.min(1)
|
|
207
|
+
.regex(SAFE_IDENTIFIER_RE, "Must be a valid SQL identifier"),
|
|
208
|
+
schemaName: z
|
|
209
|
+
.string()
|
|
210
|
+
.regex(SAFE_IDENTIFIER_RE, "Must be a valid SQL identifier")
|
|
211
|
+
.optional()
|
|
212
|
+
.default("dbo"),
|
|
213
|
+
limit: z.number().int().min(1).max(200).optional().default(20),
|
|
214
|
+
offset: z.number().int().min(0).optional().default(0),
|
|
215
|
+
whereClause: z.string().optional(),
|
|
216
|
+
orderBy: z.string().optional(),
|
|
217
|
+
parameters: z
|
|
218
|
+
.record(z.union([z.string(), z.number(), z.boolean(), z.null()]))
|
|
219
|
+
.optional(),
|
|
220
|
+
response_format: z.enum(["json", "markdown"]).optional().default("json"),
|
|
221
|
+
},
|
|
222
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
223
|
+
}, async ({ tableName, schemaName, limit: rawLimit, offset, whereClause, orderBy, parameters, response_format }) => {
|
|
224
|
+
try {
|
|
225
|
+
const pool = requirePool();
|
|
226
|
+
validateIdentifier(tableName, "table name");
|
|
227
|
+
validateIdentifier(schemaName, "schema name");
|
|
228
|
+
const limit = clampLimit(rawLimit);
|
|
229
|
+
const safeOrderBy = orderBy ? validateOrderBy(orderBy) : null;
|
|
230
|
+
const query = whereClause
|
|
231
|
+
? buildSelectWithWhereQuery(schemaName, tableName, null, whereClause, safeOrderBy, offset, limit)
|
|
232
|
+
: buildSelectQuery(schemaName, tableName, null, safeOrderBy, offset, limit);
|
|
233
|
+
const request = pool.request();
|
|
234
|
+
if (parameters) {
|
|
235
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
236
|
+
request.input(key, value);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const start = Date.now();
|
|
240
|
+
const result = await request.query(query);
|
|
241
|
+
const elapsed = Date.now() - start;
|
|
242
|
+
const { data, truncated, truncation_message } = truncatePayload(result.recordset ?? []);
|
|
243
|
+
const pagination = buildPaginationMeta(data.length, limit, offset);
|
|
244
|
+
const structured = {
|
|
245
|
+
data,
|
|
246
|
+
metadata: { ...pagination, execution_time_ms: elapsed, table: `${schemaName}.${tableName}` },
|
|
247
|
+
};
|
|
248
|
+
if (truncated) {
|
|
249
|
+
structured.truncated = truncated;
|
|
250
|
+
structured.truncation_message = truncation_message;
|
|
251
|
+
}
|
|
252
|
+
if (response_format === "markdown") {
|
|
253
|
+
return toolSuccess(formatMarkdownTable(data));
|
|
254
|
+
}
|
|
255
|
+
return toolSuccess(formatJson(structured), structured);
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
return toolError(`Get table data failed: ${toActionableError(err)}`);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import { closePoolOnShutdown } from "../db/connection.js";
|
|
5
|
+
export async function runHttpTransport(server, config) {
|
|
6
|
+
const transport = new StreamableHTTPServerTransport({
|
|
7
|
+
sessionIdGenerator: () => randomUUID(),
|
|
8
|
+
});
|
|
9
|
+
const httpServer = createServer((req, res) => {
|
|
10
|
+
// Origin validation: only allow localhost by default
|
|
11
|
+
const origin = req.headers["origin"];
|
|
12
|
+
if (origin) {
|
|
13
|
+
const allowed = [
|
|
14
|
+
"http://localhost",
|
|
15
|
+
"http://127.0.0.1",
|
|
16
|
+
`http://${config.host}`,
|
|
17
|
+
`http://${config.host}:${config.port}`,
|
|
18
|
+
];
|
|
19
|
+
const isAllowed = allowed.some((o) => origin.startsWith(o));
|
|
20
|
+
if (!isAllowed) {
|
|
21
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
22
|
+
res.end(JSON.stringify({ error: "Forbidden: origin not allowed" }));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
transport.handleRequest(req, res);
|
|
27
|
+
});
|
|
28
|
+
const shutdown = async (signal) => {
|
|
29
|
+
console.error(`\nShutting down HTTP server (${signal})...`);
|
|
30
|
+
httpServer.close();
|
|
31
|
+
await transport.close();
|
|
32
|
+
await closePoolOnShutdown();
|
|
33
|
+
process.exit(0);
|
|
34
|
+
};
|
|
35
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
36
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
37
|
+
process.on("SIGUSR2", () => shutdown("SIGUSR2"));
|
|
38
|
+
process.on("uncaughtException", (err) => {
|
|
39
|
+
console.error("Uncaught exception:", err);
|
|
40
|
+
shutdown("uncaughtException");
|
|
41
|
+
});
|
|
42
|
+
process.on("unhandledRejection", (reason) => {
|
|
43
|
+
console.error("Unhandled rejection:", reason);
|
|
44
|
+
shutdown("unhandledRejection");
|
|
45
|
+
});
|
|
46
|
+
await server.connect(transport);
|
|
47
|
+
await new Promise((resolve, reject) => {
|
|
48
|
+
httpServer.listen(config.port, config.host, () => {
|
|
49
|
+
console.error(`MCP HTTP server listening on http://${config.host}:${config.port}`);
|
|
50
|
+
resolve();
|
|
51
|
+
});
|
|
52
|
+
httpServer.on("error", reject);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { closePoolOnShutdown } from "../db/connection.js";
|
|
3
|
+
export async function runStdioTransport(server) {
|
|
4
|
+
const transport = new StdioServerTransport();
|
|
5
|
+
const shutdown = async (signal) => {
|
|
6
|
+
console.error(`\nShutting down (${signal})...`);
|
|
7
|
+
await closePoolOnShutdown();
|
|
8
|
+
console.error("Server stopped.");
|
|
9
|
+
process.exit(0);
|
|
10
|
+
};
|
|
11
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
12
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
13
|
+
process.on("SIGUSR2", () => shutdown("SIGUSR2"));
|
|
14
|
+
process.on("uncaughtException", (err) => {
|
|
15
|
+
console.error("Uncaught exception:", err);
|
|
16
|
+
shutdown("uncaughtException");
|
|
17
|
+
});
|
|
18
|
+
process.on("unhandledRejection", (reason) => {
|
|
19
|
+
console.error("Unhandled rejection:", reason);
|
|
20
|
+
shutdown("unhandledRejection");
|
|
21
|
+
});
|
|
22
|
+
await server.connect(transport);
|
|
23
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface PaginationMeta {
|
|
2
|
+
count: number;
|
|
3
|
+
limit: number;
|
|
4
|
+
offset: number;
|
|
5
|
+
has_more: boolean;
|
|
6
|
+
next_offset: number | null;
|
|
7
|
+
total_count?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ToolResult<T = unknown> {
|
|
10
|
+
data: T;
|
|
11
|
+
meta?: Record<string, unknown>;
|
|
12
|
+
pagination?: PaginationMeta;
|
|
13
|
+
execution_time_ms?: number;
|
|
14
|
+
truncated?: boolean;
|
|
15
|
+
truncation_message?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface DatabaseConfig {
|
|
18
|
+
server: string;
|
|
19
|
+
database?: string;
|
|
20
|
+
user?: string;
|
|
21
|
+
password?: string;
|
|
22
|
+
port: number;
|
|
23
|
+
encrypt: boolean;
|
|
24
|
+
trustServerCertificate: boolean;
|
|
25
|
+
connectionTimeout: number;
|
|
26
|
+
requestTimeout: number;
|
|
27
|
+
}
|
|
28
|
+
export interface ConnectionState {
|
|
29
|
+
connected: boolean;
|
|
30
|
+
config: Pick<DatabaseConfig, "server" | "database" | "port"> | null;
|
|
31
|
+
pool_info: {
|
|
32
|
+
size: number;
|
|
33
|
+
available: number;
|
|
34
|
+
pending: number;
|
|
35
|
+
borrowed: number;
|
|
36
|
+
} | null;
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare class McpToolError extends Error {
|
|
2
|
+
readonly cause?: unknown | undefined;
|
|
3
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
4
|
+
}
|
|
5
|
+
export declare function toActionableError(error: unknown): string;
|
|
6
|
+
export declare function toolError(message: string): {
|
|
7
|
+
content: {
|
|
8
|
+
type: "text";
|
|
9
|
+
text: string;
|
|
10
|
+
}[];
|
|
11
|
+
isError: true;
|
|
12
|
+
};
|
|
13
|
+
export declare function toolSuccess(text: string, structuredContent?: Record<string, unknown>): {
|
|
14
|
+
content: Array<{
|
|
15
|
+
type: "text";
|
|
16
|
+
text: string;
|
|
17
|
+
}>;
|
|
18
|
+
structuredContent?: Record<string, unknown>;
|
|
19
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export class McpToolError extends Error {
|
|
2
|
+
cause;
|
|
3
|
+
constructor(message, cause) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.cause = cause;
|
|
6
|
+
this.name = "McpToolError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function toActionableError(error) {
|
|
10
|
+
if (error instanceof Error) {
|
|
11
|
+
return error.message;
|
|
12
|
+
}
|
|
13
|
+
return String(error);
|
|
14
|
+
}
|
|
15
|
+
export function toolError(message) {
|
|
16
|
+
return {
|
|
17
|
+
content: [{ type: "text", text: `❌ ${message}` }],
|
|
18
|
+
isError: true,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function toolSuccess(text, structuredContent) {
|
|
22
|
+
const result = {
|
|
23
|
+
content: [{ type: "text", text }],
|
|
24
|
+
};
|
|
25
|
+
if (structuredContent !== undefined) {
|
|
26
|
+
result.structuredContent = structuredContent;
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MAX_TEXT_PAYLOAD_BYTES } from "../constants.js";
|
|
2
|
+
export function formatJson(data) {
|
|
3
|
+
return JSON.stringify(data, null, 2);
|
|
4
|
+
}
|
|
5
|
+
export function truncatePayload(data, maxBytes = MAX_TEXT_PAYLOAD_BYTES) {
|
|
6
|
+
const full = JSON.stringify(data);
|
|
7
|
+
if (Buffer.byteLength(full, "utf8") <= maxBytes) {
|
|
8
|
+
return { data, truncated: false };
|
|
9
|
+
}
|
|
10
|
+
// Binary search for safe row count
|
|
11
|
+
let lo = 0;
|
|
12
|
+
let hi = data.length;
|
|
13
|
+
while (lo < hi) {
|
|
14
|
+
const mid = Math.floor((lo + hi + 1) / 2);
|
|
15
|
+
if (Buffer.byteLength(JSON.stringify(data.slice(0, mid)), "utf8") <= maxBytes) {
|
|
16
|
+
lo = mid;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
hi = mid - 1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
data: data.slice(0, lo),
|
|
24
|
+
truncated: true,
|
|
25
|
+
truncation_message: `Result truncated to ${lo} of ${data.length} rows to stay within ${maxBytes} byte limit.`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function formatMarkdownTable(rows: Record<string, unknown>[], title?: string): string;
|
|
2
|
+
export declare function formatMarkdownList(obj: Record<string, unknown>, title?: string): string;
|
|
3
|
+
export declare function formatMarkdownMultiRecordsets(recordsets: unknown[][], title?: string): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function escapeCell(value) {
|
|
2
|
+
return String(value ?? "")
|
|
3
|
+
.replace(/\r?\n|\r/g, " ")
|
|
4
|
+
.replace(/\|/g, "\\|");
|
|
5
|
+
}
|
|
6
|
+
export function formatMarkdownTable(rows, title) {
|
|
7
|
+
if (rows.length === 0) {
|
|
8
|
+
return title ? `**${title}**\n\nNo results found.` : "No results found.";
|
|
9
|
+
}
|
|
10
|
+
const headers = Object.keys(rows[0]);
|
|
11
|
+
const header = `| ${headers.join(" | ")} |`;
|
|
12
|
+
const sep = `| ${headers.map(() => "---").join(" | ")} |`;
|
|
13
|
+
const body = rows
|
|
14
|
+
.map((row) => `| ${headers.map((h) => escapeCell(row[h])).join(" | ")} |`)
|
|
15
|
+
.join("\n");
|
|
16
|
+
const table = [header, sep, body].join("\n");
|
|
17
|
+
return title ? `**${title}**\n\n${table}` : table;
|
|
18
|
+
}
|
|
19
|
+
export function formatMarkdownList(obj, title) {
|
|
20
|
+
const lines = Object.entries(obj).map(([k, v]) => `- **${k}**: ${typeof v === "object" ? JSON.stringify(v) : String(v ?? "")}`);
|
|
21
|
+
const list = lines.join("\n");
|
|
22
|
+
return title ? `**${title}**\n\n${list}` : list;
|
|
23
|
+
}
|
|
24
|
+
export function formatMarkdownMultiRecordsets(recordsets, title) {
|
|
25
|
+
if (!recordsets || recordsets.length === 0)
|
|
26
|
+
return "No result sets returned.";
|
|
27
|
+
const parts = recordsets.map((rs, i) => {
|
|
28
|
+
const rows = rs;
|
|
29
|
+
return formatMarkdownTable(rows, `Result Set ${i + 1}`);
|
|
30
|
+
});
|
|
31
|
+
const body = parts.join("\n\n");
|
|
32
|
+
return title ? `**${title}**\n\n${body}` : body;
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE } from "../constants.js";
|
|
2
|
+
export function clampLimit(limit) {
|
|
3
|
+
const n = limit ?? DEFAULT_PAGE_SIZE;
|
|
4
|
+
return Math.min(Math.max(1, n), MAX_PAGE_SIZE);
|
|
5
|
+
}
|
|
6
|
+
export function buildPaginationMeta(count, limit, offset, totalCount) {
|
|
7
|
+
// has_more is an approximation when totalCount is unknown:
|
|
8
|
+
// if count === limit, there *may* be more rows (could be a false positive on the last page).
|
|
9
|
+
const has_more = totalCount != null ? offset + count < totalCount : count === limit;
|
|
10
|
+
return {
|
|
11
|
+
count,
|
|
12
|
+
limit,
|
|
13
|
+
offset,
|
|
14
|
+
has_more,
|
|
15
|
+
next_offset: has_more ? offset + limit : null,
|
|
16
|
+
total_count: totalCount,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|