mongodb-mcp-server 0.1.1 → 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 (104) hide show
  1. package/.dockerignore +11 -0
  2. package/.github/pull_request_template.md +5 -0
  3. package/.github/workflows/check-pr-title.yml +29 -0
  4. package/.github/workflows/code_health.yaml +3 -3
  5. package/.github/workflows/docker.yaml +57 -0
  6. package/.github/workflows/stale.yml +32 -0
  7. package/.smithery/Dockerfile +30 -0
  8. package/.smithery/smithery.yaml +73 -0
  9. package/CONTRIBUTING.md +1 -1
  10. package/Dockerfile +10 -0
  11. package/README.md +150 -15
  12. package/dist/common/atlas/apiClient.js +10 -1
  13. package/dist/common/atlas/apiClient.js.map +1 -1
  14. package/dist/common/atlas/cluster.js +1 -1
  15. package/dist/common/atlas/cluster.js.map +1 -1
  16. package/dist/config.js +1 -0
  17. package/dist/config.js.map +1 -1
  18. package/dist/errors.js +1 -0
  19. package/dist/errors.js.map +1 -1
  20. package/dist/helpers/indexCheck.js +63 -0
  21. package/dist/helpers/indexCheck.js.map +1 -0
  22. package/dist/index.js +17 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/logger.js +4 -0
  25. package/dist/logger.js.map +1 -1
  26. package/dist/tools/atlas/create/createProject.js +5 -1
  27. package/dist/tools/atlas/create/createProject.js.map +1 -1
  28. package/dist/tools/atlas/read/listAlerts.js +41 -0
  29. package/dist/tools/atlas/read/listAlerts.js.map +1 -0
  30. package/dist/tools/atlas/read/listProjects.js +3 -1
  31. package/dist/tools/atlas/read/listProjects.js.map +1 -1
  32. package/dist/tools/atlas/tools.js +2 -0
  33. package/dist/tools/atlas/tools.js.map +1 -1
  34. package/dist/tools/mongodb/delete/deleteMany.js +18 -0
  35. package/dist/tools/mongodb/delete/deleteMany.js.map +1 -1
  36. package/dist/tools/mongodb/metadata/explain.js +1 -1
  37. package/dist/tools/mongodb/metadata/explain.js.map +1 -1
  38. package/dist/tools/mongodb/metadata/listDatabases.js.map +1 -1
  39. package/dist/tools/mongodb/mongodbTool.js +10 -0
  40. package/dist/tools/mongodb/mongodbTool.js.map +1 -1
  41. package/dist/tools/mongodb/read/aggregate.js +9 -0
  42. package/dist/tools/mongodb/read/aggregate.js.map +1 -1
  43. package/dist/tools/mongodb/read/count.js +15 -2
  44. package/dist/tools/mongodb/read/count.js.map +1 -1
  45. package/dist/tools/mongodb/read/find.js +7 -0
  46. package/dist/tools/mongodb/read/find.js.map +1 -1
  47. package/dist/tools/mongodb/update/updateMany.js +20 -0
  48. package/dist/tools/mongodb/update/updateMany.js.map +1 -1
  49. package/dist/tools/tool.js +34 -2
  50. package/dist/tools/tool.js.map +1 -1
  51. package/package.json +9 -9
  52. package/scripts/apply.ts +4 -4
  53. package/scripts/filter.ts +1 -0
  54. package/src/common/atlas/apiClient.ts +11 -1
  55. package/src/common/atlas/cluster.ts +1 -2
  56. package/src/common/atlas/openapi.d.ts +1242 -28
  57. package/src/config.ts +2 -0
  58. package/src/errors.ts +1 -0
  59. package/src/helpers/indexCheck.ts +83 -0
  60. package/src/index.ts +20 -0
  61. package/src/logger.ts +5 -0
  62. package/src/tools/atlas/create/createProject.ts +7 -1
  63. package/src/tools/atlas/read/listAlerts.ts +45 -0
  64. package/src/tools/atlas/read/listProjects.ts +4 -2
  65. package/src/tools/atlas/tools.ts +2 -0
  66. package/src/tools/mongodb/delete/deleteMany.ts +20 -0
  67. package/src/tools/mongodb/metadata/explain.ts +1 -1
  68. package/src/tools/mongodb/metadata/listDatabases.ts +0 -1
  69. package/src/tools/mongodb/mongodbTool.ts +10 -0
  70. package/src/tools/mongodb/read/aggregate.ts +11 -0
  71. package/src/tools/mongodb/read/count.ts +18 -2
  72. package/src/tools/mongodb/read/find.ts +9 -0
  73. package/src/tools/mongodb/update/updateMany.ts +22 -0
  74. package/src/tools/tool.ts +40 -3
  75. package/tests/integration/helpers.ts +23 -0
  76. package/tests/integration/indexCheck.test.ts +463 -0
  77. package/tests/integration/server.test.ts +5 -4
  78. package/tests/integration/tools/atlas/accessLists.test.ts +2 -2
  79. package/tests/integration/tools/atlas/alerts.test.ts +42 -0
  80. package/tests/integration/tools/atlas/atlasHelpers.ts +5 -3
  81. package/tests/integration/tools/atlas/clusters.test.ts +4 -4
  82. package/tests/integration/tools/atlas/dbUsers.test.ts +7 -7
  83. package/tests/integration/tools/atlas/orgs.test.ts +2 -2
  84. package/tests/integration/tools/atlas/projects.test.ts +3 -3
  85. package/tests/integration/tools/mongodb/create/createCollection.test.ts +2 -2
  86. package/tests/integration/tools/mongodb/create/createIndex.test.ts +2 -2
  87. package/tests/integration/tools/mongodb/create/insertMany.test.ts +1 -1
  88. package/tests/integration/tools/mongodb/delete/dropCollection.test.ts +1 -1
  89. package/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts +2 -2
  90. package/tests/integration/tools/mongodb/metadata/dbStats.test.ts +4 -4
  91. package/tests/integration/tools/mongodb/metadata/explain.test.ts +10 -10
  92. package/tests/integration/tools/mongodb/metadata/listCollections.test.ts +1 -1
  93. package/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +9 -5
  94. package/tests/integration/tools/mongodb/metadata/logs.test.ts +4 -4
  95. package/tests/integration/tools/mongodb/mongodbHelpers.ts +1 -0
  96. package/tests/integration/tools/mongodb/read/aggregate.test.ts +22 -7
  97. package/tests/integration/tools/mongodb/read/collectionIndexes.test.ts +5 -5
  98. package/tests/integration/tools/mongodb/read/count.test.ts +15 -10
  99. package/tests/integration/tools/mongodb/read/find.test.ts +6 -6
  100. package/tests/integration/tools/mongodb/update/renameCollection.test.ts +4 -4
  101. package/tests/unit/EJsonTransport.test.ts +1 -1
  102. package/tests/unit/indexCheck.test.ts +149 -0
  103. package/tests/unit/session.test.ts +1 -1
  104. package/tsconfig.build.json +1 -0
