@eamonboyle/mssql-mcp 1.1.0 → 1.2.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 (129) hide show
  1. package/README.md +134 -22
  2. package/dist/__tests__/ExplainQueryTool.test.d.ts +2 -0
  3. package/dist/__tests__/ExplainQueryTool.test.d.ts.map +1 -0
  4. package/dist/__tests__/ExplainQueryTool.test.js +96 -0
  5. package/dist/__tests__/ExplainQueryTool.test.js.map +1 -0
  6. package/dist/__tests__/config.test.d.ts +2 -0
  7. package/dist/__tests__/config.test.d.ts.map +1 -0
  8. package/dist/__tests__/config.test.js +29 -0
  9. package/dist/__tests__/config.test.js.map +1 -0
  10. package/dist/__tests__/db.test.d.ts +1 -1
  11. package/dist/__tests__/db.test.js +203 -203
  12. package/dist/__tests__/promptRegistry.test.d.ts +2 -0
  13. package/dist/__tests__/promptRegistry.test.d.ts.map +1 -0
  14. package/dist/__tests__/promptRegistry.test.js +40 -0
  15. package/dist/__tests__/promptRegistry.test.js.map +1 -0
  16. package/dist/__tests__/resourceRegistry.test.d.ts +2 -0
  17. package/dist/__tests__/resourceRegistry.test.d.ts.map +1 -0
  18. package/dist/__tests__/resourceRegistry.test.js +40 -0
  19. package/dist/__tests__/resourceRegistry.test.js.map +1 -0
  20. package/dist/__tests__/schema.test.d.ts +2 -0
  21. package/dist/__tests__/schema.test.d.ts.map +1 -0
  22. package/dist/__tests__/schema.test.js +130 -0
  23. package/dist/__tests__/schema.test.js.map +1 -0
  24. package/dist/__tests__/sql.test.d.ts +2 -0
  25. package/dist/__tests__/sql.test.d.ts.map +1 -0
  26. package/dist/__tests__/sql.test.js +35 -0
  27. package/dist/__tests__/sql.test.js.map +1 -0
  28. package/dist/__tests__/toolRegistry.test.d.ts +2 -0
  29. package/dist/__tests__/toolRegistry.test.d.ts.map +1 -0
  30. package/dist/__tests__/toolRegistry.test.js +47 -0
  31. package/dist/__tests__/toolRegistry.test.js.map +1 -0
  32. package/dist/__tests__/validation.test.d.ts +1 -1
  33. package/dist/__tests__/validation.test.js +110 -110
  34. package/dist/__tests__/validation.test.js.map +1 -1
  35. package/dist/__tests__/writeTools.test.d.ts +2 -0
  36. package/dist/__tests__/writeTools.test.d.ts.map +1 -0
  37. package/dist/__tests__/writeTools.test.js +115 -0
  38. package/dist/__tests__/writeTools.test.js.map +1 -0
  39. package/dist/config.d.ts +5 -0
  40. package/dist/config.d.ts.map +1 -0
  41. package/dist/config.js +34 -0
  42. package/dist/config.js.map +1 -0
  43. package/dist/db.d.ts +28 -24
  44. package/dist/db.d.ts.map +1 -1
  45. package/dist/db.js +144 -112
  46. package/dist/db.js.map +1 -1
  47. package/dist/index.d.ts +2 -2
  48. package/dist/index.js +70 -122
  49. package/dist/index.js.map +1 -1
  50. package/dist/promptRegistry.d.ts +16 -0
  51. package/dist/promptRegistry.d.ts.map +1 -0
  52. package/dist/promptRegistry.js +107 -0
  53. package/dist/promptRegistry.js.map +1 -0
  54. package/dist/resourceRegistry.d.ts +12 -0
  55. package/dist/resourceRegistry.d.ts.map +1 -0
  56. package/dist/resourceRegistry.js +172 -0
  57. package/dist/resourceRegistry.js.map +1 -0
  58. package/dist/schema.d.ts +41 -0
  59. package/dist/schema.d.ts.map +1 -0
  60. package/dist/schema.js +211 -0
  61. package/dist/schema.js.map +1 -0
  62. package/dist/sql.d.ts +7 -0
  63. package/dist/sql.d.ts.map +1 -0
  64. package/dist/sql.js +73 -0
  65. package/dist/sql.js.map +1 -0
  66. package/dist/toolRegistry.d.ts +16 -0
  67. package/dist/toolRegistry.d.ts.map +1 -0
  68. package/dist/toolRegistry.js +401 -0
  69. package/dist/toolRegistry.js.map +1 -0
  70. package/dist/tools/CreateIndexTool.d.ts +30 -23
  71. package/dist/tools/CreateIndexTool.d.ts.map +1 -1
  72. package/dist/tools/CreateIndexTool.js +47 -77
  73. package/dist/tools/CreateIndexTool.js.map +1 -1
  74. package/dist/tools/CreateTableTool.d.ts +18 -11
  75. package/dist/tools/CreateTableTool.d.ts.map +1 -1
  76. package/dist/tools/CreateTableTool.js +37 -61
  77. package/dist/tools/CreateTableTool.js.map +1 -1
  78. package/dist/tools/DeleteDataTool.d.ts +22 -0
  79. package/dist/tools/DeleteDataTool.d.ts.map +1 -0
  80. package/dist/tools/DeleteDataTool.js +35 -0
  81. package/dist/tools/DeleteDataTool.js.map +1 -0
  82. package/dist/tools/DescribeObjectTool.d.ts +21 -0
  83. package/dist/tools/DescribeObjectTool.d.ts.map +1 -0
  84. package/dist/tools/DescribeObjectTool.js +31 -0
  85. package/dist/tools/DescribeObjectTool.js.map +1 -0
  86. package/dist/tools/DescribeTableTool.d.ts +19 -20
  87. package/dist/tools/DescribeTableTool.d.ts.map +1 -1
  88. package/dist/tools/DescribeTableTool.js +23 -44
  89. package/dist/tools/DescribeTableTool.js.map +1 -1
  90. package/dist/tools/DropTableTool.d.ts +13 -11
  91. package/dist/tools/DropTableTool.d.ts.map +1 -1
  92. package/dist/tools/DropTableTool.js +30 -44
  93. package/dist/tools/DropTableTool.js.map +1 -1
  94. package/dist/tools/ExplainQueryTool.d.ts +32 -0
  95. package/dist/tools/ExplainQueryTool.d.ts.map +1 -0
  96. package/dist/tools/ExplainQueryTool.js +102 -0
  97. package/dist/tools/ExplainQueryTool.js.map +1 -0
  98. package/dist/tools/InsertDataTool.d.ts +20 -16
  99. package/dist/tools/InsertDataTool.d.ts.map +1 -1
  100. package/dist/tools/InsertDataTool.js +91 -108
  101. package/dist/tools/InsertDataTool.js.map +1 -1
  102. package/dist/tools/ListObjectsTool.d.ts +20 -0
  103. package/dist/tools/ListObjectsTool.d.ts.map +1 -0
  104. package/dist/tools/ListObjectsTool.js +26 -0
  105. package/dist/tools/ListObjectsTool.js.map +1 -0
  106. package/dist/tools/ListTableTool.d.ts +20 -16
  107. package/dist/tools/ListTableTool.d.ts.map +1 -1
  108. package/dist/tools/ListTableTool.js +32 -51
  109. package/dist/tools/ListTableTool.js.map +1 -1
  110. package/dist/tools/ReadDataTool.d.ts +35 -33
  111. package/dist/tools/ReadDataTool.d.ts.map +1 -1
  112. package/dist/tools/ReadDataTool.js +92 -105
  113. package/dist/tools/ReadDataTool.js.map +1 -1
  114. package/dist/tools/SearchDataTool.d.ts +27 -0
  115. package/dist/tools/SearchDataTool.d.ts.map +1 -0
  116. package/dist/tools/SearchDataTool.js +61 -0
  117. package/dist/tools/SearchDataTool.js.map +1 -0
  118. package/dist/tools/UpdateDataTool.d.ts +21 -16
  119. package/dist/tools/UpdateDataTool.d.ts.map +1 -1
  120. package/dist/tools/UpdateDataTool.js +45 -65
  121. package/dist/tools/UpdateDataTool.js.map +1 -1
  122. package/dist/validation.d.ts +13 -13
  123. package/dist/validation.js +133 -133
  124. package/dist/validation.js.map +1 -1
  125. package/dist/writeSafety.d.ts +21 -0
  126. package/dist/writeSafety.d.ts.map +1 -0
  127. package/dist/writeSafety.js +169 -0
  128. package/dist/writeSafety.js.map +1 -0
  129. package/package.json +5 -4
