mssql-mcp 2.3.3 → 2.3.5

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # MS SQL Server MCP Server v2.3.2
1
+ # MS SQL Server MCP Server v2.3.5
2
2
 
3
3
  🚀 **Model Context Protocol (MCP) server** for Microsoft SQL Server - compatible with Claude Desktop, Cursor, Windsurf and VS Code.
4
4
 
@@ -1,5 +1,5 @@
1
1
  export declare const SERVER_NAME = "mssql-mcp-server";
2
- export declare const SERVER_VERSION = "2.3.3";
2
+ export declare const SERVER_VERSION = "2.3.5";
3
3
  export declare const DEFAULT_PORT = 1433;
4
4
  export declare const DEFAULT_ENCRYPT = true;
5
5
  export declare const DEFAULT_TRUST_SERVER_CERTIFICATE = false;
@@ -1,5 +1,5 @@
1
1
  export const SERVER_NAME = "mssql-mcp-server";
2
- export const SERVER_VERSION = "2.3.3";
2
+ export const SERVER_VERSION = "2.3.5";
3
3
  export const DEFAULT_PORT = 1433;
4
4
  export const DEFAULT_ENCRYPT = true;
5
5
  export const DEFAULT_TRUST_SERVER_CERTIFICATE = false;
@@ -2,12 +2,24 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
2
2
  import { closePoolOnShutdown } from "../db/connection.js";
