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.
- package/LICENSE +21 -0
- package/README.md +161 -89
- package/dist/src/config.d.ts +39 -0
- package/dist/src/config.js +37 -0
- package/dist/src/constants.d.ts +15 -0
- package/dist/src/constants.js +15 -0
- package/dist/src/db/connection.d.ts +8 -0
- package/dist/src/db/connection.js +80 -0
- package/dist/src/db/query-builders.d.ts +3 -0
- package/dist/src/db/query-builders.js +58 -0
- package/dist/src/db/validators.d.ts +5 -0
- package/dist/src/db/validators.js +25 -0
- package/dist/src/index.js +26 -0
- package/dist/src/resources/connection.d.ts +2 -0
- package/dist/src/resources/connection.js +19 -0
- package/dist/src/resources/metadata.d.ts +2 -0
- package/dist/src/resources/metadata.js +58 -0
- package/dist/src/schemas/outputs.d.ts +153 -0
- package/dist/src/schemas/outputs.js +54 -0
- package/dist/src/server.d.ts +2 -0
- package/dist/src/server.js +27 -0
- package/dist/src/tools/connect.d.ts +2 -0
- package/dist/src/tools/connect.js +45 -0
- package/dist/src/tools/databases.d.ts +2 -0
- package/dist/src/tools/databases.js +53 -0
- package/dist/src/tools/procedure.d.ts +2 -0
- package/dist/src/tools/procedure.js +106 -0
- package/dist/src/tools/query.d.ts +2 -0
- package/dist/src/tools/query.js +92 -0
- package/dist/src/tools/schema.d.ts +2 -0
- package/dist/src/tools/schema.js +96 -0
- package/dist/src/tools/status.d.ts +2 -0
- package/dist/src/tools/status.js +17 -0
- package/dist/src/tools/table.d.ts +2 -0
- package/dist/src/tools/table.js +261 -0
- package/dist/src/transports/http.d.ts +3 -0
- package/dist/src/transports/http.js +54 -0
- package/dist/src/transports/stdio.d.ts +2 -0
- package/dist/src/transports/stdio.js +23 -0
- package/dist/src/types.d.ts +37 -0
- package/dist/src/types.js +1 -0
- package/dist/src/utils/errors.d.ts +19 -0
- package/dist/src/utils/errors.js +29 -0
- package/dist/src/utils/format.d.ts +6 -0
- package/dist/src/utils/format.js +27 -0
- package/dist/src/utils/markdown.d.ts +3 -0
- package/dist/src/utils/markdown.js +33 -0
- package/dist/src/utils/pagination.d.ts +3 -0
- package/dist/src/utils/pagination.js +18 -0
- package/dist/tests/unit/markdown.test.d.ts +1 -0
- package/dist/tests/unit/markdown.test.js +70 -0
- package/dist/tests/unit/query-builders.test.d.ts +1 -0
- package/dist/tests/unit/query-builders.test.js +63 -0
- package/dist/tests/unit/tool-contracts.test.d.ts +1 -0
- package/dist/tests/unit/tool-contracts.test.js +62 -0
- package/dist/tests/unit/validators.test.d.ts +1 -0
- package/dist/tests/unit/validators.test.js +51 -0
- package/package.json +10 -6
- package/dist/index.js +0 -648
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
test("formatMarkdownTable - renders header and rows", () => {
|
|
5
|
+
const rows = [
|
|
6
|
+
{ name: "Users", schema: "dbo" },
|
|
7
|
+
{ name: "Orders", schema: "sales" },
|
|
8
|
+
];
|
|
9
|
+
const result = formatMarkdownTable(rows);
|
|
10
|
+
assert.ok(result.includes("| name | schema |"), "header row missing");
|
|
11
|
+
assert.ok(result.includes("| --- |"), "separator row missing");
|
|
12
|
+
assert.ok(result.includes("| Users | dbo |"), "data row 1 missing");
|
|
13
|
+
assert.ok(result.includes("| Orders | sales |"), "data row 2 missing");
|
|
14
|
+
});
|
|
15
|
+
test("formatMarkdownTable - includes title when provided", () => {
|
|
16
|
+
const rows = [{ id: 1 }];
|
|
17
|
+
const result = formatMarkdownTable(rows, "My Table");
|
|
18
|
+
assert.ok(result.startsWith("**My Table**"), "title missing");
|
|
19
|
+
});
|
|
20
|
+
test("formatMarkdownTable - empty rows returns no results message", () => {
|
|
21
|
+
const result = formatMarkdownTable([]);
|
|
22
|
+
assert.ok(result.includes("No results found."));
|
|
23
|
+
});
|
|
24
|
+
test("formatMarkdownTable - empty rows with title includes title", () => {
|
|
25
|
+
const result = formatMarkdownTable([], "Empty");
|
|
26
|
+
assert.ok(result.includes("**Empty**"));
|
|
27
|
+
assert.ok(result.includes("No results found."));
|
|
28
|
+
});
|
|
29
|
+
test("formatMarkdownTable - null/undefined values render as empty string", () => {
|
|
30
|
+
const rows = [{ id: 1, value: null, flag: undefined }];
|
|
31
|
+
const result = formatMarkdownTable(rows);
|
|
32
|
+
assert.ok(result.includes("| 1 |"));
|
|
33
|
+
});
|
|
34
|
+
test("formatMarkdownTable - pipe characters in cell values are escaped", () => {
|
|
35
|
+
const rows = [{ name: "foo | bar", status: "ok" }];
|
|
36
|
+
const result = formatMarkdownTable(rows);
|
|
37
|
+
assert.ok(result.includes("foo \\| bar"), "pipe not escaped");
|
|
38
|
+
assert.ok(!result.includes("foo | bar"), "raw pipe should not appear in data row");
|
|
39
|
+
});
|
|
40
|
+
test("formatMarkdownTable - newlines in cell values are replaced with space", () => {
|
|
41
|
+
const rows = [{ notes: "line1\nline2", other: "x" }];
|
|
42
|
+
const result = formatMarkdownTable(rows);
|
|
43
|
+
assert.ok(result.includes("line1 line2"), "newline not replaced");
|
|
44
|
+
assert.ok(!result.includes("line1\nline2"), "raw newline should not appear");
|
|
45
|
+
});
|
|
46
|
+
test("formatMarkdownTable - CRLF in cell values are replaced with space", () => {
|
|
47
|
+
const rows = [{ notes: "line1\r\nline2" }];
|
|
48
|
+
const result = formatMarkdownTable(rows);
|
|
49
|
+
assert.ok(result.includes("line1 line2"));
|
|
50
|
+
});
|
|
51
|
+
test("formatMarkdownList - renders key-value pairs", () => {
|
|
52
|
+
const result = formatMarkdownList({ status: "connected", server: "localhost" });
|
|
53
|
+
assert.ok(result.includes("**status**: connected"));
|
|
54
|
+
assert.ok(result.includes("**server**: localhost"));
|
|
55
|
+
});
|
|
56
|
+
test("formatMarkdownList - includes title when provided", () => {
|
|
57
|
+
const result = formatMarkdownList({ x: 1 }, "Info");
|
|
58
|
+
assert.ok(result.startsWith("**Info**"));
|
|
59
|
+
});
|
|
60
|
+
test("formatMarkdownMultiRecordsets - renders multiple result sets", () => {
|
|
61
|
+
const rs1 = [{ id: 1 }];
|
|
62
|
+
const rs2 = [{ name: "foo" }];
|
|
63
|
+
const result = formatMarkdownMultiRecordsets([rs1, rs2]);
|
|
64
|
+
assert.ok(result.includes("Result Set 1"));
|
|
65
|
+
assert.ok(result.includes("Result Set 2"));
|
|
66
|
+
});
|
|
67
|
+
test("formatMarkdownMultiRecordsets - empty recordsets returns message", () => {
|
|
68
|
+
const result = formatMarkdownMultiRecordsets([]);
|
|
69
|
+
assert.ok(result.includes("No result sets returned."));
|
|
70
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { buildPaginationMeta, clampLimit } from "../../src/utils/pagination.js";
|
|
4
|
+
import { truncatePayload } from "../../src/utils/format.js";
|
|
5
|
+
test("clampLimit - defaults to DEFAULT_PAGE_SIZE (20)", () => {
|
|
6
|
+
assert.equal(clampLimit(undefined), 20);
|
|
7
|
+
});
|
|
8
|
+
test("clampLimit - enforces max (200)", () => {
|
|
9
|
+
assert.equal(clampLimit(9999), 200);
|
|
10
|
+
assert.equal(clampLimit(201), 200);
|
|
11
|
+
});
|
|
12
|
+
test("clampLimit - enforces min (1)", () => {
|
|
13
|
+
assert.equal(clampLimit(0), 1);
|
|
14
|
+
assert.equal(clampLimit(-5), 1);
|
|
15
|
+
});
|
|
16
|
+
test("clampLimit - passes through valid value", () => {
|
|
17
|
+
assert.equal(clampLimit(50), 50);
|
|
18
|
+
assert.equal(clampLimit(1), 1);
|
|
19
|
+
assert.equal(clampLimit(200), 200);
|
|
20
|
+
});
|
|
21
|
+
test("buildPaginationMeta - has_more true when count === limit", () => {
|
|
22
|
+
const p = buildPaginationMeta(20, 20, 0);
|
|
23
|
+
assert.equal(p.has_more, true);
|
|
24
|
+
assert.equal(p.next_offset, 20);
|
|
25
|
+
assert.equal(p.offset, 0);
|
|
26
|
+
assert.equal(p.count, 20);
|
|
27
|
+
assert.equal(p.limit, 20);
|
|
28
|
+
});
|
|
29
|
+
test("buildPaginationMeta - has_more false when count < limit", () => {
|
|
30
|
+
const p = buildPaginationMeta(5, 20, 0);
|
|
31
|
+
assert.equal(p.has_more, false);
|
|
32
|
+
assert.equal(p.next_offset, null);
|
|
33
|
+
});
|
|
34
|
+
test("buildPaginationMeta - includes total_count when provided", () => {
|
|
35
|
+
const p = buildPaginationMeta(20, 20, 0, 100);
|
|
36
|
+
assert.equal(p.total_count, 100);
|
|
37
|
+
});
|
|
38
|
+
test("buildPaginationMeta - next_offset uses offset correctly", () => {
|
|
39
|
+
const p = buildPaginationMeta(20, 20, 40);
|
|
40
|
+
assert.equal(p.next_offset, 60);
|
|
41
|
+
});
|
|
42
|
+
test("truncatePayload - no truncation for small data", () => {
|
|
43
|
+
const data = [{ id: 1 }, { id: 2 }];
|
|
44
|
+
const result = truncatePayload(data, 100_000);
|
|
45
|
+
assert.equal(result.truncated, false);
|
|
46
|
+
assert.equal(result.data.length, 2);
|
|
47
|
+
assert.equal(result.truncation_message, undefined);
|
|
48
|
+
});
|
|
49
|
+
test("truncatePayload - truncates when over byte limit", () => {
|
|
50
|
+
const largeRow = { data: "x".repeat(1000) };
|
|
51
|
+
const rows = Array.from({ length: 200 }, (_, i) => ({ ...largeRow, id: i }));
|
|
52
|
+
const result = truncatePayload(rows, 10_000);
|
|
53
|
+
assert.equal(result.truncated, true);
|
|
54
|
+
assert.ok(result.data.length < 200, `Expected < 200 rows, got ${result.data.length}`);
|
|
55
|
+
assert.ok(result.truncation_message?.includes("truncated"));
|
|
56
|
+
assert.ok(result.truncation_message?.includes("200"));
|
|
57
|
+
});
|
|
58
|
+
test("truncatePayload - empty array not truncated", () => {
|
|
59
|
+
const result = truncatePayload([], 100_000);
|
|
60
|
+
assert.equal(result.truncated, false);
|
|
61
|
+
assert.equal(result.data.length, 0);
|
|
62
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
});
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mssql-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "MCP Server for MS SQL Server integration with Claude Desktop, Cursor, Windsurf and VS Code",
|
|
5
|
-
"main": "dist/index.js",
|
|
5
|
+
"main": "dist/src/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"mssql-mcp-server": "dist/index.js"
|
|
8
|
+
"mssql-mcp-server": "dist/src/index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist/**/*",
|
|
@@ -26,10 +26,13 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "tsc",
|
|
28
28
|
"dev": "tsc --watch",
|
|
29
|
-
"start": "node dist/index.js",
|
|
29
|
+
"start": "node dist/src/index.js",
|
|
30
|
+
"start:http": "cross-env MCP_TRANSPORT=http node dist/src/index.js",
|
|
30
31
|
"clean": "rimraf dist",
|
|
31
32
|
"prepublishOnly": "npm run clean && npm run build",
|
|
32
|
-
"
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"test": "node --test dist/tests/unit/validators.test.js dist/tests/unit/query-builders.test.js dist/tests/unit/tool-contracts.test.js dist/tests/unit/markdown.test.js",
|
|
35
|
+
"ci": "npm run typecheck && npm run build && npm test"
|
|
33
36
|
},
|
|
34
37
|
"keywords": [
|
|
35
38
|
"mcp",
|
|
@@ -45,7 +48,7 @@
|
|
|
45
48
|
"author": "BYMCS <hello@bymcs.com>",
|
|
46
49
|
"license": "MIT",
|
|
47
50
|
"dependencies": {
|
|
48
|
-
"@modelcontextprotocol/sdk": "
|
|
51
|
+
"@modelcontextprotocol/sdk": "1.28.0",
|
|
49
52
|
"dotenv": "^16.3.1",
|
|
50
53
|
"mssql": "^11.0.1",
|
|
51
54
|
"zod": "^3.25.0"
|
|
@@ -53,6 +56,7 @@
|
|
|
53
56
|
"devDependencies": {
|
|
54
57
|
"@types/mssql": "^9.1.7",
|
|
55
58
|
"@types/node": "^20.0.0",
|
|
59
|
+
"cross-env": "^7.0.3",
|
|
56
60
|
"rimraf": "^5.0.0",
|
|
57
61
|
"typescript": "^5.3.0"
|
|
58
62
|
}
|