@@ -1,204 +1,204 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
- const mssqlMockState = vi.hoisted(() => ({
3
- poolConfigs: [],
4
- requestObject: { kind: "mock-request" },
5
- connectCalls: vi.fn(),
6
- closeCalls: vi.fn(),
7
- requestCalls: vi.fn(),
8
- }));
9
- // Mock mssql to avoid loading native deps and transitive issues in test env
10
- vi.mock("mssql", () => {
11
- class MockConnectionPool {
12
- constructor(config) {
13
- this.config = config;
14
- this.connected = false;
15
- mssqlMockState.poolConfigs.push(config);
16
- }
17
- async connect() {
18
- this.connected = true;
19
- mssqlMockState.connectCalls(this.config);
20
- return this;
21
- }
22
- async close() {
23
- this.connected = false;
24
- mssqlMockState.closeCalls(this.config);
25
- }
26
- request() {
27
- mssqlMockState.requestCalls(this.config);
28
- return mssqlMockState.requestObject;
29
- }
30
- }
31
- return {
32
- default: {
33
- ConnectionPool: MockConnectionPool,
34
- },
35
- };
36
- });
37
- import { getAllowedDatabases, getSqlRequest, resolveDatabaseName } from "../db.js";
38
- describe("getAllowedDatabases", () => {
39
- const originalEnv = process.env;
40
- beforeEach(() => {
41
- process.env = { ...originalEnv };
42
- mssqlMockState.poolConfigs.length = 0;
43
- mssqlMockState.connectCalls.mockClear();
44
- mssqlMockState.closeCalls.mockClear();
45
- mssqlMockState.requestCalls.mockClear();
46
- });
47
- afterEach(() => {
48
- process.env = originalEnv;
49
- });
50
- it("returns DATABASE_NAME when DATABASES is not set", () => {
51
- delete process.env.DATABASES;
52
- process.env.DATABASE_NAME = "MyDb";
53
- expect(getAllowedDatabases()).toEqual(["MyDb"]);
54
- });
55
- it("returns empty array when neither DATABASES nor DATABASE_NAME is set", () => {
56
- delete process.env.DATABASES;
57
- delete process.env.DATABASE_NAME;
58
- expect(getAllowedDatabases()).toEqual([]);
59
- });
60
- it("returns empty array when DATABASE_NAME is empty string", () => {
61
- delete process.env.DATABASES;
62
- process.env.DATABASE_NAME = "";
63
- expect(getAllowedDatabases()).toEqual([]);
64
- });
65
- it("returns DATABASES split by comma when set", () => {
66
- process.env.DATABASES = "ProdDB,StagingDB,AnalyticsDB";
67
- expect(getAllowedDatabases()).toEqual([
68
- "ProdDB",
69
- "StagingDB",
70
- "AnalyticsDB",
71
- ]);
72
- });
73
- it("trims whitespace from DATABASES entries", () => {
74
- process.env.DATABASES = " ProdDB , StagingDB , AnalyticsDB ";
75
- expect(getAllowedDatabases()).toEqual([
76
- "ProdDB",
77
- "StagingDB",
78
- "AnalyticsDB",
79
- ]);
80
- });
81
- it("filters out empty entries from DATABASES", () => {
82
- process.env.DATABASES = "ProdDB,,StagingDB,";
83
- expect(getAllowedDatabases()).toEqual(["ProdDB", "StagingDB"]);
84
- });
85
- it("prefers DATABASES over DATABASE_NAME when both set", () => {
86
- process.env.DATABASES = "ProdDB,StagingDB";
87
- process.env.DATABASE_NAME = "OtherDb";
88
- expect(getAllowedDatabases()).toEqual(["ProdDB", "StagingDB"]);
89
- });
90
- });
91
- describe("resolveDatabaseName", () => {
92
- const originalEnv = process.env;
93
- beforeEach(() => {
94
- process.env = { ...originalEnv };
95
- mssqlMockState.poolConfigs.length = 0;
96
- mssqlMockState.connectCalls.mockClear();
97
- mssqlMockState.closeCalls.mockClear();
98
- mssqlMockState.requestCalls.mockClear();
99
- });
100
- afterEach(() => {
101
- process.env = originalEnv;
102
- });
103
- it("returns param when valid and in allowed list", () => {
104
- process.env.DATABASES = "ProdDB,StagingDB";
105
- expect(resolveDatabaseName("ProdDB")).toBe("ProdDB");
106
- expect(resolveDatabaseName("StagingDB")).toBe("StagingDB");
107
- });
108
- it("returns DATABASE_NAME when param omitted and DATABASES not set", () => {
109
- process.env.DATABASE_NAME = "DefaultDb";
110
- delete process.env.DATABASES;
111
- expect(resolveDatabaseName()).toBe("DefaultDb");
112
- });
113
- it("returns DATABASE_NAME when it is in DATABASES", () => {
114
- process.env.DATABASES = "ProdDB,StagingDB";
115
- process.env.DATABASE_NAME = "StagingDB";
116
- expect(resolveDatabaseName()).toBe("StagingDB");
117
- });
118
- it("returns the first DATABASES entry when DATABASE_NAME is omitted", () => {
119
- process.env.DATABASES = "ProdDB,StagingDB";
120
- delete process.env.DATABASE_NAME;
121
- expect(resolveDatabaseName()).toBe("ProdDB");
122
- });
123
- it("returns param when param provided and in allowed list", () => {
124
- process.env.DATABASES = "ProdDB,StagingDB";
125
- process.env.DATABASE_NAME = "ProdDB";
126
- expect(resolveDatabaseName("StagingDB")).toBe("StagingDB");
127
- });
128
- it("falls back to the first DATABASES entry when DATABASE_NAME is not included in DATABASES", () => {
129
- process.env.DATABASES = "ProdDB,StagingDB";
130
- process.env.DATABASE_NAME = "OtherDb";
131
- expect(resolveDatabaseName()).toBe("ProdDB");
132
- });
133
- it("returns null when param not in allowed list", () => {
134
- process.env.DATABASES = "ProdDB,StagingDB";
135
- expect(resolveDatabaseName("OtherDb")).toBeNull();
136
- });
137
- it("returns null when param is empty and DATABASE_NAME not set", () => {
138
- delete process.env.DATABASE_NAME;
139
- delete process.env.DATABASES;
140
- expect(resolveDatabaseName()).toBeNull();
141
- expect(resolveDatabaseName("")).toBeNull();
142
- });
143
- it("returns resolved name when no DATABASES set (only DATABASE_NAME allowed)", () => {
144
- delete process.env.DATABASES;
145
- process.env.DATABASE_NAME = "MyDb";
146
- expect(resolveDatabaseName()).toBe("MyDb");
147
- expect(resolveDatabaseName("MyDb")).toBe("MyDb");
148
- expect(resolveDatabaseName("AnyDb")).toBeNull();
149
- });
150
- it("trims whitespace from param", () => {
151
- process.env.DATABASES = "ProdDB";
152
- expect(resolveDatabaseName(" ProdDB ")).toBe("ProdDB");
153
- });
154
- it("returns null when neither DATABASE_NAME nor DATABASES is configured", () => {
155
- delete process.env.DATABASE_NAME;
156
- delete process.env.DATABASES;
157
- expect(resolveDatabaseName()).toBeNull();
158
- });
159
- });
160
- describe("getSqlRequest", () => {
161
- const originalEnv = process.env;
162
- beforeEach(() => {
163
- process.env = { ...originalEnv };
164
- mssqlMockState.poolConfigs.length = 0;
165
- mssqlMockState.connectCalls.mockClear();
166
- mssqlMockState.closeCalls.mockClear();
167
- mssqlMockState.requestCalls.mockClear();
168
- });
169
- afterEach(() => {
170
- process.env = originalEnv;
171
- });
172
- it("returns a helpful error when no database access is configured", async () => {
173
- delete process.env.DATABASE_NAME;
174
- delete process.env.DATABASES;
175
- const result = await getSqlRequest();
176
- expect(result.error).toBe("Invalid or disallowed database. Set DATABASE_NAME or DATABASES to configure database access. Use the databaseName parameter to target a specific configured database.");
177
- });
178
- it("returns allowed databases in the error when a disallowed database is requested", async () => {
179
- process.env.DATABASES = "ProdDB,StagingDB";
180
- const result = await getSqlRequest("OtherDb");
181
- expect(result.error).toBe("Invalid or disallowed database. Allowed: ProdDB, StagingDB. Use the databaseName parameter to target a specific configured database.");
182
- });
183
- it("returns a request using the resolved default database", async () => {
184
- process.env.DATABASES = "ProdDBDefault,StagingDB";
185
- delete process.env.DATABASE_NAME;
186
- const result = await getSqlRequest();
187
- expect(result.error).toBeUndefined();
188
- expect(result.request).toBe(mssqlMockState.requestObject);
189
- expect(mssqlMockState.poolConfigs).toContainEqual(expect.objectContaining({ database: "ProdDBDefault" }));
190
- expect(mssqlMockState.connectCalls).toHaveBeenCalledWith(expect.objectContaining({ database: "ProdDBDefault" }));
191
- expect(mssqlMockState.requestCalls).toHaveBeenCalledWith(expect.objectContaining({ database: "ProdDBDefault" }));
192
- });
193
- it("falls back to the first allowed database when DATABASE_NAME is disallowed", async () => {
194
- process.env.DATABASES = "ProdDB,StagingDB";
195
- process.env.DATABASE_NAME = "OtherDb";
196
- const result = await getSqlRequest();
197
- expect(result.error).toBeUndefined();
198
- expect(result.request).toBe(mssqlMockState.requestObject);
199
- expect(mssqlMockState.poolConfigs).toContainEqual(expect.objectContaining({ database: "ProdDB" }));
200
- expect(mssqlMockState.connectCalls).toHaveBeenCalledWith(expect.objectContaining({ database: "ProdDB" }));
201
- expect(mssqlMockState.requestCalls).toHaveBeenCalledWith(expect.objectContaining({ database: "ProdDB" }));
202
- });
203
- });
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ const mssqlMockState = vi.hoisted(() => ({
3
+ poolConfigs: [],
4
+ requestObject: { kind: "mock-request" },
5
+ connectCalls: vi.fn(),
6
+ closeCalls: vi.fn(),
7
+ requestCalls: vi.fn(),
8
+ }));
9
+ // Mock mssql to avoid loading native deps and transitive issues in test env
10
+ vi.mock("mssql", () => {
11
+ class MockConnectionPool {
12
+ constructor(config) {
13
+ this.config = config;
14
+ this.connected = false;
15
+ mssqlMockState.poolConfigs.push(config);
16
+ }
17
+ async connect() {
18
+ this.connected = true;
19
+ mssqlMockState.connectCalls(this.config);
20
+ return this;
21
+ }
22
+ async close() {
23
+ this.connected = false;
24
+ mssqlMockState.closeCalls(this.config);
25
+ }
26
+ request() {
27
+ mssqlMockState.requestCalls(this.config);
28
+ return mssqlMockState.requestObject;
29
+ }
30
+ }
31
+ return {
32
+ default: {
33
+ ConnectionPool: MockConnectionPool,
34
+ },
35
+ };
36
+ });
37
+ import { getAllowedDatabases, getSqlRequest, resolveDatabaseName } from "../db.js";
38
+ describe("getAllowedDatabases", () => {
39
+ const originalEnv = process.env;
40
+ beforeEach(() => {
41
+ process.env = { ...originalEnv };
42
+ mssqlMockState.poolConfigs.length = 0;
43
+ mssqlMockState.connectCalls.mockClear();
44
+ mssqlMockState.closeCalls.mockClear();
45
+ mssqlMockState.requestCalls.mockClear();
46
+ });
47
+ afterEach(() => {
48
+ process.env = originalEnv;
49
+ });
50
+ it("returns DATABASE_NAME when DATABASES is not set", () => {
51
+ delete process.env.DATABASES;
52
+ process.env.DATABASE_NAME = "MyDb";
53
+ expect(getAllowedDatabases()).toEqual(["MyDb"]);
54
+ });
55
+ it("returns empty array when neither DATABASES nor DATABASE_NAME is set", () => {
56
+ delete process.env.DATABASES;
57
+ delete process.env.DATABASE_NAME;
58
+ expect(getAllowedDatabases()).toEqual([]);
59
+ });
60
+ it("returns empty array when DATABASE_NAME is empty string", () => {
61
+ delete process.env.DATABASES;
62
+ process.env.DATABASE_NAME = "";
63
+ expect(getAllowedDatabases()).toEqual([]);
64
+ });
65
+ it("returns DATABASES split by comma when set", () => {
66
+ process.env.DATABASES = "ProdDB,StagingDB,AnalyticsDB";
67
+ expect(getAllowedDatabases()).toEqual([
68
+ "ProdDB",
69
+ "StagingDB",
70
+ "AnalyticsDB",
71
+ ]);
72
+ });
73
+ it("trims whitespace from DATABASES entries", () => {
74
+ process.env.DATABASES = " ProdDB , StagingDB , AnalyticsDB ";
75
+ expect(getAllowedDatabases()).toEqual([
76
+ "ProdDB",
77
+ "StagingDB",
78
+ "AnalyticsDB",
79
+ ]);
80
+ });
81
+ it("filters out empty entries from DATABASES", () => {
82
+ process.env.DATABASES = "ProdDB,,StagingDB,";
83
+ expect(getAllowedDatabases()).toEqual(["ProdDB", "StagingDB"]);
84
+ });
85
+ it("prefers DATABASES over DATABASE_NAME when both set", () => {
86
+ process.env.DATABASES = "ProdDB,StagingDB";
87
+ process.env.DATABASE_NAME = "OtherDb";
88
+ expect(getAllowedDatabases()).toEqual(["ProdDB", "StagingDB"]);
89
+ });
90
+ });
91
+ describe("resolveDatabaseName", () => {
92
+ const originalEnv = process.env;
93
+ beforeEach(() => {
94
+ process.env = { ...originalEnv };
95
+ mssqlMockState.poolConfigs.length = 0;
96
+ mssqlMockState.connectCalls.mockClear();
97
+ mssqlMockState.closeCalls.mockClear();
98
+ mssqlMockState.requestCalls.mockClear();
99
+ });
100
+ afterEach(() => {
101
+ process.env = originalEnv;
102
+ });
103
+ it("returns param when valid and in allowed list", () => {
104
+ process.env.DATABASES = "ProdDB,StagingDB";
105
+ expect(resolveDatabaseName("ProdDB")).toBe("ProdDB");
106
+ expect(resolveDatabaseName("StagingDB")).toBe("StagingDB");
107
+ });
108
+ it("returns DATABASE_NAME when param omitted and DATABASES not set", () => {
109
+ process.env.DATABASE_NAME = "DefaultDb";
110
+ delete process.env.DATABASES;
111
+ expect(resolveDatabaseName()).toBe("DefaultDb");
112
+ });
113
+ it("returns DATABASE_NAME when it is in DATABASES", () => {
114
+ process.env.DATABASES = "ProdDB,StagingDB";
115
+ process.env.DATABASE_NAME = "StagingDB";
116
+ expect(resolveDatabaseName()).toBe("StagingDB");
117
+ });
118
+ it("returns the first DATABASES entry when DATABASE_NAME is omitted", () => {
119
+ process.env.DATABASES = "ProdDB,StagingDB";
120
+ delete process.env.DATABASE_NAME;
121
+ expect(resolveDatabaseName()).toBe("ProdDB");
122
+ });
123
+ it("returns param when param provided and in allowed list", () => {
124
+ process.env.DATABASES = "ProdDB,StagingDB";
125
+ process.env.DATABASE_NAME = "ProdDB";
126
+ expect(resolveDatabaseName("StagingDB")).toBe("StagingDB");
127
+ });
128
+ it("falls back to the first DATABASES entry when DATABASE_NAME is not included in DATABASES", () => {
129
+ process.env.DATABASES = "ProdDB,StagingDB";
130
+ process.env.DATABASE_NAME = "OtherDb";
131
+ expect(resolveDatabaseName()).toBe("ProdDB");
132
+ });
133
+ it("returns null when param not in allowed list", () => {
134
+ process.env.DATABASES = "ProdDB,StagingDB";
135
+ expect(resolveDatabaseName("OtherDb")).toBeNull();
136
+ });
137
+ it("returns null when param is empty and DATABASE_NAME not set", () => {
138
+ delete process.env.DATABASE_NAME;
139
+ delete process.env.DATABASES;
140
+ expect(resolveDatabaseName()).toBeNull();
141
+ expect(resolveDatabaseName("")).toBeNull();
142
+ });
143
+ it("returns resolved name when no DATABASES set (only DATABASE_NAME allowed)", () => {
144
+ delete process.env.DATABASES;
145
+ process.env.DATABASE_NAME = "MyDb";
146
+ expect(resolveDatabaseName()).toBe("MyDb");
147
+ expect(resolveDatabaseName("MyDb")).toBe("MyDb");
148
+ expect(resolveDatabaseName("AnyDb")).toBeNull();
149
+ });
150
+ it("trims whitespace from param", () => {
151
+ process.env.DATABASES = "ProdDB";
152
+ expect(resolveDatabaseName(" ProdDB ")).toBe("ProdDB");
153
+ });
154
+ it("returns null when neither DATABASE_NAME nor DATABASES is configured", () => {
155
+ delete process.env.DATABASE_NAME;
156
+ delete process.env.DATABASES;
157
+ expect(resolveDatabaseName()).toBeNull();
158
+ });
159
+ });
160
+ describe("getSqlRequest", () => {
161
+ const originalEnv = process.env;
162
+ beforeEach(() => {
163
+ process.env = { ...originalEnv };
164
+ mssqlMockState.poolConfigs.length = 0;
165
+ mssqlMockState.connectCalls.mockClear();
166
+ mssqlMockState.closeCalls.mockClear();
167
+ mssqlMockState.requestCalls.mockClear();
168
+ });
169
+ afterEach(() => {
170
+ process.env = originalEnv;
171
+ });
172
+ it("returns a helpful error when no database access is configured", async () => {
173
+ delete process.env.DATABASE_NAME;
174
+ delete process.env.DATABASES;
175
+ const result = await getSqlRequest();
176
+ expect(result.error).toBe("Invalid or disallowed database. Set DATABASE_NAME or DATABASES to configure database access. Use the databaseName parameter to target a specific configured database.");
177
+ });
178
+ it("returns allowed databases in the error when a disallowed database is requested", async () => {
179
+ process.env.DATABASES = "ProdDB,StagingDB";
180
+ const result = await getSqlRequest("OtherDb");
181
+ expect(result.error).toBe("Invalid or disallowed database. Allowed: ProdDB, StagingDB. Use the databaseName parameter to target a specific configured database.");
182
+ });
183
+ it("returns a request using the resolved default database", async () => {
184
+ process.env.DATABASES = "ProdDBDefault,StagingDB";
185
+ delete process.env.DATABASE_NAME;
186
+ const result = await getSqlRequest();
187
+ expect(result.error).toBeUndefined();
188
+ expect(result.request).toBe(mssqlMockState.requestObject);
189
+ expect(mssqlMockState.poolConfigs).toContainEqual(expect.objectContaining({ database: "ProdDBDefault" }));
190
+ expect(mssqlMockState.connectCalls).toHaveBeenCalledWith(expect.objectContaining({ database: "ProdDBDefault" }));
191
+ expect(mssqlMockState.requestCalls).toHaveBeenCalledWith(expect.objectContaining({ database: "ProdDBDefault" }));
192
+ });
193
+ it("falls back to the first allowed database when DATABASE_NAME is disallowed", async () => {
194
+ process.env.DATABASES = "ProdDB,StagingDB";
195
+ process.env.DATABASE_NAME = "OtherDb";
196
+ const result = await getSqlRequest();
197
+ expect(result.error).toBeUndefined();
198
+ expect(result.request).toBe(mssqlMockState.requestObject);
199
+ expect(mssqlMockState.poolConfigs).toContainEqual(expect.objectContaining({ database: "ProdDB" }));
200
+ expect(mssqlMockState.connectCalls).toHaveBeenCalledWith(expect.objectContaining({ database: "ProdDB" }));
201
+ expect(mssqlMockState.requestCalls).toHaveBeenCalledWith(expect.objectContaining({ database: "ProdDB" }));
202
+ });
203
+ });
204
204
  //# sourceMappingURL=db.test.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=promptRegistry.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promptRegistry.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/promptRegistry.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { registerPrompts } from "../promptRegistry.js";