3
3
  export async function runStdioTransport(server) {
4
4
  const transport = new StdioServerTransport();
5
- const shutdown = async (signal) => {
6
- console.error(`\nShutting down (${signal})...`);
5
+ let shuttingDown = false;
6
+ const shutdown = async (reason) => {
7
+ if (shuttingDown)
8
+ return;
9
+ shuttingDown = true;
10
+ console.error(`\nShutting down (${reason})...`);
7
11
  await closePoolOnShutdown();
8
12
  console.error("Server stopped.");
9
13
  process.exit(0);
10
14
  };
15
+ // Per the MCP stdio spec, the client signals shutdown by closing the child's
16
+ // stdin; the server is expected to exit when stdin reaches EOF. Windows does
17
+ // not reliably deliver OS signals to child processes when the parent exits,
18
+ // so we must also react to stdin EOF and transport close, otherwise orphaned
19
+ // node processes accumulate. See https://github.com/BYMCS/mssql-mcp/issues/2
20
+ transport.onclose = () => shutdown("stdio-close");
21
+ process.stdin.on("end", () => shutdown("stdin-end"));
22
+ process.stdin.on("close", () => shutdown("stdin-close"));
11
23
  process.on("SIGINT", () => shutdown("SIGINT"));
12
24
  process.on("SIGTERM", () => shutdown("SIGTERM"));
13
25
  process.on("SIGUSR2", () => shutdown("SIGUSR2"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mssql-mcp",
3
- "version": "2.3.3",
3
+ "version": "2.3.5",
4
4
  "description": "MCP Server for MS SQL Server integration with Claude Desktop, Cursor, Windsurf and VS Code",
5
5
  "main": "dist/src/index.js",
6
6
  "type": "module",
@@ -8,7 +8,7 @@
8
8
  "mssql-mcp-server": "dist/src/index.js"
9
9
  },
10
10
  "files": [
11
- "dist/**/*",
11
+ "dist/src/**/*",
12
12
  "README.md",
13
13
  "package.json"
14
14
  ],
@@ -1 +0,0 @@
1
- export {};
@@ -1,180 +0,0 @@
1
- import { strict as assert } from "node:assert";
2
- import { test } from "node:test";
3
- import { ConfigSchema, loadHttpConfig, loadConfigFromEnv } from "../../src/config.js";
4
- // --- ConfigSchema ---
5
- test("ConfigSchema - applies default port 1433", () => {
6
- const result = ConfigSchema.parse({ server: "localhost" });
7
- assert.equal(result.port, 1433);
8
- });
9
- test("ConfigSchema - applies default encrypt true", () => {
10
- const result = ConfigSchema.parse({ server: "localhost" });
11
- assert.equal(result.encrypt, true);
12
- });
13
- test("ConfigSchema - applies default trustServerCertificate false", () => {
14
- const result = ConfigSchema.parse({ server: "localhost" });
15
- assert.equal(result.trustServerCertificate, false);
16
- });
17
- test("ConfigSchema - applies default connectionTimeout 30000", () => {
18
- const result = ConfigSchema.parse({ server: "localhost" });
19
- assert.equal(result.connectionTimeout, 30000);
20
- });
21
- test("ConfigSchema - applies default requestTimeout 30000", () => {
22
- const result = ConfigSchema.parse({ server: "localhost" });
23
- assert.equal(result.requestTimeout, 30000);
24
- });
25
- test("ConfigSchema - accepts overridden port", () => {
26
- const result = ConfigSchema.parse({ server: "myserver", port: 1434 });
27
- assert.equal(result.port, 1434);
28
- });
29
- test("ConfigSchema - accepts encrypt false", () => {
30
- const result = ConfigSchema.parse({ server: "myserver", encrypt: false });
31
- assert.equal(result.encrypt, false);
32
- });
33
- test("ConfigSchema - accepts trustServerCertificate true", () => {
34
- const result = ConfigSchema.parse({ server: "myserver", trustServerCertificate: true });
35
- assert.equal(result.trustServerCertificate, true);
36
- });
37
- test("ConfigSchema - throws on empty server string", () => {
38
- assert.throws(() => ConfigSchema.parse({ server: "" }), /Server address is required/);
39
- });
40
- test("ConfigSchema - throws on missing server", () => {
41
- assert.throws(() => ConfigSchema.parse({}));
42
- });
43
- test("ConfigSchema - throws on port out of range", () => {
44
- assert.throws(() => ConfigSchema.parse({ server: "localhost", port: 0 }));
45
- assert.throws(() => ConfigSchema.parse({ server: "localhost", port: 65536 }));
46
- });
47
- // --- loadHttpConfig ---
48
- test("loadHttpConfig - returns default host 127.0.0.1 when env not set", () => {
49
- const savedHost = process.env.MCP_HOST;
50
- const savedPort = process.env.MCP_PORT;
51
- delete process.env.MCP_HOST;
52
- delete process.env.MCP_PORT;
53
- try {
54
- const config = loadHttpConfig();
55
- assert.equal(config.host, "127.0.0.1");
56
- }
57
- finally {
58
- if (savedHost !== undefined)
59
- process.env.MCP_HOST = savedHost;
60
- if (savedPort !== undefined)
61
- process.env.MCP_PORT = savedPort;
62
- }
63
- });
64
- test("loadHttpConfig - returns default port 3001 when env not set", () => {
65
- const savedHost = process.env.MCP_HOST;
66
- const savedPort = process.env.MCP_PORT;
67
- delete process.env.MCP_HOST;
68
- delete process.env.MCP_PORT;
69
- try {
70
- const config = loadHttpConfig();
71
- assert.equal(config.port, 3001);
72
- }
73
- finally {
74
- if (savedHost !== undefined)
75
- process.env.MCP_HOST = savedHost;
76
- if (savedPort !== undefined)
77
- process.env.MCP_PORT = savedPort;
78
- }
79
- });
80
- test("loadHttpConfig - reads host from MCP_HOST", () => {
81
- const savedHost = process.env.MCP_HOST;
82
- process.env.MCP_HOST = "0.0.0.0";
83
- try {
84
- const config = loadHttpConfig();
85
- assert.equal(config.host, "0.0.0.0");
86
- }
87
- finally {
88
- if (savedHost !== undefined)
89
- process.env.MCP_HOST = savedHost;
90
- else
91
- delete process.env.MCP_HOST;
92
- }
93
- });
94
- test("loadHttpConfig - reads port from MCP_PORT", () => {
95
- const savedPort = process.env.MCP_PORT;
96
- process.env.MCP_PORT = "8080";
97
- try {
98
- const config = loadHttpConfig();
99
- assert.equal(config.port, 8080);
100
- }
101
- finally {
102
- if (savedPort !== undefined)
103
- process.env.MCP_PORT = savedPort;
104
- else
105
- delete process.env.MCP_PORT;
106
- }
107
- });
108
- // --- loadConfigFromEnv ---
109
- test("loadConfigFromEnv - throws when DB_SERVER not set", () => {
110
- const saved = process.env.DB_SERVER;
111
- delete process.env.DB_SERVER;
112
- try {
113
- assert.throws(() => loadConfigFromEnv());
114
- }
115
- finally {
116
- if (saved !== undefined)
117
- process.env.DB_SERVER = saved;
118
- }
119
- });
120
- test("loadConfigFromEnv - reads DB_SERVER and applies defaults", () => {
121
- const savedServer = process.env.DB_SERVER;
122
- const savedEncrypt = process.env.DB_ENCRYPT;
123
- process.env.DB_SERVER = "myserver";
124
- delete process.env.DB_ENCRYPT;
125
- try {
126
- const config = loadConfigFromEnv();
127
- assert.equal(config.server, "myserver");
128
- assert.equal(config.port, 1433);
129
- assert.equal(config.encrypt, true);
130
- assert.equal(config.trustServerCertificate, false);
131
- }
132
- finally {
133
- if (savedServer !== undefined)
134
- process.env.DB_SERVER = savedServer;
135
- else
136
- delete process.env.DB_SERVER;
137
- if (savedEncrypt !== undefined)
138
- process.env.DB_ENCRYPT = savedEncrypt;
139
- }
140
- });
141
- test("loadConfigFromEnv - DB_ENCRYPT=false disables encryption", () => {
142
- const savedServer = process.env.DB_SERVER;
143
- const savedEncrypt = process.env.DB_ENCRYPT;
144
- process.env.DB_SERVER = "myserver";
145
- process.env.DB_ENCRYPT = "false";
146
- try {
147
- const config = loadConfigFromEnv();
148
- assert.equal(config.encrypt, false);
149
- }
150
- finally {
151
- if (savedServer !== undefined)
152
- process.env.DB_SERVER = savedServer;
153
- else
154
- delete process.env.DB_SERVER;
155
- if (savedEncrypt !== undefined)
156
- process.env.DB_ENCRYPT = savedEncrypt;
157
- else
158
- delete process.env.DB_ENCRYPT;
159
- }
160
- });
161
- test("loadConfigFromEnv - DB_TRUST_SERVER_CERTIFICATE=true enables trust", () => {
162
- const savedServer = process.env.DB_SERVER;
163
- const savedTrust = process.env.DB_TRUST_SERVER_CERTIFICATE;
164
- process.env.DB_SERVER = "myserver";
165
- process.env.DB_TRUST_SERVER_CERTIFICATE = "true";
166
- try {
167
- const config = loadConfigFromEnv();
168
- assert.equal(config.trustServerCertificate, true);
169
- }
170
- finally {
171
- if (savedServer !== undefined)
172
- process.env.DB_SERVER = savedServer;
173
- else
174
- delete process.env.DB_SERVER;
175
- if (savedTrust !== undefined)
176
- process.env.DB_TRUST_SERVER_CERTIFICATE = savedTrust;
177
- else
178
- delete process.env.DB_TRUST_SERVER_CERTIFICATE;
179
- }
180
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,52 +0,0 @@
1
- import { strict as assert } from "node:assert";
2
- import { test } from "node:test";
3
- import { McpToolError, toActionableError, toolError } from "../../src/utils/errors.js";
4
- test("toolError - wraps message with error emoji", () => {
5
- const result = toolError("connection failed");
6
- assert.equal(result.content[0].type, "text");
7
- assert.ok(result.content[0].text.includes("❌"));
8
- assert.ok(result.content[0].text.includes("connection failed"));
9
- });
10
- test("toolError - sets isError flag to true", () => {
11
- const result = toolError("oops");
12
- assert.equal(result.isError, true);
13
- });
14
- test("toolError - content type is always 'text'", () => {
15
- const result = toolError("msg");
16
- assert.equal(result.content.length, 1);
17
- assert.equal(result.content[0].type, "text");
18
- });
19
- test("toActionableError - extracts message from Error instance", () => {
20
- const err = new Error("connection refused");
21
- assert.equal(toActionableError(err), "connection refused");
22
- });
23
- test("toActionableError - converts plain string to string", () => {
24
- assert.equal(toActionableError("plain string"), "plain string");
25
- });
26
- test("toActionableError - converts number to string", () => {
27
- assert.equal(toActionableError(42), "42");
28
- });
29
- test("toActionableError - converts null to string", () => {
30
- assert.equal(toActionableError(null), "null");
31
- });
32
- test("McpToolError - has correct name", () => {
33
- const err = new McpToolError("test error");
34
- assert.equal(err.name, "McpToolError");
35
- });
36
- test("McpToolError - is instanceof Error", () => {
37
- const err = new McpToolError("test error");
38
- assert.ok(err instanceof Error);
39
- });
40
- test("McpToolError - stores message", () => {
41
- const err = new McpToolError("something failed");
42
- assert.equal(err.message, "something failed");
43
- });
44
- test("McpToolError - stores cause when provided", () => {
45
- const cause = new Error("original cause");
46
- const err = new McpToolError("wrapper", cause);
47
- assert.equal(err.cause, cause);
48
- });
49
- test("McpToolError - cause is undefined when not provided", () => {
50
- const err = new McpToolError("no cause");
51
- assert.equal(err.cause, undefined);
52
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,106 +0,0 @@
1
- import { strict as assert } from "node:assert";
2
- import { test } from "node:test";
3
- import { formatDisplayValue, normalizeOutput } from "../../src/utils/format.js";
4
- // Type stubs — getTypeName() uses function.name lowercased,
5
- // or falls back to object.name property.
6
- const DateType = { name: "Date" };
7
- const TimeType = { name: "Time" };
8
- function pad(value, width) {
9
- return String(value).padStart(width, "0");
10
- }
11
- function formatLocalDate(d) {
12
- return `${pad(d.getFullYear(), 4)}-${pad(d.getMonth() + 1, 2)}-${pad(d.getDate(), 2)}`;
13
- }
14
- function formatLocalTime(d) {
15
- return `${pad(d.getHours(), 2)}:${pad(d.getMinutes(), 2)}:${pad(d.getSeconds(), 2)}`;
16
- }
17
- function formatLocalDateTime(d) {
18
- return `${formatLocalDate(d)} ${formatLocalTime(d)}`;
19
- }
20
- // --- formatDisplayValue ---
21
- test("formatDisplayValue - null returns empty string", () => {
22
- assert.equal(formatDisplayValue(null), "");
23
- });
24
- test("formatDisplayValue - undefined returns empty string", () => {
25
- assert.equal(formatDisplayValue(undefined), "");
26
- });
27
- test("formatDisplayValue - string returns itself", () => {
28
- assert.equal(formatDisplayValue("hello"), "hello");
29
- });
30
- test("formatDisplayValue - number returns string representation", () => {
31
- assert.equal(formatDisplayValue(42), "42");
32
- assert.equal(formatDisplayValue(3.14), "3.14");
33
- });
34
- test("formatDisplayValue - boolean returns string representation", () => {
35
- assert.equal(formatDisplayValue(true), "true");
36
- assert.equal(formatDisplayValue(false), "false");
37
- });
38
- test("formatDisplayValue - object returns JSON string", () => {
39
- assert.equal(formatDisplayValue({ a: 1 }), JSON.stringify({ a: 1 }));
40
- });
41
- // --- normalizeOutput: SQL date types ---
42
- test("normalizeOutput - 'date' type returns date-only string", () => {
43
- const d = new Date(2026, 3, 1, 10, 51, 23, 600);
44
- const result = normalizeOutput(d, { type: DateType });
45
- assert.equal(result, formatLocalDate(d));
46
- });
47
- test("normalizeOutput - 'time' type with scale=0 returns time without fraction", () => {
48
- const d = new Date(2026, 3, 1, 10, 51, 23, 0);
49
- const result = normalizeOutput(d, { type: TimeType, scale: 0 });
50
- assert.equal(result, "10:51:23");
51
- });
52
- test("normalizeOutput - 'time' type with scale=3 includes milliseconds", () => {
53
- const d = new Date(2026, 3, 1, 10, 51, 23, 600);
54
- const result = normalizeOutput(d, { type: TimeType, scale: 3 });
55
- assert.equal(result, "10:51:23.600");
56
- });
57
- test("normalizeOutput - datetime with scale=0 returns no fraction", () => {
58
- const d = new Date(2026, 3, 1, 10, 51, 23, 600);
59
- const result = normalizeOutput(d, { scale: 0 });
60
- assert.equal(result, `${formatLocalDate(d)} ${formatLocalTime(d)}`);
61
- });
62
- test("normalizeOutput - datetime with scale=3 returns 3-digit fraction", () => {
63
- const d = new Date(2026, 3, 1, 10, 51, 23, 600);
64
- const result = normalizeOutput(d, { scale: 3 });
65
- assert.equal(result, `${formatLocalDateTime(d)}.600`);
66
- });
67
- test("normalizeOutput - datetime with no column metadata trims trailing zeros", () => {
68
- // 600ms → base "6000000" → trimmed to "6"
69
- const d = new Date(2026, 3, 1, 10, 51, 23, 600);
70
- const result = normalizeOutput(d);
71
- assert.ok(result.startsWith(formatLocalDate(d)));
72
- assert.ok(!result.endsWith("0"), `Trailing zeros not trimmed: ${result}`);
73
- });
74
- test("normalizeOutput - datetime with zero milliseconds and no scale returns no fraction", () => {
75
- // 0ms → base "0000000" → trimmed to "" → no fraction
76
- const d = new Date(2026, 3, 1, 10, 51, 23, 0);
77
- const result = normalizeOutput(d);
78
- assert.equal(result, `${formatLocalDateTime(d)}`);
79
- });
80
- // --- normalizeOutput: primitives & structures ---
81
- test("normalizeOutput - passes through number", () => {
82
- assert.equal(normalizeOutput(42), 42);
83
- });
84
- test("normalizeOutput - passes through string", () => {
85
- assert.equal(normalizeOutput("hello"), "hello");
86
- });
87
- test("normalizeOutput - passes through null", () => {
88
- assert.equal(normalizeOutput(null), null);
89
- });
90
- test("normalizeOutput - passes through boolean", () => {
91
- assert.equal(normalizeOutput(true), true);
92
- });
93
- test("normalizeOutput - normalizes Date inside plain object", () => {
94
- const d = new Date(2026, 3, 1, 10, 0, 0, 0);
95
- const result = normalizeOutput({ created: d });
96
- assert.equal(result.created, `${formatLocalDateTime(d)}`);
97
- });
98
- test("normalizeOutput - normalizes array of primitives unchanged", () => {
99
- const result = normalizeOutput([1, "two", null]);
100
- assert.deepEqual(result, [1, "two", null]);
101
- });
102
- test("normalizeOutput - normalizes Date values inside array", () => {
103
- const d = new Date(2026, 3, 1, 10, 0, 0, 0);
104
- const result = normalizeOutput([d]);
105
- assert.equal(result[0], `${formatLocalDateTime(d)}`);
106
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,90 +0,0 @@
1
- import { strict as assert } from "node:assert";
2
- import { test } from "node:test";
3
- import { formatMarkdownTable, formatMarkdownList, formatMarkdownMultiRecordsets, } from "../../src/utils/markdown.js";
4
- function DateTimeOffset() { }
5
- function pad(value, width) {
6
- return String(value).padStart(width, "0");
7
- }
8
- function formatUtcDateTime(date) {
9
- return [
10
- `${pad(date.getUTCFullYear(), 4)}-${pad(date.getUTCMonth() + 1, 2)}-${pad(date.getUTCDate(), 2)}`,
11
- `${pad(date.getUTCHours(), 2)}:${pad(date.getUTCMinutes(), 2)}:${pad(date.getUTCSeconds(), 2)}.${pad(date.getUTCMilliseconds(), 3)}6938`,
12
- ].join(" ");
13
- }
14
- test("formatMarkdownTable - renders header and rows", () => {
15
- const rows = [
16
- { name: "Users", schema: "dbo" },
17
- { name: "Orders", schema: "sales" },
18
- ];
19
- const result = formatMarkdownTable(rows);
20
- assert.ok(result.includes("| name | schema |"), "header row missing");
21
- assert.ok(result.includes("| --- |"), "separator row missing");
22
- assert.ok(result.includes("| Users | dbo |"), "data row 1 missing");
23
- assert.ok(result.includes("| Orders | sales |"), "data row 2 missing");
24
- });
25
- test("formatMarkdownTable - includes title when provided", () => {
26
- const rows = [{ id: 1 }];
27
- const result = formatMarkdownTable(rows, "My Table");
28
- assert.ok(result.startsWith("**My Table**"), "title missing");
29
- });
30
- test("formatMarkdownTable - empty rows returns no results message", () => {
31
- const result = formatMarkdownTable([]);
32
- assert.ok(result.includes("No results found."));
33
- });
34
- test("formatMarkdownTable - empty rows with title includes title", () => {
35
- const result = formatMarkdownTable([], "Empty");
36
- assert.ok(result.includes("**Empty**"));
37
- assert.ok(result.includes("No results found."));
38
- });
39
- test("formatMarkdownTable - null/undefined values render as empty string", () => {
40
- const rows = [{ id: 1, value: null, flag: undefined }];
41
- const result = formatMarkdownTable(rows);
42
- assert.ok(result.includes("| 1 |"));
43
- });
44
- test("formatMarkdownTable - pipe characters in cell values are escaped", () => {
45
- const rows = [{ name: "foo | bar", status: "ok" }];
46
- const result = formatMarkdownTable(rows);
47
- assert.ok(result.includes("foo \\| bar"), "pipe not escaped");
48
- assert.ok(!result.includes("foo | bar"), "raw pipe should not appear in data row");
49
- });
50
- test("formatMarkdownTable - newlines in cell values are replaced with space", () => {
51
- const rows = [{ notes: "line1\nline2", other: "x" }];
52
- const result = formatMarkdownTable(rows);
53
- assert.ok(result.includes("line1 line2"), "newline not replaced");
54
- assert.ok(!result.includes("line1\nline2"), "raw newline should not appear");
55
- });
56
- test("formatMarkdownTable - CRLF in cell values are replaced with space", () => {
57
- const rows = [{ notes: "line1\r\nline2" }];
58
- const result = formatMarkdownTable(rows);
59
- assert.ok(result.includes("line1 line2"));
60
- });
61
- test("formatMarkdownTable - datetimeoffset values render without JS timezone string", () => {
62
- const offsetTime = new Date(Date.UTC(2026, 3, 1, 10, 51, 23, 600));
63
- Object.defineProperty(offsetTime, "nanosecondsDelta", { value: 0.0006938 });
64
- const rows = Object.assign([{ offset_time: offsetTime }], {
65
- columns: { offset_time: { type: DateTimeOffset, scale: 7 } },
66
- });
67
- const result = formatMarkdownTable(rows);
68
- assert.ok(result.includes(formatUtcDateTime(offsetTime)));
69
- assert.ok(!result.includes("GMT"));
70
- });
71
- test("formatMarkdownList - renders key-value pairs", () => {
72
- const result = formatMarkdownList({ status: "connected", server: "localhost" });
73
- assert.ok(result.includes("**status**: connected"));
74
- assert.ok(result.includes("**server**: localhost"));
75
- });
76
- test("formatMarkdownList - includes title when provided", () => {
77
- const result = formatMarkdownList({ x: 1 }, "Info");
78
- assert.ok(result.startsWith("**Info**"));
79
- });
80
- test("formatMarkdownMultiRecordsets - renders multiple result sets", () => {
81
- const rs1 = [{ id: 1 }];
82
- const rs2 = [{ name: "foo" }];
83
- const result = formatMarkdownMultiRecordsets([rs1, rs2]);
84
- assert.ok(result.includes("Result Set 1"));
85
- assert.ok(result.includes("Result Set 2"));
86
- });
87
- test("formatMarkdownMultiRecordsets - empty recordsets returns message", () => {
88
- const result = formatMarkdownMultiRecordsets([]);
89
- assert.ok(result.includes("No result sets returned."));
90
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,87 +0,0 @@
1
- import { strict as assert } from "node:assert";
2
- import { test } from "node:test";
3
- import { buildSelectQuery, buildSelectWithWhereQuery, buildSchemaObjectsQuery, } from "../../src/db/query-builders.js";
4
- test("buildSelectQuery - basic SELECT *", () => {
5
- const q = buildSelectQuery("dbo", "Users", null, null, 0, 20);
6
- assert.ok(q.includes("[dbo].[Users]"));
7
- assert.ok(q.includes("OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY"));
8
- assert.ok(q.includes("SELECT *"));
9
- });
10
- test("buildSelectQuery - with columns projection", () => {
11
- const q = buildSelectQuery("dbo", "Users", ["Id", "Name"], null, 0, 10);
12
- assert.ok(q.includes("[Id], [Name]"));
13
- assert.ok(!q.includes("SELECT *"));
14
- });
15
- test("buildSelectQuery - with orderBy", () => {
16
- const q = buildSelectQuery("dbo", "Users", null, "Name ASC", 0, 20);
17
- assert.ok(q.includes("ORDER BY Name ASC"));
18
- });
19
- test("buildSelectQuery - default ORDER BY fallback", () => {
20
- const q = buildSelectQuery("dbo", "Users", null, null, 0, 20);
21
- assert.ok(q.includes("ORDER BY (SELECT NULL)"));
22
- });
23
- test("buildSelectQuery - offset and limit", () => {
24
- const q = buildSelectQuery("dbo", "Orders", null, null, 40, 10);
25
- assert.ok(q.includes("OFFSET 40 ROWS FETCH NEXT 10 ROWS ONLY"));
26
- });
27
- test("buildSelectWithWhereQuery - includes WHERE", () => {
28
- const q = buildSelectWithWhereQuery("dbo", "Orders", null, "Status = @status", null, 0, 20);
29
- assert.ok(q.includes("WHERE Status = @status"));
30
- assert.ok(q.includes("[dbo].[Orders]"));
31
- });
32
- test("buildSelectQuery - rejects invalid schema", () => {
33
- assert.throws(() => buildSelectQuery("bad schema", "Users", null, null, 0, 20), /Invalid identifier/);
34
- });
35
- test("buildSelectQuery - rejects invalid table", () => {
36
- assert.throws(() => buildSelectQuery("dbo", "bad table", null, null, 0, 20), /Invalid identifier/);
37
- });
38
- test("buildSchemaObjectsQuery - tables only", () => {
39
- const q = buildSchemaObjectsQuery("tables");
40
- assert.ok(q.includes("INFORMATION_SCHEMA.TABLES"));
41
- assert.ok(!q.includes("INFORMATION_SCHEMA.VIEWS"));
42
- assert.ok(!q.includes("ROUTINES"));
43
- });
44
- test("buildSchemaObjectsQuery - views only", () => {
45
- const q = buildSchemaObjectsQuery("views");
46
- assert.ok(q.includes("INFORMATION_SCHEMA.VIEWS"));
47
- assert.ok(!q.includes("INFORMATION_SCHEMA.TABLES"));
48
- });
49
- test("buildSchemaObjectsQuery - all includes all types", () => {
50
- const q = buildSchemaObjectsQuery("all");
51
- assert.ok(q.includes("INFORMATION_SCHEMA.TABLES"));
52
- assert.ok(q.includes("INFORMATION_SCHEMA.VIEWS"));
53
- assert.ok(q.includes("ROUTINE_TYPE = 'PROCEDURE'"));
54
- assert.ok(q.includes("ROUTINE_TYPE = 'FUNCTION'"));
55
- });
56
- test("buildSchemaObjectsQuery - with schema filter uses @schemaName", () => {
57
- const q = buildSchemaObjectsQuery("all", "dbo");
58
- assert.ok(q.includes("@schemaName"));
59
- });
60
- test("buildSchemaObjectsQuery - without schema filter has no @schemaName", () => {
61
- const q = buildSchemaObjectsQuery("tables");
62
- assert.ok(!q.includes("@schemaName"));
63
- });
64
- test("buildSchemaObjectsQuery - procedures only", () => {
65
- const q = buildSchemaObjectsQuery("procedures");
66
- assert.ok(q.includes("ROUTINE_TYPE = 'PROCEDURE'"));
67
- assert.ok(!q.includes("INFORMATION_SCHEMA.TABLES"));
68
- assert.ok(!q.includes("INFORMATION_SCHEMA.VIEWS"));
69
- assert.ok(!q.includes("ROUTINE_TYPE = 'FUNCTION'"));
70
- });
71
- test("buildSchemaObjectsQuery - functions only", () => {
72
- const q = buildSchemaObjectsQuery("functions");
73
- assert.ok(q.includes("ROUTINE_TYPE = 'FUNCTION'"));
74
- assert.ok(!q.includes("INFORMATION_SCHEMA.TABLES"));
75
- assert.ok(!q.includes("ROUTINE_TYPE = 'PROCEDURE'"));
76
- });
77
- test("buildSelectWithWhereQuery - with column projection", () => {
78
- const q = buildSelectWithWhereQuery("dbo", "Orders", ["Id", "Status"], "Status = @status", null, 0, 10);
79
- assert.ok(q.includes("[Id], [Status]"));
80
- assert.ok(q.includes("WHERE Status = @status"));
81
- assert.ok(!q.includes("SELECT *"));
82
- });
83
- test("buildSelectWithWhereQuery - with orderBy", () => {
84
- const q = buildSelectWithWhereQuery("dbo", "Orders", null, "Total > @min", "Total DESC", 0, 20);
85
- assert.ok(q.includes("ORDER BY Total DESC"));
86
- assert.ok(q.includes("WHERE Total > @min"));
87
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,95 +0,0 @@
1
- import { strict as assert } from "node:assert";
2
- import { test } from "node:test";
3
- import { toolSuccess, toolSuccessMarkdown } from "../../src/utils/errors.js";
4
- import { buildPaginationMeta, clampLimit } from "../../src/utils/pagination.js";
5
- import { formatJson, truncatePayload } from "../../src/utils/format.js";
6
- function DateTime2() { }
7
- function pad(value, width) {
8
- return String(value).padStart(width, "0");
9
- }
10
- function formatLocalDateTime(date) {
11
- return [
12
- `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1, 2)}-${pad(date.getDate(), 2)}`,
13
- `${pad(date.getHours(), 2)}:${pad(date.getMinutes(), 2)}:${pad(date.getSeconds(), 2)}.${pad(date.getMilliseconds(), 3)}6938`,
14
- ].join(" ");
15
- }
16
- test("clampLimit - defaults to DEFAULT_PAGE_SIZE (20)", () => {
17
- assert.equal(clampLimit(undefined), 20);
18
- });
19
- test("clampLimit - enforces max (200)", () => {
20
- assert.equal(clampLimit(9999), 200);
21
- assert.equal(clampLimit(201), 200);
22
- });
23
- test("clampLimit - enforces min (1)", () => {
24
- assert.equal(clampLimit(0), 1);
25
- assert.equal(clampLimit(-5), 1);
26
- });
27
- test("clampLimit - passes through valid value", () => {
28
- assert.equal(clampLimit(50), 50);
29
- assert.equal(clampLimit(1), 1);
30
- assert.equal(clampLimit(200), 200);
31
- });
32
- test("buildPaginationMeta - has_more true when count === limit", () => {
33
- const p = buildPaginationMeta(20, 20, 0);
34
- assert.equal(p.has_more, true);
35
- assert.equal(p.next_offset, 20);
36
- assert.equal(p.offset, 0);
37
- assert.equal(p.count, 20);
38
- assert.equal(p.limit, 20);
39
- });
40
- test("buildPaginationMeta - has_more false when count < limit", () => {
41
- const p = buildPaginationMeta(5, 20, 0);
42
- assert.equal(p.has_more, false);
43
- assert.equal(p.next_offset, null);
44
- });
45
- test("buildPaginationMeta - includes total_count when provided", () => {
46
- const p = buildPaginationMeta(20, 20, 0, 100);
47
- assert.equal(p.total_count, 100);
48
- });
49
- test("buildPaginationMeta - next_offset uses offset correctly", () => {
50
- const p = buildPaginationMeta(20, 20, 40);
51
- assert.equal(p.next_offset, 60);
52
- });
53
- test("truncatePayload - no truncation for small data", () => {
54
- const data = [{ id: 1 }, { id: 2 }];
55
- const result = truncatePayload(data, 100_000);
56
- assert.equal(result.truncated, false);
57
- assert.equal(result.data.length, 2);
58
- assert.equal(result.truncation_message, undefined);
59
- });
60
- test("truncatePayload - truncates when over byte limit", () => {
61
- const largeRow = { data: "x".repeat(1000) };
62
- const rows = Array.from({ length: 200 }, (_, i) => ({ ...largeRow, id: i }));
63
- const result = truncatePayload(rows, 10_000);
64
- assert.equal(result.truncated, true);
65
- assert.ok(result.data.length < 200, `Expected < 200 rows, got ${result.data.length}`);
66
- assert.ok(result.truncation_message?.includes("truncated"));
67
- assert.ok(result.truncation_message?.includes("200"));
68
- });
69
- test("truncatePayload - empty array not truncated", () => {
70
- const result = truncatePayload([], 100_000);
71
- assert.equal(result.truncated, false);
72
- assert.equal(result.data.length, 0);
73
- });
74
- test("formatJson - recordset datetime values use SQL-like formatting", () => {
75
- const serverTime = new Date(2026, 3, 1, 10, 51, 23, 600);
76
- Object.defineProperty(serverTime, "nanosecondsDelta", { value: 0.0006938 });
77
- const rows = Object.assign([{ server_time: serverTime }], {
78
- columns: { server_time: { type: DateTime2, scale: 7 } },
79
- });
80
- const result = formatJson({ recordset: rows });
81
- assert.ok(result.includes(formatLocalDateTime(serverTime)));
82
- assert.ok(!result.includes("GMT"));
83
- assert.ok(!result.includes("T10:51:23"));
84
- });
85
- test("toolSuccessMarkdown - returns text content without structuredContent", () => {
86
- const result = toolSuccessMarkdown("| col |\n| --- |\n| value |");
87
- assert.equal(result.content[0].type, "text");
88
- assert.ok(result.content[0].text.includes("| col |"));
89
- assert.equal(result.structuredContent, undefined);
90
- });
91
- test("toolSuccess - keeps structuredContent for json-style responses", () => {
92
- const result = toolSuccess("{\n \"ok\": true\n}", { ok: true });
93
- assert.equal(result.content[0].type, "text");
94
- assert.equal(result.structuredContent?.ok, true);
95
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,63 +0,0 @@
1
- import { strict as assert } from "node:assert";
2
- import { test } from "node:test";
3
- import { isValidIdentifier, validateIdentifier, bracketIdentifier, validateOrderBy, } from "../../src/db/validators.js";
4
- test("isValidIdentifier - valid names", () => {
5
- assert.ok(isValidIdentifier("dbo"));
6
- assert.ok(isValidIdentifier("MyTable"));
7
- assert.ok(isValidIdentifier("_private"));
8
- assert.ok(isValidIdentifier("table_123"));
9
- assert.ok(isValidIdentifier("a$b"));
10
- assert.ok(isValidIdentifier("col#1"));
11
- assert.ok(isValidIdentifier("_@col"));
12
- });
13
- test("isValidIdentifier - invalid names", () => {
14
- assert.ok(!isValidIdentifier(""));
15
- assert.ok(!isValidIdentifier("1table"));
16
- assert.ok(!isValidIdentifier("ta ble"));
17
- assert.ok(!isValidIdentifier("ta'ble"));
18
- assert.ok(!isValidIdentifier("ta;ble"));
19
- assert.ok(!isValidIdentifier("ta--ble"));
20
- assert.ok(!isValidIdentifier("ta/*ble"));
21
- assert.ok(!isValidIdentifier("ta\nble"));
22
- });
23
- test("validateIdentifier - throws on invalid", () => {
24
- assert.throws(() => validateIdentifier("bad name", "test"), /Invalid test/);
25
- assert.throws(() => validateIdentifier("1start", "test"), /Invalid test/);
26
- });
27
- test("validateIdentifier - returns valid name", () => {
28
- assert.equal(validateIdentifier("dbo", "schema"), "dbo");
29
- });
30
- test("bracketIdentifier - wraps valid name", () => {
31
- assert.equal(bracketIdentifier("dbo"), "[dbo]");
32
- assert.equal(bracketIdentifier("MyTable"), "[MyTable]");
33
- assert.equal(bracketIdentifier("_col"), "[_col]");
34
- });
35
- test("bracketIdentifier - throws on invalid", () => {
36
- assert.throws(() => bracketIdentifier("bad name"), /Invalid identifier/);
37
- assert.throws(() => bracketIdentifier("1bad"), /Invalid identifier/);
38
- });
39
- test("validateOrderBy - valid clauses", () => {
40
- assert.equal(validateOrderBy("Name ASC"), "Name ASC");
41
- assert.equal(validateOrderBy("Id DESC, Name ASC"), "Id DESC, Name ASC");
42
- assert.equal(validateOrderBy("CreatedAt"), "CreatedAt");
43
- assert.equal(validateOrderBy("[Id] DESC"), "[Id] DESC");
44
- assert.equal(validateOrderBy("t.Name"), "t.Name");
45
- });
46
- test("validateOrderBy - invalid clauses", () => {
47
- assert.throws(() => validateOrderBy("1+1"), /Invalid ORDER BY/);
48
- assert.throws(() => validateOrderBy("Name; DROP TABLE"), /Invalid ORDER BY/);
49
- assert.throws(() => validateOrderBy("Name UNION SELECT"), /Invalid ORDER BY/);
50
- assert.throws(() => validateOrderBy("Name--comment"), /Invalid ORDER BY/);
51
- });
52
- test("isValidIdentifier - 128-char identifier is valid (at limit)", () => {
53
- const name = "a" + "x".repeat(127); // 1 + 127 = 128 chars
54
- assert.ok(isValidIdentifier(name));
55
- });
56
- test("isValidIdentifier - 129-char identifier is invalid (over limit)", () => {
57
- const name = "a" + "x".repeat(128); // 1 + 128 = 129 chars
58
- assert.ok(!isValidIdentifier(name));
59
- });
60
- test("isValidIdentifier - single char identifier is valid", () => {
61
- assert.ok(isValidIdentifier("a"));
62
- assert.ok(isValidIdentifier("_"));
63
- });