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.
Files changed (55) hide show
  1. package/.github/pull_request_template.md +5 -0
  2. package/.github/workflows/code_health.yaml +3 -3
  3. package/.github/workflows/docker.yaml +1 -1
  4. package/.smithery/smithery.yaml +10 -0
  5. package/README.md +15 -1
  6. package/dist/config.js +1 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/errors.js +1 -0
  9. package/dist/errors.js.map +1 -1
  10. package/dist/helpers/indexCheck.js +63 -0
  11. package/dist/helpers/indexCheck.js.map +1 -0
  12. package/dist/logger.js +0 -1
  13. package/dist/logger.js.map +1 -1
  14. package/dist/server.js +1 -1
  15. package/dist/server.js.map +1 -1
  16. package/dist/telemetry/telemetry.js +78 -115
  17. package/dist/telemetry/telemetry.js.map +1 -1
  18. package/dist/tools/mongodb/delete/deleteMany.js +18 -0
  19. package/dist/tools/mongodb/delete/deleteMany.js.map +1 -1
  20. package/dist/tools/mongodb/metadata/explain.js +1 -1
  21. package/dist/tools/mongodb/metadata/explain.js.map +1 -1
  22. package/dist/tools/mongodb/mongodbTool.js +10 -0
  23. package/dist/tools/mongodb/mongodbTool.js.map +1 -1
  24. package/dist/tools/mongodb/read/aggregate.js +9 -0
  25. package/dist/tools/mongodb/read/aggregate.js.map +1 -1
  26. package/dist/tools/mongodb/read/count.js +13 -0
  27. package/dist/tools/mongodb/read/count.js.map +1 -1
  28. package/dist/tools/mongodb/read/find.js +7 -0
  29. package/dist/tools/mongodb/read/find.js.map +1 -1
  30. package/dist/tools/mongodb/update/updateMany.js +20 -0
  31. package/dist/tools/mongodb/update/updateMany.js.map +1 -1
  32. package/dist/tools/tool.js +4 -4
  33. package/dist/tools/tool.js.map +1 -1
  34. package/package.json +2 -2
  35. package/src/config.ts +2 -0
  36. package/src/errors.ts +1 -0
  37. package/src/helpers/indexCheck.ts +83 -0
  38. package/src/logger.ts +0 -1
  39. package/src/server.ts +1 -1
  40. package/src/telemetry/telemetry.ts +98 -150
  41. package/src/telemetry/types.ts +0 -1
  42. package/src/tools/mongodb/delete/deleteMany.ts +20 -0
  43. package/src/tools/mongodb/metadata/explain.ts +1 -1
  44. package/src/tools/mongodb/mongodbTool.ts +10 -0
  45. package/src/tools/mongodb/read/aggregate.ts +11 -0
  46. package/src/tools/mongodb/read/count.ts +15 -0
  47. package/src/tools/mongodb/read/find.ts +9 -0
  48. package/src/tools/mongodb/update/updateMany.ts +22 -0
  49. package/src/tools/tool.ts +5 -5
  50. package/tests/integration/indexCheck.test.ts +463 -0
  51. package/tests/integration/server.test.ts +5 -4
  52. package/tests/integration/telemetry.test.ts +28 -0
  53. package/tests/integration/tools/mongodb/mongodbHelpers.ts +1 -0
  54. package/tests/unit/indexCheck.test.ts +149 -0
  55. 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
- expect(capabilities?.prompts).toBeUndefined();
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
+ });
@@ -70,6 +70,7 @@ export function setupMongoDBIntegrationTest(): MongoDBIntegrationTest {
70
70
  tmpDir: dbsDir,
71
71
  logDir: path.join(tmpDir, "mongodb-runner", "logs"),
72
72
  topology: "standalone",
73
+ version: "8.0.10",
73
74
  });
74
75
 
75
76
  return;
@@ -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
+ });