@@ -43,7 +43,7 @@ describeWithAtlas("projects", (integration) => {
43
43
  })) as CallToolResult;
44
44
  expect(response.content).toBeArray();
45
45
  expect(response.content).toHaveLength(1);
46
- expect(response.content[0].text).toContain(projName);
46
+ expect(response.content[0]?.text).toContain(projName);
47
47
  });
48
48
  });
49
49
  describe("atlas-list-projects", () => {
@@ -62,8 +62,8 @@ describeWithAtlas("projects", (integration) => {
62
62
  .callTool({ name: "atlas-list-projects", arguments: {} })) as CallToolResult;
63
63
  expect(response.content).toBeArray();
64
64
  expect(response.content).toHaveLength(1);
65
- expect(response.content[0].text).toContain(projName);
66
- const data = parseTable(response.content[0].text as string);
65
+ expect(response.content[0]?.text).toContain(projName);
66
+ const data = parseTable(response.content[0]?.text as string);
67
67
  expect(data).toBeArray();
68
68
  expect(data.length).toBeGreaterThan(0);
69
69
  let found = false;
@@ -34,7 +34,7 @@ describeWithMongoDB("createCollection tool", (integration) => {
34
34
 
35
35
  collections = await mongoClient.db(integration.randomDbName()).listCollections().toArray();
36
36
  expect(collections).toHaveLength(1);
37
- expect(collections[0].name).toEqual("bar");
37
+ expect(collections[0]?.name).toEqual("bar");
38
38
  });
39
39
  });
40
40
 
@@ -78,7 +78,7 @@ describeWithMongoDB("createCollection tool", (integration) => {
78
78
  expect(content).toEqual(`Collection "collection1" created in database "${integration.randomDbName()}".`);
79
79
  collections = await mongoClient.db(integration.randomDbName()).listCollections().toArray();
80
80
  expect(collections).toHaveLength(1);
81
- expect(collections[0].name).toEqual("collection1");
81
+ expect(collections[0]?.name).toEqual("collection1");
82
82
 
83
83
  // Make sure we didn't drop the existing collection
84
84
  documents = await mongoClient.db(integration.randomDbName()).collection("collection1").find({}).toArray();
@@ -38,10 +38,10 @@ describeWithMongoDB("createIndex tool", (integration) => {
38
38
  const mongoClient = integration.mongoClient();
39
39
  const collections = await mongoClient.db(integration.randomDbName()).listCollections().toArray();
40
40
  expect(collections).toHaveLength(1);
41
- expect(collections[0].name).toEqual("coll1");
41
+ expect(collections[0]?.name).toEqual("coll1");
42
42
  const indexes = await mongoClient.db(integration.randomDbName()).collection(collection).indexes();
43
43
  expect(indexes).toHaveLength(expected.length + 1);
44
- expect(indexes[0].name).toEqual("_id_");
44
+ expect(indexes[0]?.name).toEqual("_id_");
45
45
  for (const index of expected) {
46
46
  const foundIndex = indexes.find((i) => i.name === index.name);
47
47
  expectDefined(foundIndex);
@@ -82,7 +82,7 @@ describeWithMongoDB("insertMany tool", (integration) => {
82
82
  const content = getResponseContent(response.content);
83
83
  expect(content).toContain("Error running insert-many");
84
84
  expect(content).toContain("duplicate key error");
85
- expect(content).toContain(insertedIds[0].toString());
85
+ expect(content).toContain(insertedIds[0]?.toString());
86
86
  });
87
87
 
88
88
  validateAutoConnectBehavior(integration, "insert-many", () => {
@@ -54,7 +54,7 @@ describeWithMongoDB("dropCollection tool", (integration) => {
54
54
  );
55
55
  const collections = await integration.mongoClient().db(integration.randomDbName()).listCollections().toArray();
56
56
  expect(collections).toHaveLength(1);
57
- expect(collections[0].name).toBe("coll2");
57
+ expect(collections[0]?.name).toBe("coll2");
58
58
  });
59
59
 
60
60
  validateAutoConnectBehavior(integration, "drop-collection", () => {
@@ -132,11 +132,11 @@ describeWithMongoDB("collectionSchema tool", (integration) => {
132
132
  expect(items).toHaveLength(2);
133
133
 
134
134
  // Expect to find _id, name, age
135
- expect(items[0].text).toEqual(
135
+ expect(items[0]?.text).toEqual(
136
136
  `Found ${Object.entries(testCase.expectedSchema).length} fields in the schema for "${integration.randomDbName()}.foo"`
137
137
  );
138
138
 
139
- const schema = JSON.parse(items[1].text) as SimplifiedSchema;
139
+ const schema = JSON.parse(items[1]?.text ?? "{}") as SimplifiedSchema;
140
140
  expect(schema).toEqual(testCase.expectedSchema);
141
141
  });
142
142
  }
@@ -28,9 +28,9 @@ describeWithMongoDB("dbStats tool", (integration) => {
28
28
  });
29
29
  const elements = getResponseElements(response.content);
30
30
  expect(elements).toHaveLength(2);
31
- expect(elements[0].text).toBe(`Statistics for database ${integration.randomDbName()}`);
31
+ expect(elements[0]?.text).toBe(`Statistics for database ${integration.randomDbName()}`);
32
32
 
33
- const stats = JSON.parse(elements[1].text) as {
33
+ const stats = JSON.parse(elements[1]?.text ?? "{}") as {
34
34
  db: string;
35
35
  collections: number;
36
36
  storageSize: number;
@@ -75,9 +75,9 @@ describeWithMongoDB("dbStats tool", (integration) => {
75
75
  });
76
76
  const elements = getResponseElements(response.content);
77
77
  expect(elements).toHaveLength(2);
78
- expect(elements[0].text).toBe(`Statistics for database ${integration.randomDbName()}`);
78
+ expect(elements[0]?.text).toBe(`Statistics for database ${integration.randomDbName()}`);
79
79
 
80
- const stats = JSON.parse(elements[1].text) as {
80
+ const stats = JSON.parse(elements[1]?.text ?? "{}") as {
81
81
  db: string;
82
82
  collections: unknown;
83
83
  storageSize: unknown;
@@ -89,12 +89,12 @@ describeWithMongoDB("explain tool", (integration) => {
89
89
 
90
90
  const content = getResponseElements(response.content);
91
91
  expect(content).toHaveLength(2);
92
- expect(content[0].text).toEqual(
92
+ expect(content[0]?.text).toEqual(
93
93
  `Here is some information about the winning plan chosen by the query optimizer for running the given \`${testCase.method}\` operation in "${integration.randomDbName()}.coll1". This information can be used to understand how the query was executed and to optimize the query performance.`
94
94
  );
95
95
 
96
- expect(content[1].text).toContain("queryPlanner");
97
- expect(content[1].text).toContain("winningPlan");
96
+ expect(content[1]?.text).toContain("queryPlanner");
97
+ expect(content[1]?.text).toContain("winningPlan");
98
98
  });
99
99
  }
100
100
  });
@@ -139,22 +139,22 @@ describeWithMongoDB("explain tool", (integration) => {
139
139
 
140
140
  const content = getResponseElements(response.content);
141
141
  expect(content).toHaveLength(2);
142
- expect(content[0].text).toEqual(
142
+ expect(content[0]?.text).toEqual(
143
143
  `Here is some information about the winning plan chosen by the query optimizer for running the given \`${testCase.method}\` operation in "${integration.randomDbName()}.people". This information can be used to understand how the query was executed and to optimize the query performance.`
144
144
  );
145
145
 
146
- expect(content[1].text).toContain("queryPlanner");
147
- expect(content[1].text).toContain("winningPlan");
146
+ expect(content[1]?.text).toContain("queryPlanner");
147
+ expect(content[1]?.text).toContain("winningPlan");
148
148
 
149
149
  if (indexed) {
150
150
  if (testCase.method === "count") {
151
- expect(content[1].text).toContain("COUNT_SCAN");
151
+ expect(content[1]?.text).toContain("COUNT_SCAN");
152
152
  } else {
153
- expect(content[1].text).toContain("IXSCAN");
153
+ expect(content[1]?.text).toContain("IXSCAN");
154
154
  }
155
- expect(content[1].text).toContain("name_1");
155
+ expect(content[1]?.text).toContain("name_1");
156
156
  } else {
157
- expect(content[1].text).toContain("COLLSCAN");
157
+ expect(content[1]?.text).toContain("COLLSCAN");
158
158
  }
159
159
  });
160
160
  }
@@ -45,7 +45,7 @@ describeWithMongoDB("listCollections tool", (integration) => {
45
45
  });
46
46
  const items = getResponseElements(response.content);
47
47
  expect(items).toHaveLength(1);
48
- expect(items[0].text).toContain('Name: "collection-1"');
48
+ expect(items[0]?.text).toContain('Name: "collection-1"');
49
49
 
50
50
  await mongoClient.db(integration.randomDbName()).createCollection("collection-2");
51
51
 
@@ -65,9 +65,13 @@ describeWithMongoDB("listDatabases tool", (integration) => {
65
65
 
66
66
  function getDbNames(content: unknown): (string | null)[] {
67
67
  const responseItems = getResponseElements(content);
68
-
69
- return responseItems.map((item) => {
70
- const match = item.text.match(/Name: (.*), Size: \d+ bytes/);
71
- return match ? match[1] : null;
72
- });
68
+ return responseItems
69
+ .map((item) => {
70
+ if (item && typeof item.text === "string") {
71
+ const match = item.text.match(/Name: ([^,]+), Size: \d+ bytes/);
72
+ return match ? match[1] : null;
73
+ }
74
+ return null;
75
+ })
76
+ .filter((item): item is string | null => item !== undefined);
73
77
  }
@@ -37,11 +37,11 @@ describeWithMongoDB("logs tool", (integration) => {
37
37
 
38
38
  // Default limit is 50
39
39
  expect(elements.length).toBeLessThanOrEqual(51);
40
- expect(elements[0].text).toMatch(/Found: \d+ messages/);
40
+ expect(elements[0]?.text).toMatch(/Found: \d+ messages/);
41
41
 
42
42
  for (let i = 1; i < elements.length; i++) {
43
43
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
44
- const log = JSON.parse(elements[i].text);
44
+ const log = JSON.parse(elements[i]?.text ?? "{}");
45
45
  expect(log).toHaveProperty("t");
46
46
  expect(log).toHaveProperty("msg");
47
47
  }
@@ -59,7 +59,7 @@ describeWithMongoDB("logs tool", (integration) => {
59
59
  const elements = getResponseElements(response);
60
60
  expect(elements.length).toBeLessThanOrEqual(51);
61
61
  for (let i = 1; i < elements.length; i++) {
62
- const log = JSON.parse(elements[i].text) as { tags: string[] };
62
+ const log = JSON.parse(elements[i]?.text ?? "{}") as { tags: string[] };
63
63
  expect(log).toHaveProperty("t");
64
64
  expect(log).toHaveProperty("msg");
65
65
  expect(log).toHaveProperty("tags");
@@ -76,7 +76,7 @@ describeWithMongoDB("logs tool", (integration) => {
76
76
  validate: (content) => {
77
77
  const elements = getResponseElements(content);
78
78
  expect(elements.length).toBeLessThanOrEqual(51);
79
- expect(elements[0].text).toMatch(/Found: \d+ messages/);
79
+ expect(elements[0]?.text).toMatch(/Found: \d+ messages/);
80
80
  },
81
81
  };
82
82
  });
@@ -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;
@@ -35,7 +35,7 @@ describeWithMongoDB("aggregate tool", (integration) => {
35
35
 
36
36
  const elements = getResponseElements(response.content);
37
37
  expect(elements).toHaveLength(1);
38
- expect(elements[0].text).toEqual('Found 0 documents in the collection "people":');
38
+ expect(elements[0]?.text).toEqual('Found 0 documents in the collection "people":');
39
39
  });
40
40
 
41
41
  it("can run aggragation on an empty collection", async () => {
@@ -53,7 +53,7 @@ describeWithMongoDB("aggregate tool", (integration) => {
53
53
 
54
54
  const elements = getResponseElements(response.content);
55
55
  expect(elements).toHaveLength(1);
56
- expect(elements[0].text).toEqual('Found 0 documents in the collection "people":');
56
+ expect(elements[0]?.text).toEqual('Found 0 documents in the collection "people":');
57
57
  });
58
58
 
59
59
  it("can run aggragation on an existing collection", async () => {
@@ -79,11 +79,21 @@ describeWithMongoDB("aggregate tool", (integration) => {
79
79
 
80
80
  const elements = getResponseElements(response.content);
81
81
  expect(elements).toHaveLength(3);
82
- expect(elements[0].text).toEqual('Found 2 documents in the collection "people":');
83
- /* eslint-disable @typescript-eslint/no-unsafe-assignment */
84
- expect(JSON.parse(elements[1].text)).toEqual({ _id: expect.any(Object), name: "Søren", age: 15 });
85
- expect(JSON.parse(elements[2].text)).toEqual({ _id: expect.any(Object), name: "Laura", age: 10 });
86
- /* eslint-enable @typescript-eslint/no-unsafe-assignment */
82
+ expect(elements[0]?.text).toEqual('Found 2 documents in the collection "people":');
83
+ expect(asObject(JSON.parse(elements[1]?.text ?? "{}"))).toEqual(
84
+ expect.objectContaining({
85
+ _id: expect.any(Object) as object,
86
+ name: "Søren",
87
+ age: 15,
88
+ })
89
+ );
90
+ expect(asObject(JSON.parse(elements[2]?.text ?? "{}"))).toEqual(
91
+ expect.objectContaining({
92
+ _id: expect.any(Object) as object,
93
+ name: "Laura",
94
+ age: 10,
95
+ })
96
+ );
87
97
  });
88
98
 
89
99
  validateAutoConnectBehavior(integration, "aggregate", () => {
@@ -97,3 +107,8 @@ describeWithMongoDB("aggregate tool", (integration) => {
97
107
  };
98
108
  });
99
109
  });
110
+
111
+ function asObject(val: unknown): Record<string, unknown> {
112
+ if (typeof val === "object" && val !== null) return val as Record<string, unknown>;
113
+ throw new Error("Expected an object");
114
+ }
@@ -28,7 +28,7 @@ describeWithMongoDB("collectionIndexes tool", (integration) => {
28
28
 
29
29
  const elements = getResponseElements(response.content);
30
30
  expect(elements).toHaveLength(1);
31
- expect(elements[0].text).toEqual(
31
+ expect(elements[0]?.text).toEqual(
32
32
  'The indexes for "non-existent.people" cannot be determined because the collection does not exist.'
33
33
  );
34
34
  });
@@ -47,8 +47,8 @@ describeWithMongoDB("collectionIndexes tool", (integration) => {
47
47
 
48
48
  const elements = getResponseElements(response.content);
49
49
  expect(elements).toHaveLength(2);
50
- expect(elements[0].text).toEqual('Found 1 indexes in the collection "people":');
51
- expect(elements[1].text).toEqual('Name "_id_", definition: {"_id":1}');
50
+ expect(elements[0]?.text).toEqual('Found 1 indexes in the collection "people":');
51
+ expect(elements[1]?.text).toEqual('Name "_id_", definition: {"_id":1}');
52
52
  });
53
53
 
54
54
  it("returns all indexes for a collection", async () => {
@@ -74,8 +74,8 @@ describeWithMongoDB("collectionIndexes tool", (integration) => {
74
74
 
75
75
  const elements = getResponseElements(response.content);
76
76
  expect(elements).toHaveLength(indexTypes.length + 2);
77
- expect(elements[0].text).toEqual(`Found ${indexTypes.length + 1} indexes in the collection "people":`);
78
- expect(elements[1].text).toEqual('Name "_id_", definition: {"_id":1}');
77
+ expect(elements[0]?.text).toEqual(`Found ${indexTypes.length + 1} indexes in the collection "people":`);
78
+ expect(elements[1]?.text).toEqual('Name "_id_", definition: {"_id":1}');
79
79
 
80
80
  for (const indexType of indexTypes) {
81
81
  const index = elements.find((element) => element.text.includes(`prop_${indexType}`));
@@ -8,16 +8,21 @@ import {
8
8
  } from "../../../helpers.js";
9
9
 
10
10
  describeWithMongoDB("count tool", (integration) => {
11
- validateToolMetadata(integration, "count", "Gets the number of documents in a MongoDB collection", [
12
- {
13
- name: "query",
14
- description:
15
- "The query filter to count documents. Matches the syntax of the filter argument of db.collection.count()",
16
- type: "object",
17
- required: false,
18
- },
19
- ...databaseCollectionParameters,
20
- ]);
11
+ validateToolMetadata(
12
+ integration,
13
+ "count",
14
+ "Gets the number of documents in a MongoDB collection using db.collection.count() and query as an optional filter parameter",
15
+ [
16
+ {
17
+ name: "query",
18
+ description:
19
+ "A filter/query parameter. Allows users to filter the documents to count. Matches the syntax of the filter argument of db.collection.count().",
20
+ type: "object",
21
+ required: false,
22
+ },
23
+ ...databaseCollectionParameters,
24
+ ]
25
+ );
21
26
 
22
27
  validateThrowsForInvalidArguments(integration, "count", [
23
28
  {},
@@ -149,10 +149,10 @@ describeWithMongoDB("find tool", (integration) => {
149
149
  });
150
150
  const elements = getResponseElements(response.content);
151
151
  expect(elements).toHaveLength(expected.length + 1);
152
- expect(elements[0].text).toEqual(`Found ${expected.length} documents in the collection "foo":`);
152
+ expect(elements[0]?.text).toEqual(`Found ${expected.length} documents in the collection "foo":`);
153
153
 
154
154
  for (let i = 0; i < expected.length; i++) {
155
- expect(JSON.parse(elements[i + 1].text)).toEqual(expected[i]);
155
+ expect(JSON.parse(elements[i + 1]?.text ?? "{}")).toEqual(expected[i]);
156
156
  }
157
157
  });
158
158
  }
@@ -165,11 +165,11 @@ describeWithMongoDB("find tool", (integration) => {
165
165
  });
166
166
  const elements = getResponseElements(response.content);
167
167
  expect(elements).toHaveLength(11);
168
- expect(elements[0].text).toEqual('Found 10 documents in the collection "foo":');
168
+ expect(elements[0]?.text).toEqual('Found 10 documents in the collection "foo":');
169
169
 
170
170
  for (let i = 0; i < 10; i++) {
171
171
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
172
- expect(JSON.parse(elements[i + 1].text).value).toEqual(i);
172
+ expect(JSON.parse(elements[i + 1]?.text ?? "{}").value).toEqual(i);
173
173
  }
174
174
  });
175
175
 
@@ -194,10 +194,10 @@ describeWithMongoDB("find tool", (integration) => {
194
194
 
195
195
  const elements = getResponseElements(response.content);
196
196
  expect(elements).toHaveLength(2);
197
- expect(elements[0].text).toEqual('Found 1 documents in the collection "foo":');
197
+ expect(elements[0]?.text).toEqual('Found 1 documents in the collection "foo":');
198
198
 
199
199
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
200
- expect(JSON.parse(elements[1].text).value).toEqual(fooObject.value);
200
+ expect(JSON.parse(elements[1]?.text ?? "{}").value).toEqual(fooObject.value);
201
201
  });
202
202
  });
203
203
 
@@ -94,7 +94,7 @@ describeWithMongoDB("renameCollection tool", (integration) => {
94
94
  .find({})
95
95
  .toArray();
96
96
  expect(docsInAfter).toHaveLength(1);
97
- expect(docsInAfter[0].value).toEqual(42);
97
+ expect(docsInAfter[0]?.value).toEqual(42);
98
98
  });
99
99
 
100
100
  it("returns an error when renaming to an existing collection", async () => {
@@ -123,7 +123,7 @@ describeWithMongoDB("renameCollection tool", (integration) => {
123
123
  .find({})
124
124
  .toArray();
125
125
  expect(docsInBefore).toHaveLength(1);
126
- expect(docsInBefore[0].value).toEqual(42);
126
+ expect(docsInBefore[0]?.value).toEqual(42);
127
127
 
128
128
  const docsInAfter = await integration
129
129
  .mongoClient()
@@ -132,7 +132,7 @@ describeWithMongoDB("renameCollection tool", (integration) => {
132
132
  .find({})
133
133
  .toArray();
134
134
  expect(docsInAfter).toHaveLength(1);
135
- expect(docsInAfter[0].value).toEqual(84);
135
+ expect(docsInAfter[0]?.value).toEqual(84);
136
136
  });
137
137
 
138
138
  it("renames to existing collection with dropTarget", async () => {
@@ -174,7 +174,7 @@ describeWithMongoDB("renameCollection tool", (integration) => {
174
174
  .find({})
175
175
  .toArray();
176
176
  expect(docsInAfter).toHaveLength(1);
177
- expect(docsInAfter[0].value).toEqual(42);
177
+ expect(docsInAfter[0]?.value).toEqual(42);
178
178
  });
179
179
  });
180
180
 
@@ -37,7 +37,7 @@ describe("EJsonTransport", () => {
37
37
  );
38
38
 
39
39
  expect(messages.length).toBe(1);
40
- const message = messages[0].message;
40
+ const message = messages[0]?.message;
41
41
 
42
42
  expect(message).toEqual({
43
43
  jsonrpc: "2.0",
@@ -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
+ });
@@ -53,7 +53,7 @@ describe("Session", () => {
53
53
  typeof NodeDriverServiceProvider.connect
54
54
  >;
55
55
  expect(connectMock).toHaveBeenCalledOnce();
56
- const connectionString = connectMock.mock.calls[0][0];
56
+ const connectionString = connectMock.mock.calls[0]?.[0];
57
57
  if (testCase.expectAppName) {
58
58
  expect(connectionString).toContain("appName=MongoDB+MCP+Server");
59
59
  } else {
@@ -7,6 +7,7 @@
7
7
  "outDir": "./dist",
8
8
  "strict": true,
9
9
  "strictNullChecks": true,
10
+ "noUncheckedIndexedAccess": true,
10
11
  "esModuleInterop": true,
11
12
  "types": ["node"],
12
13
  "sourceMap": true,