mongodb-mcp-server 0.1.2 → 0.1.3
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/.github/pull_request_template.md +5 -0
- package/.github/workflows/code_health.yaml +3 -3
- package/.github/workflows/docker.yaml +1 -1
- package/.smithery/smithery.yaml +10 -0
- package/README.md +15 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/errors.js +1 -0
- package/dist/errors.js.map +1 -1
- package/dist/helpers/indexCheck.js +63 -0
- package/dist/helpers/indexCheck.js.map +1 -0
- package/dist/logger.js +0 -1
- package/dist/logger.js.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/telemetry/telemetry.js +78 -115
- package/dist/telemetry/telemetry.js.map +1 -1
- package/dist/tools/mongodb/delete/deleteMany.js +18 -0
- package/dist/tools/mongodb/delete/deleteMany.js.map +1 -1
- package/dist/tools/mongodb/metadata/explain.js +1 -1
- package/dist/tools/mongodb/metadata/explain.js.map +1 -1
- package/dist/tools/mongodb/mongodbTool.js +10 -0
- package/dist/tools/mongodb/mongodbTool.js.map +1 -1
- package/dist/tools/mongodb/read/aggregate.js +9 -0
- package/dist/tools/mongodb/read/aggregate.js.map +1 -1
- package/dist/tools/mongodb/read/count.js +13 -0
- package/dist/tools/mongodb/read/count.js.map +1 -1
- package/dist/tools/mongodb/read/find.js +7 -0
- package/dist/tools/mongodb/read/find.js.map +1 -1
- package/dist/tools/mongodb/update/updateMany.js +20 -0
- package/dist/tools/mongodb/update/updateMany.js.map +1 -1
- package/dist/tools/tool.js +4 -4
- package/dist/tools/tool.js.map +1 -1
- package/package.json +2 -2
- package/src/config.ts +2 -0
- package/src/errors.ts +1 -0
- package/src/helpers/indexCheck.ts +83 -0
- package/src/logger.ts +0 -1
- package/src/server.ts +1 -1
- package/src/telemetry/telemetry.ts +98 -150
- package/src/telemetry/types.ts +0 -1
- package/src/tools/mongodb/delete/deleteMany.ts +20 -0
- package/src/tools/mongodb/metadata/explain.ts +1 -1
- package/src/tools/mongodb/mongodbTool.ts +10 -0
- package/src/tools/mongodb/read/aggregate.ts +11 -0
- package/src/tools/mongodb/read/count.ts +15 -0
- package/src/tools/mongodb/read/find.ts +9 -0
- package/src/tools/mongodb/update/updateMany.ts +22 -0
- package/src/tools/tool.ts +5 -5
- package/tests/integration/indexCheck.test.ts +463 -0
- package/tests/integration/server.test.ts +5 -4
- package/tests/integration/telemetry.test.ts +28 -0
- package/tests/integration/tools/mongodb/mongodbHelpers.ts +1 -0
- package/tests/unit/indexCheck.test.ts +149 -0
- package/tests/unit/telemetry.test.ts +58 -106
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import { defaultTestConfig, getResponseContent } from "./helpers.js";
|
|
2
|
+
import { describeWithMongoDB } from "./tools/mongodb/mongodbHelpers.js";
|
|
3
|
+
|
|
4
|
+
describe("IndexCheck integration tests", () => {
|
|
5
|
+
describe("with indexCheck enabled", () => {
|
|
6
|
+
describeWithMongoDB(
|
|
7
|
+
"indexCheck functionality",
|
|
8
|
+
(integration) => {
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await integration.connectMcpClient();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("find operations", () => {
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
// Insert test data for find operations
|
|
16
|
+
await integration
|
|
17
|
+
.mongoClient()
|
|
18
|
+
.db(integration.randomDbName())
|
|
19
|
+
.collection("find-test-collection")
|
|
20
|
+
.insertMany([
|
|
21
|
+
{ name: "document1", value: 1, category: "A" },
|
|
22
|
+
{ name: "document2", value: 2, category: "B" },
|
|
23
|
+
{ name: "document3", value: 3, category: "A" },
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should reject queries that perform collection scans", async () => {
|
|
28
|
+
const response = await integration.mcpClient().callTool({
|
|
29
|
+
name: "find",
|
|
30
|
+
arguments: {
|
|
31
|
+
database: integration.randomDbName(),
|
|
32
|
+
collection: "find-test-collection",
|
|
33
|
+
filter: { category: "A" }, // No index on category field
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const content = getResponseContent(response.content);
|
|
38
|
+
expect(content).toContain("Index check failed");
|
|
39
|
+
expect(content).toContain("collection scan (COLLSCAN)");
|
|
40
|
+
expect(content).toContain("MDB_MCP_INDEX_CHECK");
|
|
41
|
+
expect(response.isError).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should allow queries that use indexes", async () => {
|
|
45
|
+
// Create an index on the category field
|
|
46
|
+
await integration
|
|
47
|
+
.mongoClient()
|
|
48
|
+
.db(integration.randomDbName())
|
|
49
|
+
.collection("find-test-collection")
|
|
50
|
+
.createIndex({ category: 1 });
|
|
51
|
+
|
|
52
|
+
const response = await integration.mcpClient().callTool({
|
|
53
|
+
name: "find",
|
|
54
|
+
arguments: {
|
|
55
|
+
database: integration.randomDbName(),
|
|
56
|
+
collection: "find-test-collection",
|
|
57
|
+
filter: { category: "A" }, // Now has index
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(response.isError).toBeFalsy();
|
|
62
|
+
const content = getResponseContent(response.content);
|
|
63
|
+
expect(content).toContain("Found");
|
|
64
|
+
expect(content).toContain("documents");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should allow queries using _id (IDHACK)", async () => {
|
|
68
|
+
const docs = await integration
|
|
69
|
+
.mongoClient()
|
|
70
|
+
.db(integration.randomDbName())
|
|
71
|
+
.collection("find-test-collection")
|
|
72
|
+
.find({})
|
|
73
|
+
.toArray();
|
|
74
|
+
|
|
75
|
+
expect(docs.length).toBeGreaterThan(0);
|
|
76
|
+
|
|
77
|
+
const response = await integration.mcpClient().callTool({
|
|
78
|
+
name: "find",
|
|
79
|
+
arguments: {
|
|
80
|
+
database: integration.randomDbName(),
|
|
81
|
+
collection: "find-test-collection",
|
|
82
|
+
filter: { _id: docs[0]?._id }, // Uses _id index (IDHACK)
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(response.isError).toBeFalsy();
|
|
87
|
+
const content = getResponseContent(response.content);
|
|
88
|
+
expect(content).toContain("Found 1 documents");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("count operations", () => {
|
|
93
|
+
beforeEach(async () => {
|
|
94
|
+
// Insert test data for count operations
|
|
95
|
+
await integration
|
|
96
|
+
.mongoClient()
|
|
97
|
+
.db(integration.randomDbName())
|
|
98
|
+
.collection("count-test-collection")
|
|
99
|
+
.insertMany([
|
|
100
|
+
{ name: "document1", value: 1, category: "A" },
|
|
101
|
+
{ name: "document2", value: 2, category: "B" },
|
|
102
|
+
{ name: "document3", value: 3, category: "A" },
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should reject count queries that perform collection scans", async () => {
|
|
107
|
+
const response = await integration.mcpClient().callTool({
|
|
108
|
+
name: "count",
|
|
109
|
+
arguments: {
|
|
110
|
+
database: integration.randomDbName(),
|
|
111
|
+
collection: "count-test-collection",
|
|
112
|
+
query: { value: { $gt: 1 } }, // No index on value field
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const content = getResponseContent(response.content);
|
|
117
|
+
expect(content).toContain("Index check failed");
|
|
118
|
+
expect(content).toContain("count operation");
|
|
119
|
+
expect(response.isError).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should allow count queries with indexes", async () => {
|
|
123
|
+
// Create an index on the value field
|
|
124
|
+
await integration
|
|
125
|
+
.mongoClient()
|
|
126
|
+
.db(integration.randomDbName())
|
|
127
|
+
.collection("count-test-collection")
|
|
128
|
+
.createIndex({ value: 1 });
|
|
129
|
+
|
|
130
|
+
const response = await integration.mcpClient().callTool({
|
|
131
|
+
name: "count",
|
|
132
|
+
arguments: {
|
|
133
|
+
database: integration.randomDbName(),
|
|
134
|
+
collection: "count-test-collection",
|
|
135
|
+
query: { value: { $gt: 1 } }, // Now has index
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(response.isError).toBeFalsy();
|
|
140
|
+
const content = getResponseContent(response.content);
|
|
141
|
+
expect(content).toContain("Found");
|
|
142
|
+
expect(content).toMatch(/\d+ documents/);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("aggregate operations", () => {
|
|
147
|
+
beforeEach(async () => {
|
|
148
|
+
// Insert test data for aggregate operations
|
|
149
|
+
await integration
|
|
150
|
+
.mongoClient()
|
|
151
|
+
.db(integration.randomDbName())
|
|
152
|
+
.collection("aggregate-test-collection")
|
|
153
|
+
.insertMany([
|
|
154
|
+
{ name: "document1", value: 1, category: "A" },
|
|
155
|
+
{ name: "document2", value: 2, category: "B" },
|
|
156
|
+
{ name: "document3", value: 3, category: "A" },
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("should reject aggregation queries that perform collection scans", async () => {
|
|
161
|
+
const response = await integration.mcpClient().callTool({
|
|
162
|
+
name: "aggregate",
|
|
163
|
+
arguments: {
|
|
164
|
+
database: integration.randomDbName(),
|
|
165
|
+
collection: "aggregate-test-collection",
|
|
166
|
+
pipeline: [
|
|
167
|
+
{ $match: { category: "A" } }, // No index on category
|
|
168
|
+
{ $group: { _id: "$category", count: { $sum: 1 } } },
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const content = getResponseContent(response.content);
|
|
174
|
+
expect(content).toContain("Index check failed");
|
|
175
|
+
expect(content).toContain("aggregate operation");
|
|
176
|
+
expect(response.isError).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should allow aggregation queries with indexes", async () => {
|
|
180
|
+
// Create an index on the category field
|
|
181
|
+
await integration
|
|
182
|
+
.mongoClient()
|
|
183
|
+
.db(integration.randomDbName())
|
|
184
|
+
.collection("aggregate-test-collection")
|
|
185
|
+
.createIndex({ category: 1 });
|
|
186
|
+
|
|
187
|
+
const response = await integration.mcpClient().callTool({
|
|
188
|
+
name: "aggregate",
|
|
189
|
+
arguments: {
|
|
190
|
+
database: integration.randomDbName(),
|
|
191
|
+
collection: "aggregate-test-collection",
|
|
192
|
+
pipeline: [
|
|
193
|
+
{ $match: { category: "A" } }, // Now has index
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(response.isError).toBeFalsy();
|
|
199
|
+
const content = getResponseContent(response.content);
|
|
200
|
+
expect(content).toContain("Found");
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("updateMany operations", () => {
|
|
205
|
+
beforeEach(async () => {
|
|
206
|
+
// Insert test data for updateMany operations
|
|
207
|
+
await integration
|
|
208
|
+
.mongoClient()
|
|
209
|
+
.db(integration.randomDbName())
|
|
210
|
+
.collection("update-test-collection")
|
|
211
|
+
.insertMany([
|
|
212
|
+
{ name: "document1", value: 1, category: "A" },
|
|
213
|
+
{ name: "document2", value: 2, category: "B" },
|
|
214
|
+
{ name: "document3", value: 3, category: "A" },
|
|
215
|
+
]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should reject updateMany queries that perform collection scans", async () => {
|
|
219
|
+
const response = await integration.mcpClient().callTool({
|
|
220
|
+
name: "update-many",
|
|
221
|
+
arguments: {
|
|
222
|
+
database: integration.randomDbName(),
|
|
223
|
+
collection: "update-test-collection",
|
|
224
|
+
filter: { category: "A" }, // No index on category
|
|
225
|
+
update: { $set: { updated: true } },
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const content = getResponseContent(response.content);
|
|
230
|
+
expect(content).toContain("Index check failed");
|
|
231
|
+
expect(content).toContain("updateMany operation");
|
|
232
|
+
expect(response.isError).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should allow updateMany queries with indexes", async () => {
|
|
236
|
+
// Create an index on the category field
|
|
237
|
+
await integration
|
|
238
|
+
.mongoClient()
|
|
239
|
+
.db(integration.randomDbName())
|
|
240
|
+
.collection("update-test-collection")
|
|
241
|
+
.createIndex({ category: 1 });
|
|
242
|
+
|
|
243
|
+
const response = await integration.mcpClient().callTool({
|
|
244
|
+
name: "update-many",
|
|
245
|
+
arguments: {
|
|
246
|
+
database: integration.randomDbName(),
|
|
247
|
+
collection: "update-test-collection",
|
|
248
|
+
filter: { category: "A" }, // Now has index
|
|
249
|
+
update: { $set: { updated: true } },
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(response.isError).toBeFalsy();
|
|
254
|
+
const content = getResponseContent(response.content);
|
|
255
|
+
expect(content).toContain("Matched");
|
|
256
|
+
expect(content).toContain("Modified");
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("deleteMany operations", () => {
|
|
261
|
+
beforeEach(async () => {
|
|
262
|
+
// Insert test data for deleteMany operations
|
|
263
|
+
await integration
|
|
264
|
+
.mongoClient()
|
|
265
|
+
.db(integration.randomDbName())
|
|
266
|
+
.collection("delete-test-collection")
|
|
267
|
+
.insertMany([
|
|
268
|
+
{ name: "document1", value: 1, category: "A" },
|
|
269
|
+
{ name: "document2", value: 2, category: "B" },
|
|
270
|
+
{ name: "document3", value: 3, category: "A" },
|
|
271
|
+
]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should reject deleteMany queries that perform collection scans", async () => {
|
|
275
|
+
const response = await integration.mcpClient().callTool({
|
|
276
|
+
name: "delete-many",
|
|
277
|
+
arguments: {
|
|
278
|
+
database: integration.randomDbName(),
|
|
279
|
+
collection: "delete-test-collection",
|
|
280
|
+
filter: { value: { $lt: 2 } }, // No index on value
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const content = getResponseContent(response.content);
|
|
285
|
+
expect(content).toContain("Index check failed");
|
|
286
|
+
expect(content).toContain("deleteMany operation");
|
|
287
|
+
expect(response.isError).toBe(true);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("should allow deleteMany queries with indexes", async () => {
|
|
291
|
+
// Create an index on the value field
|
|
292
|
+
await integration
|
|
293
|
+
.mongoClient()
|
|
294
|
+
.db(integration.randomDbName())
|
|
295
|
+
.collection("delete-test-collection")
|
|
296
|
+
.createIndex({ value: 1 });
|
|
297
|
+
|
|
298
|
+
const response = await integration.mcpClient().callTool({
|
|
299
|
+
name: "delete-many",
|
|
300
|
+
arguments: {
|
|
301
|
+
database: integration.randomDbName(),
|
|
302
|
+
collection: "delete-test-collection",
|
|
303
|
+
filter: { value: { $lt: 2 } }, // Now has index
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
expect(response.isError).toBeFalsy();
|
|
308
|
+
const content = getResponseContent(response.content);
|
|
309
|
+
expect(content).toContain("Deleted");
|
|
310
|
+
expect(content).toMatch(/`\d+` document\(s\)/);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
() => ({
|
|
315
|
+
...defaultTestConfig,
|
|
316
|
+
indexCheck: true, // Enable indexCheck
|
|
317
|
+
})
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe("with indexCheck disabled", () => {
|
|
322
|
+
describeWithMongoDB(
|
|
323
|
+
"indexCheck disabled functionality",
|
|
324
|
+
(integration) => {
|
|
325
|
+
beforeEach(async () => {
|
|
326
|
+
await integration.connectMcpClient();
|
|
327
|
+
|
|
328
|
+
// insert test data for disabled indexCheck tests
|
|
329
|
+
await integration
|
|
330
|
+
.mongoClient()
|
|
331
|
+
.db(integration.randomDbName())
|
|
332
|
+
.collection("disabled-test-collection")
|
|
333
|
+
.insertMany([
|
|
334
|
+
{ name: "document1", value: 1, category: "A" },
|
|
335
|
+
{ name: "document2", value: 2, category: "B" },
|
|
336
|
+
{ name: "document3", value: 3, category: "A" },
|
|
337
|
+
]);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("should allow all queries regardless of index usage", async () => {
|
|
341
|
+
// Test find operation without index
|
|
342
|
+
const findResponse = await integration.mcpClient().callTool({
|
|
343
|
+
name: "find",
|
|
344
|
+
arguments: {
|
|
345
|
+
database: integration.randomDbName(),
|
|
346
|
+
collection: "disabled-test-collection",
|
|
347
|
+
filter: { category: "A" }, // No index, but should be allowed
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
expect(findResponse.isError).toBeFalsy();
|
|
352
|
+
const findContent = getResponseContent(findResponse.content);
|
|
353
|
+
expect(findContent).toContain("Found");
|
|
354
|
+
expect(findContent).not.toContain("Index check failed");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("should allow count operations without indexes", async () => {
|
|
358
|
+
const response = await integration.mcpClient().callTool({
|
|
359
|
+
name: "count",
|
|
360
|
+
arguments: {
|
|
361
|
+
database: integration.randomDbName(),
|
|
362
|
+
collection: "disabled-test-collection",
|
|
363
|
+
query: { value: { $gt: 1 } }, // No index, but should be allowed
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
expect(response.isError).toBeFalsy();
|
|
368
|
+
const content = getResponseContent(response.content);
|
|
369
|
+
expect(content).toContain("Found");
|
|
370
|
+
expect(content).not.toContain("Index check failed");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("should allow aggregate operations without indexes", async () => {
|
|
374
|
+
const response = await integration.mcpClient().callTool({
|
|
375
|
+
name: "aggregate",
|
|
376
|
+
arguments: {
|
|
377
|
+
database: integration.randomDbName(),
|
|
378
|
+
collection: "disabled-test-collection",
|
|
379
|
+
pipeline: [
|
|
380
|
+
{ $match: { category: "A" } }, // No index, but should be allowed
|
|
381
|
+
{ $group: { _id: "$category", count: { $sum: 1 } } },
|
|
382
|
+
],
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
expect(response.isError).toBeFalsy();
|
|
387
|
+
const content = getResponseContent(response.content);
|
|
388
|
+
expect(content).toContain("Found");
|
|
389
|
+
expect(content).not.toContain("Index check failed");
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("should allow updateMany operations without indexes", async () => {
|
|
393
|
+
const response = await integration.mcpClient().callTool({
|
|
394
|
+
name: "update-many",
|
|
395
|
+
arguments: {
|
|
396
|
+
database: integration.randomDbName(),
|
|
397
|
+
collection: "disabled-test-collection",
|
|
398
|
+
filter: { category: "A" }, // No index, but should be allowed
|
|
399
|
+
update: { $set: { updated: true } },
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
expect(response.isError).toBeFalsy();
|
|
404
|
+
const content = getResponseContent(response.content);
|
|
405
|
+
expect(content).toContain("Matched");
|
|
406
|
+
expect(content).not.toContain("Index check failed");
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("should allow deleteMany operations without indexes", async () => {
|
|
410
|
+
const response = await integration.mcpClient().callTool({
|
|
411
|
+
name: "delete-many",
|
|
412
|
+
arguments: {
|
|
413
|
+
database: integration.randomDbName(),
|
|
414
|
+
collection: "disabled-test-collection",
|
|
415
|
+
filter: { value: { $lt: 2 } }, // No index, but should be allowed
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
expect(response.isError).toBeFalsy();
|
|
420
|
+
const content = getResponseContent(response.content);
|
|
421
|
+
expect(content).toContain("Deleted");
|
|
422
|
+
expect(content).not.toContain("Index check failed");
|
|
423
|
+
});
|
|
424
|
+
},
|
|
425
|
+
() => ({
|
|
426
|
+
...defaultTestConfig,
|
|
427
|
+
indexCheck: false, // Disable indexCheck
|
|
428
|
+
})
|
|
429
|
+
);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
describe("indexCheck configuration validation", () => {
|
|
433
|
+
describeWithMongoDB(
|
|
434
|
+
"default indexCheck behavior",
|
|
435
|
+
(integration) => {
|
|
436
|
+
it("should allow collection scans by default when indexCheck is not specified", async () => {
|
|
437
|
+
await integration.connectMcpClient();
|
|
438
|
+
|
|
439
|
+
await integration
|
|
440
|
+
.mongoClient()
|
|
441
|
+
.db(integration.randomDbName())
|
|
442
|
+
.collection("default-test-collection")
|
|
443
|
+
.insertOne({ name: "test", value: 1 });
|
|
444
|
+
|
|
445
|
+
const response = await integration.mcpClient().callTool({
|
|
446
|
+
name: "find",
|
|
447
|
+
arguments: {
|
|
448
|
+
database: integration.randomDbName(),
|
|
449
|
+
collection: "default-test-collection",
|
|
450
|
+
filter: { name: "test" }, // No index, should be allowed by default
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
expect(response.isError).toBeFalsy();
|
|
455
|
+
});
|
|
456
|
+
},
|
|
457
|
+
() => ({
|
|
458
|
+
...defaultTestConfig,
|
|
459
|
+
// indexCheck not specified, should default to false
|
|
460
|
+
})
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
@@ -47,11 +47,12 @@ describe("Server integration test", () => {
|
|
|
47
47
|
it("should return capabilities", () => {
|
|
48
48
|
const capabilities = integration.mcpClient().getServerCapabilities();
|
|
49
49
|
expectDefined(capabilities);
|
|
50
|
-
expect(capabilities.completions).toBeUndefined();
|
|
51
|
-
expect(capabilities.experimental).toBeUndefined();
|
|
52
|
-
expectDefined(capabilities?.tools);
|
|
53
50
|
expectDefined(capabilities?.logging);
|
|
54
|
-
|
|
51
|
+
expectDefined(capabilities?.completions);
|
|
52
|
+
expectDefined(capabilities?.tools);
|
|
53
|
+
expectDefined(capabilities?.resources);
|
|
54
|
+
expect(capabilities.experimental).toBeUndefined();
|
|
55
|
+
expect(capabilities.prompts).toBeUndefined();
|
|
55
56
|
});
|
|
56
57
|
});
|
|
57
58
|
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createHmac } from "crypto";
|
|
2
|
+
import { Telemetry } from "../../src/telemetry/telemetry.js";
|
|
3
|
+
import { Session } from "../../src/session.js";
|
|
4
|
+
import { config } from "../../src/config.js";
|
|
5
|
+
import nodeMachineId from "node-machine-id";
|
|
6
|
+
|
|
7
|
+
describe("Telemetry", () => {
|
|
8
|
+
it("should resolve the actual machine ID", async () => {
|
|
9
|
+
const actualId: string = await nodeMachineId.machineId(true);
|
|
10
|
+
|
|
11
|
+
const actualHashedId = createHmac("sha256", actualId.toUpperCase()).update("atlascli").digest("hex");
|
|
12
|
+
|
|
13
|
+
const telemetry = Telemetry.create(
|
|
14
|
+
new Session({
|
|
15
|
+
apiBaseUrl: "",
|
|
16
|
+
}),
|
|
17
|
+
config
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
expect(telemetry.getCommonProperties().device_id).toBe(undefined);
|
|
21
|
+
expect(telemetry["isBufferingEvents"]).toBe(true);
|
|
22
|
+
|
|
23
|
+
await telemetry.deviceIdPromise;
|
|
24
|
+
|
|
25
|
+
expect(telemetry.getCommonProperties().device_id).toBe(actualHashedId);
|
|
26
|
+
expect(telemetry["isBufferingEvents"]).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { usesIndex, getIndexCheckErrorMessage } from "../../src/helpers/indexCheck.js";
|
|
2
|
+
import { Document } from "mongodb";
|
|
3
|
+
|
|
4
|
+
describe("indexCheck", () => {
|
|
5
|
+
describe("usesIndex", () => {
|
|
6
|
+
it("should return true for IXSCAN", () => {
|
|
7
|
+
const explainResult: Document = {
|
|
8
|
+
queryPlanner: {
|
|
9
|
+
winningPlan: {
|
|
10
|
+
stage: "IXSCAN",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
expect(usesIndex(explainResult)).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should return true for COUNT_SCAN", () => {
|
|
18
|
+
const explainResult: Document = {
|
|
19
|
+
queryPlanner: {
|
|
20
|
+
winningPlan: {
|
|
21
|
+
stage: "COUNT_SCAN",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
expect(usesIndex(explainResult)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should return true for IDHACK", () => {
|
|
29
|
+
const explainResult: Document = {
|
|
30
|
+
queryPlanner: {
|
|
31
|
+
winningPlan: {
|
|
32
|
+
stage: "IDHACK",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
expect(usesIndex(explainResult)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should return true for EXPRESS_IXSCAN (MongoDB 8.0+)", () => {
|
|
40
|
+
const explainResult: Document = {
|
|
41
|
+
queryPlanner: {
|
|
42
|
+
winningPlan: {
|
|
43
|
+
stage: "EXPRESS_IXSCAN",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
expect(usesIndex(explainResult)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should return true for EXPRESS_CLUSTERED_IXSCAN (MongoDB 8.0+)", () => {
|
|
51
|
+
const explainResult: Document = {
|
|
52
|
+
queryPlanner: {
|
|
53
|
+
winningPlan: {
|
|
54
|
+
stage: "EXPRESS_CLUSTERED_IXSCAN",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
expect(usesIndex(explainResult)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should return true for EXPRESS_UPDATE (MongoDB 8.0+)", () => {
|
|
62
|
+
const explainResult: Document = {
|
|
63
|
+
queryPlanner: {
|
|
64
|
+
winningPlan: {
|
|
65
|
+
stage: "EXPRESS_UPDATE",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
expect(usesIndex(explainResult)).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should return true for EXPRESS_DELETE (MongoDB 8.0+)", () => {
|
|
73
|
+
const explainResult: Document = {
|
|
74
|
+
queryPlanner: {
|
|
75
|
+
winningPlan: {
|
|
76
|
+
stage: "EXPRESS_DELETE",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
expect(usesIndex(explainResult)).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should return false for COLLSCAN", () => {
|
|
84
|
+
const explainResult: Document = {
|
|
85
|
+
queryPlanner: {
|
|
86
|
+
winningPlan: {
|
|
87
|
+
stage: "COLLSCAN",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
expect(usesIndex(explainResult)).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should return true for nested IXSCAN in inputStage", () => {
|
|
95
|
+
const explainResult: Document = {
|
|
96
|
+
queryPlanner: {
|
|
97
|
+
winningPlan: {
|
|
98
|
+
stage: "LIMIT",
|
|
99
|
+
inputStage: {
|
|
100
|
+
stage: "IXSCAN",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
expect(usesIndex(explainResult)).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should return true for nested EXPRESS_IXSCAN in inputStage", () => {
|
|
109
|
+
const explainResult: Document = {
|
|
110
|
+
queryPlanner: {
|
|
111
|
+
winningPlan: {
|
|
112
|
+
stage: "SORT",
|
|
113
|
+
inputStage: {
|
|
114
|
+
stage: "EXPRESS_IXSCAN",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
expect(usesIndex(explainResult)).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should return false for unknown stage types", () => {
|
|
123
|
+
const explainResult: Document = {
|
|
124
|
+
queryPlanner: {
|
|
125
|
+
winningPlan: {
|
|
126
|
+
stage: "UNKNOWN_STAGE",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
expect(usesIndex(explainResult)).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should handle missing queryPlanner", () => {
|
|
134
|
+
const explainResult: Document = {};
|
|
135
|
+
expect(usesIndex(explainResult)).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("getIndexCheckErrorMessage", () => {
|
|
140
|
+
it("should generate appropriate error message", () => {
|
|
141
|
+
const message = getIndexCheckErrorMessage("testdb", "testcoll", "find");
|
|
142
|
+
expect(message).toContain("Index check failed");
|
|
143
|
+
expect(message).toContain("testdb.testcoll");
|
|
144
|
+
expect(message).toContain("find operation");
|
|
145
|
+
expect(message).toContain("collection scan (COLLSCAN)");
|
|
146
|
+
expect(message).toContain("MDB_MCP_INDEX_CHECK");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|