mssql-mcp 2.3.1 → 2.3.2
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 +1 -1
- package/dist/src/constants.d.ts +1 -1
- package/dist/src/constants.js +1 -1
- package/dist/src/db/connection.js +1 -0
- package/dist/src/tools/databases.js +2 -4
- package/dist/src/tools/procedure.js +3 -10
- package/dist/src/tools/query.js +2 -12
- package/dist/src/tools/schema.js +3 -5
- package/dist/src/tools/table.js +5 -15
- package/dist/src/utils/errors.d.ts +6 -0
- package/dist/src/utils/errors.js +7 -1
- package/dist/src/utils/format.d.ts +8 -0
- package/dist/src/utils/format.js +116 -7
- package/dist/src/utils/markdown.js +6 -4
- package/dist/tests/unit/markdown.test.js +20 -0
- package/dist/tests/unit/tool-contracts.test.js +34 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/src/constants.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const SERVER_NAME = "mssql-mcp-server";
|
|
2
|
-
export declare const SERVER_VERSION = "2.3.
|
|
2
|
+
export declare const SERVER_VERSION = "2.3.2";
|
|
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;
|
package/dist/src/constants.js
CHANGED
|
@@ -19,6 +19,7 @@ export async function connectPool(config) {
|
|
|
19
19
|
encrypt: config.encrypt,
|
|
20
20
|
trustServerCertificate: config.trustServerCertificate,
|
|
21
21
|
enableArithAbort: true,
|
|
22
|
+
useUTC: false,
|
|
22
23
|
},
|
|
23
24
|
connectionTimeout: config.connectionTimeout,
|
|
24
25
|
requestTimeout: config.requestTimeout,
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { requirePool } from "../db/connection.js";
|
|
3
|
-
import { toActionableError, toolError, toolSuccess } from "../utils/errors.js";
|
|
3
|
+
import { toActionableError, toolError, toolSuccess, toolSuccessMarkdown } from "../utils/errors.js";
|
|
4
4
|
import { formatJson } from "../utils/format.js";
|
|
5
5
|
import { formatMarkdownTable } from "../utils/markdown.js";
|
|
6
6
|
import { buildPaginationMeta, clampLimit } from "../utils/pagination.js";
|
|
7
|
-
import { PaginationSchema, DatabaseInfoSchema } from "../schemas/outputs.js";
|
|
8
7
|
export function registerDatabasesTools(server) {
|
|
9
8
|
server.registerTool("mssql_list_databases", {
|
|
10
9
|
title: "List Databases",
|
|
@@ -20,7 +19,6 @@ export function registerDatabasesTools(server) {
|
|
|
20
19
|
.describe("Output format: 'json' for structured data, 'markdown' for human-readable table"),
|
|
21
20
|
},
|
|
22
21
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
23
|
-
outputSchema: { databases: z.array(DatabaseInfoSchema), pagination: PaginationSchema },
|
|
24
22
|
}, async ({ limit: rawLimit, offset, response_format }) => {
|
|
25
23
|
try {
|
|
26
24
|
const pool = requirePool();
|
|
@@ -40,7 +38,7 @@ export function registerDatabasesTools(server) {
|
|
|
40
38
|
const rows = page;
|
|
41
39
|
let text = formatMarkdownTable(rows, "Databases");
|
|
42
40
|
text += `\n\n*Showing ${page.length} of ${allRows.length} databases*`;
|
|
43
|
-
return
|
|
41
|
+
return toolSuccessMarkdown(text);
|
|
44
42
|
}
|
|
45
43
|
return toolSuccess(formatJson(structured), structured);
|
|
46
44
|
}
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { requirePool } from "../db/connection.js";
|
|
3
3
|
import { validateIdentifier, bracketIdentifier } from "../db/validators.js";
|
|
4
|
-
import { toActionableError, toolError, toolSuccess } from "../utils/errors.js";
|
|
4
|
+
import { toActionableError, toolError, toolSuccess, toolSuccessMarkdown } from "../utils/errors.js";
|
|
5
5
|
import { formatJson } from "../utils/format.js";
|
|
6
6
|
import { formatMarkdownMultiRecordsets } from "../utils/markdown.js";
|
|
7
|
-
const ProcedureOutputSchema = {
|
|
8
|
-
recordsets: z.array(z.array(z.record(z.unknown()))),
|
|
9
|
-
rows_affected: z.array(z.number()),
|
|
10
|
-
output: z.record(z.unknown()),
|
|
11
|
-
return_value: z.unknown(),
|
|
12
|
-
};
|
|
13
7
|
export function registerProcedureTools(server) {
|
|
14
8
|
server.registerTool("mssql_execute_stored_procedure", {
|
|
15
9
|
title: "Execute Stored Procedure",
|
|
@@ -31,7 +25,6 @@ export function registerProcedureTools(server) {
|
|
|
31
25
|
.describe("Output format: 'json' for structured data, 'markdown' for human-readable tables"),
|
|
32
26
|
},
|
|
33
27
|
annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: true },
|
|
34
|
-
outputSchema: ProcedureOutputSchema,
|
|
35
28
|
}, async ({ procedureName, schemaName, parameters, response_format }) => {
|
|
36
29
|
try {
|
|
37
30
|
const pool = requirePool();
|
|
@@ -52,7 +45,7 @@ export function registerProcedureTools(server) {
|
|
|
52
45
|
return_value: result.returnValue,
|
|
53
46
|
};
|
|
54
47
|
if (response_format === "markdown") {
|
|
55
|
-
return
|
|
48
|
+
return toolSuccessMarkdown(formatMarkdownMultiRecordsets(result.recordsets, qualifiedName));
|
|
56
49
|
}
|
|
57
50
|
return toolSuccess(formatJson(structured), structured);
|
|
58
51
|
}
|
|
@@ -95,7 +88,7 @@ export function registerProcedureTools(server) {
|
|
|
95
88
|
returnValue: result.returnValue,
|
|
96
89
|
};
|
|
97
90
|
if (response_format === "markdown") {
|
|
98
|
-
return
|
|
91
|
+
return toolSuccessMarkdown(formatMarkdownMultiRecordsets(result.recordsets));
|
|
99
92
|
}
|
|
100
93
|
return toolSuccess(formatJson(structured), structured);
|
|
101
94
|
}
|
package/dist/src/tools/query.js
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { requirePool } from "../db/connection.js";
|
|
3
|
-
import { toActionableError, toolError, toolSuccess } from "../utils/errors.js";
|
|
3
|
+
import { toActionableError, toolError, toolSuccess, toolSuccessMarkdown } from "../utils/errors.js";
|
|
4
4
|
import { formatJson, truncatePayload } from "../utils/format.js";
|
|
5
5
|
import { formatMarkdownTable } from "../utils/markdown.js";
|
|
6
|
-
const QueryOutputSchema = {
|
|
7
|
-
recordset: z.array(z.record(z.unknown())),
|
|
8
|
-
rows_affected: z.array(z.number()),
|
|
9
|
-
output: z.record(z.unknown()),
|
|
10
|
-
execution_time_ms: z.number(),
|
|
11
|
-
parameters_used: z.number(),
|
|
12
|
-
truncated: z.boolean(),
|
|
13
|
-
truncation_message: z.string().optional(),
|
|
14
|
-
};
|
|
15
6
|
async function runSqlQueryCore(query, parameters, response_format, logLabel) {
|
|
16
7
|
try {
|
|
17
8
|
const pool = requirePool();
|
|
@@ -41,7 +32,7 @@ async function runSqlQueryCore(query, parameters, response_format, logLabel) {
|
|
|
41
32
|
if (truncated)
|
|
42
33
|
text += `\n\n> ⚠️ ${truncation_message}`;
|
|
43
34
|
text += `\n\n*Rows affected: ${(result.rowsAffected ?? []).join(", ")} · ${elapsed}ms*`;
|
|
44
|
-
return
|
|
35
|
+
return toolSuccessMarkdown(text);
|
|
45
36
|
}
|
|
46
37
|
return toolSuccess(formatJson(structured), structured);
|
|
47
38
|
}
|
|
@@ -72,7 +63,6 @@ export function registerQueryTools(server) {
|
|
|
72
63
|
.describe("Output format: 'json' for structured data, 'markdown' for human-readable table"),
|
|
73
64
|
},
|
|
74
65
|
annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: true },
|
|
75
|
-
outputSchema: QueryOutputSchema,
|
|
76
66
|
}, ({ query, parameters, response_format }) => runSqlQueryCore(query, parameters, response_format, "mssql_run_sql_query"));
|
|
77
67
|
// Backward-compatible alias
|
|
78
68
|
server.registerTool("mssql_execute_query", {
|
package/dist/src/tools/schema.js
CHANGED
|
@@ -2,11 +2,10 @@ import { z } from "zod";
|
|
|
2
2
|
import sql from "mssql";
|
|
3
3
|
import { requirePool } from "../db/connection.js";
|
|
4
4
|
import { buildSchemaObjectsQuery } from "../db/query-builders.js";
|
|
5
|
-
import { toActionableError, toolError, toolSuccess } from "../utils/errors.js";
|
|
5
|
+
import { toActionableError, toolError, toolSuccess, toolSuccessMarkdown } from "../utils/errors.js";
|
|
6
6
|
import { formatJson } from "../utils/format.js";
|
|
7
7
|
import { formatMarkdownTable } from "../utils/markdown.js";
|
|
8
8
|
import { buildPaginationMeta, clampLimit } from "../utils/pagination.js";
|
|
9
|
-
import { PaginationSchema, SchemaObjectSchema } from "../schemas/outputs.js";
|
|
10
9
|
export function registerSchemaTools(server) {
|
|
11
10
|
server.registerTool("mssql_list_schema_objects", {
|
|
12
11
|
title: "List Schema Objects",
|
|
@@ -32,7 +31,6 @@ export function registerSchemaTools(server) {
|
|
|
32
31
|
.describe("Output format: 'json' for structured data, 'markdown' for human-readable table"),
|
|
33
32
|
},
|
|
34
33
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
35
|
-
outputSchema: { objects: z.array(SchemaObjectSchema), pagination: PaginationSchema },
|
|
36
34
|
}, async ({ objectType, schemaName, limit: rawLimit, offset, response_format }) => {
|
|
37
35
|
try {
|
|
38
36
|
const pool = requirePool();
|
|
@@ -51,7 +49,7 @@ export function registerSchemaTools(server) {
|
|
|
51
49
|
const rows = page;
|
|
52
50
|
let text = formatMarkdownTable(rows, `Schema Objects (${objectType})`);
|
|
53
51
|
text += `\n\n*Showing ${page.length} of ${allRows.length} · offset ${offset}*`;
|
|
54
|
-
return
|
|
52
|
+
return toolSuccessMarkdown(text);
|
|
55
53
|
}
|
|
56
54
|
return toolSuccess(formatJson(structured), structured);
|
|
57
55
|
}
|
|
@@ -85,7 +83,7 @@ export function registerSchemaTools(server) {
|
|
|
85
83
|
const result = await request.query(query);
|
|
86
84
|
const rows = result.recordset ?? [];
|
|
87
85
|
if (response_format === "markdown") {
|
|
88
|
-
return
|
|
86
|
+
return toolSuccessMarkdown(formatMarkdownTable(rows));
|
|
89
87
|
}
|
|
90
88
|
return toolSuccess(formatJson(rows));
|
|
91
89
|
}
|
package/dist/src/tools/table.js
CHANGED
|
@@ -3,11 +3,10 @@ import sql from "mssql";
|
|
|
3
3
|
import { requirePool } from "../db/connection.js";
|
|
4
4
|
import { validateIdentifier, validateOrderBy, SAFE_IDENTIFIER_RE } from "../db/validators.js";
|
|
5
5
|
import { buildSelectQuery, buildSelectWithWhereQuery } from "../db/query-builders.js";
|
|
6
|
-
import { toActionableError, toolError, toolSuccess } from "../utils/errors.js";
|
|
6
|
+
import { toActionableError, toolError, toolSuccess, toolSuccessMarkdown } from "../utils/errors.js";
|
|
7
7
|
import { formatJson, truncatePayload } from "../utils/format.js";
|
|
8
8
|
import { formatMarkdownTable } from "../utils/markdown.js";
|
|
9
9
|
import { buildPaginationMeta, clampLimit } from "../utils/pagination.js";
|
|
10
|
-
import { PaginationSchema, ColumnInfoSchema } from "../schemas/outputs.js";
|
|
11
10
|
export function registerTableTools(server) {
|
|
12
11
|
server.registerTool("mssql_describe_table_columns", {
|
|
13
12
|
title: "Describe Table Columns",
|
|
@@ -23,7 +22,6 @@ export function registerTableTools(server) {
|
|
|
23
22
|
.describe("Output format: 'json' for structured data, 'markdown' for human-readable table"),
|
|
24
23
|
},
|
|
25
24
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
26
|
-
outputSchema: { columns: z.array(ColumnInfoSchema), table: z.string() },
|
|
27
25
|
}, async ({ tableName, schemaName, response_format }) => {
|
|
28
26
|
try {
|
|
29
27
|
const pool = requirePool();
|
|
@@ -47,7 +45,7 @@ export function registerTableTools(server) {
|
|
|
47
45
|
};
|
|
48
46
|
if (response_format === "markdown") {
|
|
49
47
|
const rows = result.recordset;
|
|
50
|
-
return
|
|
48
|
+
return toolSuccessMarkdown(formatMarkdownTable(rows, `Columns: ${schemaName}.${tableName}`));
|
|
51
49
|
}
|
|
52
50
|
return toolSuccess(formatJson(structured), structured);
|
|
53
51
|
}
|
|
@@ -86,7 +84,7 @@ export function registerTableTools(server) {
|
|
|
86
84
|
`);
|
|
87
85
|
const rows = result.recordset ?? [];
|
|
88
86
|
if (response_format === "markdown") {
|
|
89
|
-
return
|
|
87
|
+
return toolSuccessMarkdown(formatMarkdownTable(rows));
|
|
90
88
|
}
|
|
91
89
|
return toolSuccess(formatJson(rows));
|
|
92
90
|
}
|
|
@@ -140,14 +138,6 @@ export function registerTableTools(server) {
|
|
|
140
138
|
.describe("Output format: 'json' for structured data, 'markdown' for human-readable table"),
|
|
141
139
|
},
|
|
142
140
|
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
141
|
}, async ({ tableName, schemaName, columns, limit: rawLimit, offset, whereClause, orderBy, parameters, response_format }) => {
|
|
152
142
|
try {
|
|
153
143
|
const pool = requirePool();
|
|
@@ -185,7 +175,7 @@ export function registerTableTools(server) {
|
|
|
185
175
|
if (truncated)
|
|
186
176
|
text += `\n\n> ⚠️ ${truncation_message}`;
|
|
187
177
|
text += `\n\n*${rows.length} rows · offset ${offset} · ${elapsed}ms*`;
|
|
188
|
-
return
|
|
178
|
+
return toolSuccessMarkdown(text);
|
|
189
179
|
}
|
|
190
180
|
return toolSuccess(formatJson(structured), structured);
|
|
191
181
|
}
|
|
@@ -250,7 +240,7 @@ export function registerTableTools(server) {
|
|
|
250
240
|
structured.truncation_message = truncation_message;
|
|
251
241
|
}
|
|
252
242
|
if (response_format === "markdown") {
|
|
253
|
-
return
|
|
243
|
+
return toolSuccessMarkdown(formatMarkdownTable(data));
|
|
254
244
|
}
|
|
255
245
|
return toolSuccess(formatJson(structured), structured);
|
|
256
246
|
}
|
|
@@ -10,6 +10,12 @@ export declare function toolError(message: string): {
|
|
|
10
10
|
}[];
|
|
11
11
|
isError: true;
|
|
12
12
|
};
|
|
13
|
+
export declare function toolSuccessMarkdown(text: string): {
|
|
14
|
+
content: {
|
|
15
|
+
type: "text";
|
|
16
|
+
text: string;
|
|
17
|
+
}[];
|
|
18
|
+
};
|
|
13
19
|
export declare function toolSuccess(text: string, structuredContent?: Record<string, unknown>): {
|
|
14
20
|
content: Array<{
|
|
15
21
|
type: "text";
|
package/dist/src/utils/errors.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeOutput } from "./format.js";
|
|
1
2
|
export class McpToolError extends Error {
|
|
2
3
|
cause;
|
|
3
4
|
constructor(message, cause) {
|
|
@@ -18,12 +19,17 @@ export function toolError(message) {
|
|
|
18
19
|
isError: true,
|
|
19
20
|
};
|
|
20
21
|
}
|
|
22
|
+
export function toolSuccessMarkdown(text) {
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: "text", text }],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
21
27
|
export function toolSuccess(text, structuredContent) {
|
|
22
28
|
const result = {
|
|
23
29
|
content: [{ type: "text", text }],
|
|
24
30
|
};
|
|
25
31
|
if (structuredContent !== undefined) {
|
|
26
|
-
result.structuredContent = structuredContent;
|
|
32
|
+
result.structuredContent = normalizeOutput(structuredContent);
|
|
27
33
|
}
|
|
28
34
|
return result;
|
|
29
35
|
}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export declare function formatJson(data: unknown): string;
|
|
2
|
+
export declare function formatDisplayValue(value: unknown, column?: {
|
|
3
|
+
type?: unknown;
|
|
4
|
+
scale?: number;
|
|
5
|
+
}): string;
|
|
6
|
+
export declare function normalizeOutput(value: unknown, column?: {
|
|
7
|
+
type?: unknown;
|
|
8
|
+
scale?: number;
|
|
9
|
+
}): unknown;
|
|
2
10
|
export declare function truncatePayload(data: unknown[], maxBytes?: number): {
|
|
3
11
|
data: unknown[];
|
|
4
12
|
truncated: boolean;
|
package/dist/src/utils/format.js
CHANGED
|
@@ -1,18 +1,127 @@
|
|
|
1
1
|
import { MAX_TEXT_PAYLOAD_BYTES } from "../constants.js";
|
|
2
|
+
function pad(value, width) {
|
|
3
|
+
return String(value).padStart(width, "0");
|
|
4
|
+
}
|
|
5
|
+
function getRecordsetColumns(value) {
|
|
6
|
+
if (!Array.isArray(value)) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
const columns = value.columns;
|
|
10
|
+
if (!columns || typeof columns !== "object" || Array.isArray(columns)) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
return columns;
|
|
14
|
+
}
|
|
15
|
+
function getTypeName(column) {
|
|
16
|
+
const type = column?.type;
|
|
17
|
+
if (typeof type === "function") {
|
|
18
|
+
return type.name || undefined;
|
|
19
|
+
}
|
|
20
|
+
if (type && typeof type === "object" && "name" in type) {
|
|
21
|
+
const name = type.name;
|
|
22
|
+
return typeof name === "string" ? name : undefined;
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
function getExtraFraction(date) {
|
|
27
|
+
const raw = typeof date.nanosecondsDelta === "number"
|
|
28
|
+
? date.nanosecondsDelta
|
|
29
|
+
: typeof date.nanosecondDelta === "number"
|
|
30
|
+
? date.nanosecondDelta
|
|
31
|
+
: 0;
|
|
32
|
+
if (!Number.isFinite(raw)) {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
return Math.max(0, Math.min(9999, Math.round(raw * 10_000_000)));
|
|
36
|
+
}
|
|
37
|
+
function buildFraction(date, milliseconds, scale) {
|
|
38
|
+
const base = `${pad(milliseconds, 3)}${pad(getExtraFraction(date), 4)}`;
|
|
39
|
+
const safeScale = scale === undefined ? undefined : Math.max(0, Math.min(7, scale));
|
|
40
|
+
if (safeScale === 0) {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
if (safeScale !== undefined) {
|
|
44
|
+
return base.slice(0, safeScale);
|
|
45
|
+
}
|
|
46
|
+
return base.replace(/0+$/, "");
|
|
47
|
+
}
|
|
48
|
+
function formatDateParts(date, useUtcClock) {
|
|
49
|
+
return {
|
|
50
|
+
year: useUtcClock ? date.getUTCFullYear() : date.getFullYear(),
|
|
51
|
+
month: (useUtcClock ? date.getUTCMonth() : date.getMonth()) + 1,
|
|
52
|
+
day: useUtcClock ? date.getUTCDate() : date.getDate(),
|
|
53
|
+
hours: useUtcClock ? date.getUTCHours() : date.getHours(),
|
|
54
|
+
minutes: useUtcClock ? date.getUTCMinutes() : date.getMinutes(),
|
|
55
|
+
seconds: useUtcClock ? date.getUTCSeconds() : date.getSeconds(),
|
|
56
|
+
milliseconds: useUtcClock ? date.getUTCMilliseconds() : date.getMilliseconds(),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function formatDateValue(date, column) {
|
|
60
|
+
const typeName = getTypeName(column)?.toLowerCase();
|
|
61
|
+
const useUtcClock = typeName === "datetimeoffset";
|
|
62
|
+
const parts = formatDateParts(date, useUtcClock);
|
|
63
|
+
const fraction = buildFraction(date, parts.milliseconds, column?.scale);
|
|
64
|
+
const dateText = `${pad(parts.year, 4)}-${pad(parts.month, 2)}-${pad(parts.day, 2)}`;
|
|
65
|
+
const timeText = `${pad(parts.hours, 2)}:${pad(parts.minutes, 2)}:${pad(parts.seconds, 2)}`;
|
|
66
|
+
if (typeName === "date") {
|
|
67
|
+
return dateText;
|
|
68
|
+
}
|
|
69
|
+
if (typeName === "time") {
|
|
70
|
+
return fraction ? `${timeText}.${fraction}` : timeText;
|
|
71
|
+
}
|
|
72
|
+
return fraction ? `${dateText} ${timeText}.${fraction}` : `${dateText} ${timeText}`;
|
|
73
|
+
}
|
|
74
|
+
function isPlainObject(value) {
|
|
75
|
+
return typeof value === "object"
|
|
76
|
+
&& value !== null
|
|
77
|
+
&& !Array.isArray(value)
|
|
78
|
+
&& !Buffer.isBuffer(value)
|
|
79
|
+
&& !(value instanceof Date);
|
|
80
|
+
}
|
|
81
|
+
function normalizeRecordRow(row, columns) {
|
|
82
|
+
return Object.fromEntries(Object.entries(row).map(([key, value]) => [key, normalizeOutput(value, columns[key])]));
|
|
83
|
+
}
|
|
2
84
|
export function formatJson(data) {
|
|
3
|
-
return JSON.stringify(data, null, 2);
|
|
85
|
+
return JSON.stringify(normalizeOutput(data), null, 2);
|
|
86
|
+
}
|
|
87
|
+
export function formatDisplayValue(value, column) {
|
|
88
|
+
const normalized = normalizeOutput(value, column);
|
|
89
|
+
if (normalized == null) {
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
if (typeof normalized === "object") {
|
|
93
|
+
return JSON.stringify(normalized);
|
|
94
|
+
}
|
|
95
|
+
return String(normalized);
|
|
96
|
+
}
|
|
97
|
+
export function normalizeOutput(value, column) {
|
|
98
|
+
if (value instanceof Date) {
|
|
99
|
+
return formatDateValue(value, column);
|
|
100
|
+
}
|
|
101
|
+
const columns = getRecordsetColumns(value);
|
|
102
|
+
if (Array.isArray(value)) {
|
|
103
|
+
if (columns) {
|
|
104
|
+
return value.map((row) => isPlainObject(row) ? normalizeRecordRow(row, columns) : normalizeOutput(row));
|
|
105
|
+
}
|
|
106
|
+
return value.map((entry) => normalizeOutput(entry));
|
|
107
|
+
}
|
|
108
|
+
if (isPlainObject(value)) {
|
|
109
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, normalizeOutput(entry)]));
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
4
112
|
}
|
|
5
113
|
export function truncatePayload(data, maxBytes = MAX_TEXT_PAYLOAD_BYTES) {
|
|
6
|
-
const
|
|
114
|
+
const normalizedData = normalizeOutput(data);
|
|
115
|
+
const full = JSON.stringify(normalizedData);
|
|
7
116
|
if (Buffer.byteLength(full, "utf8") <= maxBytes) {
|
|
8
|
-
return { data, truncated: false };
|
|
117
|
+
return { data: normalizedData, truncated: false };
|
|
9
118
|
}
|
|
10
119
|
// Binary search for safe row count
|
|
11
120
|
let lo = 0;
|
|
12
|
-
let hi =
|
|
121
|
+
let hi = normalizedData.length;
|
|
13
122
|
while (lo < hi) {
|
|
14
123
|
const mid = Math.floor((lo + hi + 1) / 2);
|
|
15
|
-
if (Buffer.byteLength(JSON.stringify(
|
|
124
|
+
if (Buffer.byteLength(JSON.stringify(normalizedData.slice(0, mid)), "utf8") <= maxBytes) {
|
|
16
125
|
lo = mid;
|
|
17
126
|
}
|
|
18
127
|
else {
|
|
@@ -20,8 +129,8 @@ export function truncatePayload(data, maxBytes = MAX_TEXT_PAYLOAD_BYTES) {
|
|
|
20
129
|
}
|
|
21
130
|
}
|
|
22
131
|
return {
|
|
23
|
-
data:
|
|
132
|
+
data: normalizedData.slice(0, lo),
|
|
24
133
|
truncated: true,
|
|
25
|
-
truncation_message: `Result truncated to ${lo} of ${
|
|
134
|
+
truncation_message: `Result truncated to ${lo} of ${normalizedData.length} rows to stay within ${maxBytes} byte limit.`,
|
|
26
135
|
};
|
|
27
136
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { formatDisplayValue } from "./format.js";
|
|
2
|
+
function escapeCell(value, column) {
|
|
3
|
+
return formatDisplayValue(value, column)
|
|
3
4
|
.replace(/\r?\n|\r/g, " ")
|
|
4
5
|
.replace(/\|/g, "\\|");
|
|
5
6
|
}
|
|
@@ -7,17 +8,18 @@ export function formatMarkdownTable(rows, title) {
|
|
|
7
8
|
if (rows.length === 0) {
|
|
8
9
|
return title ? `**${title}**\n\nNo results found.` : "No results found.";
|
|
9
10
|
}
|
|
11
|
+
const columns = (rows.columns) ?? undefined;
|
|
10
12
|
const headers = Object.keys(rows[0]);
|
|
11
13
|
const header = `| ${headers.join(" | ")} |`;
|
|
12
14
|
const sep = `| ${headers.map(() => "---").join(" | ")} |`;
|
|
13
15
|
const body = rows
|
|
14
|
-
.map((row) => `| ${headers.map((h) => escapeCell(row[h])).join(" | ")} |`)
|
|
16
|
+
.map((row) => `| ${headers.map((h) => escapeCell(row[h], columns?.[h])).join(" | ")} |`)
|
|
15
17
|
.join("\n");
|
|
16
18
|
const table = [header, sep, body].join("\n");
|
|
17
19
|
return title ? `**${title}**\n\n${table}` : table;
|
|
18
20
|
}
|
|
19
21
|
export function formatMarkdownList(obj, title) {
|
|
20
|
-
const lines = Object.entries(obj).map(([k, v]) => `- **${k}**: ${
|
|
22
|
+
const lines = Object.entries(obj).map(([k, v]) => `- **${k}**: ${formatDisplayValue(v)}`);
|
|
21
23
|
const list = lines.join("\n");
|
|
22
24
|
return title ? `**${title}**\n\n${list}` : list;
|
|
23
25
|
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { strict as assert } from "node:assert";
|
|
2
2
|
import { test } from "node:test";
|
|
3
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
|
+
}
|
|
4
14
|
test("formatMarkdownTable - renders header and rows", () => {
|
|
5
15
|
const rows = [
|
|
6
16
|
{ name: "Users", schema: "dbo" },
|
|
@@ -48,6 +58,16 @@ test("formatMarkdownTable - CRLF in cell values are replaced with space", () =>
|
|
|
48
58
|
const result = formatMarkdownTable(rows);
|
|
49
59
|
assert.ok(result.includes("line1 line2"));
|
|
50
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
|
+
});
|
|
51
71
|
test("formatMarkdownList - renders key-value pairs", () => {
|
|
52
72
|
const result = formatMarkdownList({ status: "connected", server: "localhost" });
|
|
53
73
|
assert.ok(result.includes("**status**: connected"));
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { strict as assert } from "node:assert";
|
|
2
2
|
import { test } from "node:test";
|
|
3
|
+
import { toolSuccess, toolSuccessMarkdown } from "../../src/utils/errors.js";
|
|
3
4
|
import { buildPaginationMeta, clampLimit } from "../../src/utils/pagination.js";
|
|
4
|
-
import { truncatePayload } from "../../src/utils/format.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
|
+
}
|
|
5
16
|
test("clampLimit - defaults to DEFAULT_PAGE_SIZE (20)", () => {
|
|
6
17
|
assert.equal(clampLimit(undefined), 20);
|
|
7
18
|
});
|
|
@@ -60,3 +71,25 @@ test("truncatePayload - empty array not truncated", () => {
|
|
|
60
71
|
assert.equal(result.truncated, false);
|
|
61
72
|
assert.equal(result.data.length, 0);
|
|
62
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
|
+
});
|