3
+ describe("promptRegistry", () => {
4
+ it("registers prompt callbacks without a fallback branch", () => {
5
+ const callbacks = new Map();
6
+ const server = {
7
+ registerPrompt(name, _meta, callback) {
8
+ callbacks.set(name, callback);
9
+ },
10
+ };
11
+ registerPrompts(server, { isReadOnly: false });
12
+ const callback = callbacks.get("review_write_operation");
13
+ expect(callback).toBeDefined();
14
+ expect(callback?.({
15
+ operation: "delete inactive users",
16
+ databaseName: "AppDb",
17
+ })).toMatchObject({
18
+ description: "Review a proposed change and identify safety checks before running it.",
19
+ });
20
+ });
21
+ it("review_write_operation tells the assistant not to bypass when read-only", () => {
22
+ const callbacks = new Map();
23
+ const server = {
24
+ registerPrompt(name, _meta, callback) {
25
+ callbacks.set(name, callback);
26
+ },
27
+ };
28
+ registerPrompts(server, { isReadOnly: true });
29
+ const callback = callbacks.get("review_write_operation");
30
+ const result = callback({
31
+ operation: "DELETE FROM dbo.Users WHERE id = 1",
32
+ databaseName: "AppDb",
33
+ });
34
+ const text = result.messages[0].content.text;
35
+ expect(text).toContain("read-only");
36
+ expect(text).toMatch(/sqlcmd|SSMS/i);
37
+ expect(text).toMatch(/STOP|Do not execute/i);
38
+ });
39
+ });
40
+ //# sourceMappingURL=promptRegistry.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promptRegistry.test.js","sourceRoot":"","sources":["../../src/__tests__/promptRegistry.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,SAAS,GAAG,IAAI,GAAG,EAAqD,CAAC;QAC/E,MAAM,MAAM,GAAG;YACb,cAAc,CACZ,IAAY,EACZ,KAAc,EACd,QAAmD;gBAEnD,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YAChC,CAAC;SACF,CAAC;QAEF,eAAe,CAAC,MAAe,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;QAExD,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;QACzD,MAAM,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;QAC/B,MAAM,CACJ,QAAQ,EAAE,CAAC;YACT,SAAS,EAAE,uBAAuB;YAClC,YAAY,EAAE,OAAO;SACtB,CAAC,CACH,CAAC,aAAa,CAAC;YACd,WAAW,EACT,wEAAwE;SAC3E,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yEAAyE,EAAE,GAAG,EAAE;QACjF,MAAM,SAAS,GAAG,IAAI,GAAG,EAAqD,CAAC;QAC/E,MAAM,MAAM,GAAG;YACb,cAAc,CACZ,IAAY,EACZ,KAAc,EACd,QAAmD;gBAEnD,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YAChC,CAAC;SACF,CAAC;QAEF,eAAe,CAAC,MAAe,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QAEvD,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,wBAAwB,CAAE,CAAC;QAC1D,MAAM,MAAM,GAAG,QAAQ,CAAC;YACtB,SAAS,EAAE,oCAAoC;YAC/C,YAAY,EAAE,OAAO;SACtB,CAAkD,CAAC;QAEpD,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QACpC,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QACrC,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=resourceRegistry.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resourceRegistry.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/resourceRegistry.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ const resourceRegistryMockState = vi.hoisted(() => ({
3
+ listDatabaseTables: vi.fn(async () => [
4
+ { schemaName: "dbo", tableName: "Users" },
5
+ ]),
6
+ listDatabaseObjects: vi.fn(async () => []),
7
+ }));
8
+ vi.mock("../schema.js", () => ({
9
+ describeDatabaseObject: vi.fn(),
10
+ describeTableSchema: vi.fn(),
11
+ listDatabaseObjects: resourceRegistryMockState.listDatabaseObjects,
12
+ listDatabaseTables: resourceRegistryMockState.listDatabaseTables,
13
+ }));
14
+ import { registerResources } from "../resourceRegistry.js";
15
+ describe("resourceRegistry", () => {
16
+ it("caches database listing reads for a short TTL", async () => {
17
+ const handlers = new Map();
18
+ const server = {
19
+ registerResource(name, _resource, _meta, handler) {
20
+ handlers.set(name, handler);
21
+ },
22
+ };
23
+ registerResources(server, {
24
+ serverName: "mssql-mcp-server",
25
+ serverVersion: "1.2.0",
26
+ isReadOnly: false,
27
+ allowedDatabases: ["AppDb"],
28
+ toolNames: [],
29
+ maxRows: 100,
30
+ queryTimeoutMs: 30000,
31
+ });
32
+ const readTables = handlers.get("AppDb_tables");
33
+ expect(readTables).toBeDefined();
34
+ await readTables?.(new URL("mssql://database/AppDb/tables"));
35
+ await readTables?.(new URL("mssql://database/AppDb/tables"));
36
+ expect(resourceRegistryMockState.listDatabaseTables).toHaveBeenCalledTimes(1);
37
+ expect(resourceRegistryMockState.listDatabaseObjects).not.toHaveBeenCalled();
38
+ });
39
+ });
40
+ //# sourceMappingURL=resourceRegistry.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resourceRegistry.test.js","sourceRoot":"","sources":["../../src/__tests__/resourceRegistry.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAElD,MAAM,yBAAyB,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAClD,kBAAkB,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC;QACpC,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE;KAC1C,CAAC;IACF,mBAAmB,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC;CAC3C,CAAC,CAAC,CAAC;AAEJ,EAAE,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,CAAC;IAC7B,sBAAsB,EAAE,EAAE,CAAC,EAAE,EAAE;IAC/B,mBAAmB,EAAE,EAAE,CAAC,EAAE,EAAE;IAC5B,mBAAmB,EAAE,yBAAyB,CAAC,mBAAmB;IAClE,kBAAkB,EAAE,yBAAyB,CAAC,kBAAkB;CACjE,CAAC,CAAC,CAAC;AAEJ,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3D,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;QAC7D,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA0C,CAAC;QACnE,MAAM,MAAM,GAAG;YACb,gBAAgB,CACd,IAAY,EACZ,SAAkB,EAClB,KAAc,EACd,OAAuC;gBAEvC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YAC9B,CAAC;SACF,CAAC;QAEF,iBAAiB,CAAC,MAAe,EAAE;YACjC,UAAU,EAAE,kBAAkB;YAC9B,aAAa,EAAE,OAAO;YACtB,UAAU,EAAE,KAAK;YACjB,gBAAgB,EAAE,CAAC,OAAO,CAAC;YAC3B,SAAS,EAAE,EAAE;YACb,OAAO,EAAE,GAAG;YACZ,cAAc,EAAE,KAAK;SACtB,CAAC,CAAC;QAEH,MAAM,UAAU,GAAG,QAAQ,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAChD,MAAM,CAAC,UAAU,CAAC,CAAC,WAAW,EAAE,CAAC;QAEjC,MAAM,UAAU,EAAE,CAAC,IAAI,GAAG,CAAC,+BAA+B,CAAC,CAAC,CAAC;QAC7D,MAAM,UAAU,EAAE,CAAC,IAAI,GAAG,CAAC,+BAA+B,CAAC,CAAC,CAAC;QAE7D,MAAM,CAAC,yBAAyB,CAAC,kBAAkB,CAAC,CAAC,qBAAqB,CACxE,CAAC,CACF,CAAC;QACF,MAAM,CACJ,yBAAyB,CAAC,mBAAmB,CAC9C,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC3B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=schema.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/schema.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,130 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ const schemaMockState = vi.hoisted(() => ({
3
+ getSqlRequestCalls: 0,
4
+ inputCalls: [],
5
+ queryCalls: [],
6
+ queryResults: [],
7
+ }));
8
+ vi.mock("mssql", () => ({
9
+ default: {
10
+ NVarChar: "NVarChar",
11
+ Int: "Int",
12
+ },
13
+ }));
14
+ vi.mock("../db.js", () => ({
15
+ getSqlRequest: vi.fn(async () => {
16
+ schemaMockState.getSqlRequestCalls += 1;
17
+ return {
18
+ request: {
19
+ input(name, type, value) {
20
+ schemaMockState.inputCalls.push({ name, type, value });
21
+ },
22
+ async query(query) {
23
+ schemaMockState.queryCalls.push(query);
24
+ return (schemaMockState.queryResults.shift() ?? {
25
+ recordset: [],
26
+ recordsets: [],
27
+ });
28
+ },
29
+ },
30
+ };
31
+ }),
32
+ }));
33
+ import { describeDatabaseObject } from "../schema.js";
34
+ describe("describeDatabaseObject", () => {
35
+ beforeEach(() => {
36
+ schemaMockState.getSqlRequestCalls = 0;
37
+ schemaMockState.inputCalls.length = 0;
38
+ schemaMockState.queryCalls.length = 0;
39
+ schemaMockState.queryResults.length = 0;
40
+ });
41
+ it("throws an ambiguity error when multiple schema matches exist", async () => {
42
+ schemaMockState.queryResults.push({
43
+ recordset: [
44
+ {
45
+ objectId: 1,
46
+ name: "Users",
47
+ schemaName: "dbo",
48
+ typeCode: "U",
49
+ typeDescription: "USER_TABLE",
50
+ createdAt: new Date(),
51
+ modifiedAt: new Date(),
52
+ definition: null,
53
+ },
54
+ {
55
+ objectId: 2,
56
+ name: "Users",
57
+ schemaName: "audit",
58
+ typeCode: "V",
59
+ typeDescription: "VIEW",
60
+ createdAt: new Date(),
61
+ modifiedAt: new Date(),
62
+ definition: null,
63
+ },
64
+ ],
65
+ });
66
+ await expect(describeDatabaseObject("Users", "AppDb")).rejects.toThrow("Specify schemaName or objectTypes to disambiguate");
67
+ expect(schemaMockState.getSqlRequestCalls).toBe(1);
68
+ });
69
+ it("reuses a single request flow and batches detail queries", async () => {
70
+ schemaMockState.queryResults.push({
71
+ recordset: [
72
+ {
73
+ objectId: 7,
74
+ name: "Users",
75
+ schemaName: "dbo",
76
+ typeCode: "U",
77
+ typeDescription: "USER_TABLE",
78
+ createdAt: new Date("2024-01-01"),
79
+ modifiedAt: new Date("2024-01-02"),
80
+ definition: null,
81
+ },
82
+ ],
83
+ }, {
84
+ recordsets: [
85
+ [
86
+ {
87
+ name: "id",
88
+ dataType: "int",
89
+ maxLength: 4,
90
+ precision: 10,
91
+ scale: 0,
92
+ isNullable: false,
93
+ isIdentity: true,
94
+ defaultValue: null,
95
+ },
96
+ ],
97
+ [
98
+ {
99
+ name: "PK_Users",
100
+ type: "CLUSTERED",
101
+ isUnique: true,
102
+ columnName: "id",
103
+ keyOrdinal: 1,
104
+ },
105
+ ],
106
+ ],
107
+ });
108
+ const result = await describeDatabaseObject("Users", "AppDb", "dbo");
109
+ expect(result).toMatchObject({
110
+ name: "Users",
111
+ schemaName: "dbo",
112
+ objectType: "table",
113
+ columns: [
114
+ {
115
+ name: "id",
116
+ dataType: "int",
117
+ },
118
+ ],
119
+ indexes: [
120
+ {
121
+ name: "PK_Users",
122
+ columns: ["id"],
123
+ },
124
+ ],
125
+ });
126
+ expect(schemaMockState.getSqlRequestCalls).toBe(1);
127
+ expect(schemaMockState.queryCalls).toHaveLength(2);
128
+ });
129
+ });
130
+ //# sourceMappingURL=schema.test.js.map