mssql-mcp 2.1.1 → 2.3.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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +161 -89
  3. package/dist/src/config.d.ts +39 -0
  4. package/dist/src/config.js +37 -0
  5. package/dist/src/constants.d.ts +15 -0
  6. package/dist/src/constants.js +15 -0
  7. package/dist/src/db/connection.d.ts +8 -0
  8. package/dist/src/db/connection.js +80 -0
  9. package/dist/src/db/query-builders.d.ts +3 -0
  10. package/dist/src/db/query-builders.js +58 -0
  11. package/dist/src/db/validators.d.ts +5 -0
  12. package/dist/src/db/validators.js +25 -0
  13. package/dist/src/index.js +26 -0
  14. package/dist/src/resources/connection.d.ts +2 -0
  15. package/dist/src/resources/connection.js +19 -0
  16. package/dist/src/resources/metadata.d.ts +2 -0
  17. package/dist/src/resources/metadata.js +58 -0
  18. package/dist/src/schemas/outputs.d.ts +153 -0
  19. package/dist/src/schemas/outputs.js +54 -0
  20. package/dist/src/server.d.ts +2 -0
  21. package/dist/src/server.js +27 -0
  22. package/dist/src/tools/connect.d.ts +2 -0
  23. package/dist/src/tools/connect.js +45 -0
  24. package/dist/src/tools/databases.d.ts +2 -0
  25. package/dist/src/tools/databases.js +53 -0
  26. package/dist/src/tools/procedure.d.ts +2 -0
  27. package/dist/src/tools/procedure.js +106 -0
  28. package/dist/src/tools/query.d.ts +2 -0
  29. package/dist/src/tools/query.js +92 -0
  30. package/dist/src/tools/schema.d.ts +2 -0
  31. package/dist/src/tools/schema.js +96 -0
  32. package/dist/src/tools/status.d.ts +2 -0
  33. package/dist/src/tools/status.js +17 -0
  34. package/dist/src/tools/table.d.ts +2 -0
  35. package/dist/src/tools/table.js +261 -0
  36. package/dist/src/transports/http.d.ts +3 -0
  37. package/dist/src/transports/http.js +54 -0
  38. package/dist/src/transports/stdio.d.ts +2 -0
  39. package/dist/src/transports/stdio.js +23 -0
  40. package/dist/src/types.d.ts +37 -0
  41. package/dist/src/types.js +1 -0
  42. package/dist/src/utils/errors.d.ts +19 -0
  43. package/dist/src/utils/errors.js +29 -0
  44. package/dist/src/utils/format.d.ts +6 -0
  45. package/dist/src/utils/format.js +27 -0
  46. package/dist/src/utils/markdown.d.ts +3 -0
  47. package/dist/src/utils/markdown.js +33 -0
  48. package/dist/src/utils/pagination.d.ts +3 -0
  49. package/dist/src/utils/pagination.js +18 -0
  50. package/dist/tests/unit/markdown.test.d.ts +1 -0
  51. package/dist/tests/unit/markdown.test.js +70 -0
  52. package/dist/tests/unit/query-builders.test.d.ts +1 -0
  53. package/dist/tests/unit/query-builders.test.js +63 -0
  54. package/dist/tests/unit/tool-contracts.test.d.ts +1 -0
  55. package/dist/tests/unit/tool-contracts.test.js +62 -0
  56. package/dist/tests/unit/validators.test.d.ts +1 -0
  57. package/dist/tests/unit/validators.test.js +51 -0
  58. package/package.json +10 -6
  59. package/dist/index.js +0 -648
  60. /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,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerStatusTool(server: McpServer): void;
@@ -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,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerTableTools(server: McpServer): void;
@@ -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,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { HttpConfig } from "../config.js";
3
+ export declare function runHttpTransport(server: McpServer, config: HttpConfig): Promise<void>;
@@ -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,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function runStdioTransport(server: McpServer): Promise<void>;
@@ -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,6 @@
1
+ export declare function formatJson(data: unknown): string;
2
+ export declare function truncatePayload(data: unknown[], maxBytes?: number): {
3
+ data: unknown[];
4
+ truncated: boolean;
5
+ truncation_message?: string;
6
+ };
@@ -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,3 @@
1
+ import type { PaginationMeta } from "../types.js";
2
+ export declare function clampLimit(limit: number | undefined): number;
3
+ export declare function buildPaginationMeta(count: number, limit: number, offset: number, totalCount?: number): PaginationMeta;
@@ -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 {};