mongodb-mcp-server 0.0.4 → 0.0.6

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 (185) hide show
  1. package/.github/CODEOWNERS +3 -0
  2. package/.github/dependabot.yml +10 -0
  3. package/.github/workflows/code_health.yaml +53 -22
  4. package/.github/workflows/code_health_fork.yaml +106 -0
  5. package/.github/workflows/codeql.yml +34 -0
  6. package/.github/workflows/lint.yml +37 -0
  7. package/.github/workflows/prepare_release.yaml +6 -4
  8. package/.github/workflows/publish.yaml +6 -3
  9. package/.prettierrc.json +1 -1
  10. package/README.md +18 -0
  11. package/dist/common/atlas/apiClient.js +28 -4
  12. package/dist/common/atlas/apiClient.js.map +1 -1
  13. package/dist/config.js +4 -7
  14. package/dist/config.js.map +1 -1
  15. package/dist/errors.js +1 -1
  16. package/dist/errors.js.map +1 -1
  17. package/dist/index.js +12 -7
  18. package/dist/index.js.map +1 -1
  19. package/dist/logger.js +72 -28
  20. package/dist/logger.js.map +1 -1
  21. package/dist/packageInfo.js +6 -0
  22. package/dist/packageInfo.js.map +1 -0
  23. package/dist/server.js +114 -10
  24. package/dist/server.js.map +1 -1
  25. package/dist/session.js +66 -16
  26. package/dist/session.js.map +1 -1
  27. package/dist/telemetry/constants.js +15 -0
  28. package/dist/telemetry/constants.js.map +1 -0
  29. package/dist/telemetry/eventCache.js +53 -0
  30. package/dist/telemetry/eventCache.js.map +1 -0
  31. package/dist/telemetry/telemetry.js +97 -0
  32. package/dist/telemetry/telemetry.js.map +1 -0
  33. package/dist/telemetry/types.js +2 -0
  34. package/dist/telemetry/types.js.map +1 -0
  35. package/dist/tools/atlas/atlasTool.js +8 -3
  36. package/dist/tools/atlas/atlasTool.js.map +1 -1
  37. package/dist/tools/atlas/{createAccessList.js → create/createAccessList.js} +1 -2
  38. package/dist/tools/atlas/create/createAccessList.js.map +1 -0
  39. package/dist/tools/atlas/{createDBUser.js → create/createDBUser.js} +1 -2
  40. package/dist/tools/atlas/create/createDBUser.js.map +1 -0
  41. package/dist/tools/atlas/{createFreeCluster.js → create/createFreeCluster.js} +5 -3
  42. package/dist/tools/atlas/create/createFreeCluster.js.map +1 -0
  43. package/dist/tools/atlas/{createProject.js → create/createProject.js} +1 -2
  44. package/dist/tools/atlas/create/createProject.js.map +1 -0
  45. package/dist/tools/atlas/metadata/connectCluster.js +97 -0
  46. package/dist/tools/atlas/metadata/connectCluster.js.map +1 -0
  47. package/dist/tools/atlas/{inspectAccessList.js → read/inspectAccessList.js} +1 -2
  48. package/dist/tools/atlas/read/inspectAccessList.js.map +1 -0
  49. package/dist/tools/atlas/{inspectCluster.js → read/inspectCluster.js} +1 -2
  50. package/dist/tools/atlas/read/inspectCluster.js.map +1 -0
  51. package/dist/tools/atlas/{listClusters.js → read/listClusters.js} +1 -2
  52. package/dist/tools/atlas/read/listClusters.js.map +1 -0
  53. package/dist/tools/atlas/{listDBUsers.js → read/listDBUsers.js} +1 -2
  54. package/dist/tools/atlas/read/listDBUsers.js.map +1 -0
  55. package/dist/tools/atlas/{listOrgs.js → read/listOrgs.js} +1 -2
  56. package/dist/tools/atlas/read/listOrgs.js.map +1 -0
  57. package/dist/tools/atlas/{listProjects.js → read/listProjects.js} +11 -5
  58. package/dist/tools/atlas/read/listProjects.js.map +1 -0
  59. package/dist/tools/atlas/tools.js +12 -10
  60. package/dist/tools/atlas/tools.js.map +1 -1
  61. package/dist/tools/mongodb/create/insertMany.js +1 -1
  62. package/dist/tools/mongodb/create/insertMany.js.map +1 -1
  63. package/dist/tools/mongodb/delete/deleteMany.js +1 -2
  64. package/dist/tools/mongodb/delete/deleteMany.js.map +1 -1
  65. package/dist/tools/mongodb/metadata/collectionSchema.js +15 -13
  66. package/dist/tools/mongodb/metadata/collectionSchema.js.map +1 -1
  67. package/dist/tools/mongodb/metadata/collectionStorageSize.js +32 -3
  68. package/dist/tools/mongodb/metadata/collectionStorageSize.js.map +1 -1
  69. package/dist/tools/mongodb/metadata/connect.js +59 -72
  70. package/dist/tools/mongodb/metadata/connect.js.map +1 -1
  71. package/dist/tools/mongodb/metadata/dbStats.js +6 -1
  72. package/dist/tools/mongodb/metadata/dbStats.js.map +1 -1
  73. package/dist/tools/mongodb/metadata/explain.js +14 -7
  74. package/dist/tools/mongodb/metadata/explain.js.map +1 -1
  75. package/dist/tools/mongodb/metadata/logs.js +45 -0
  76. package/dist/tools/mongodb/metadata/logs.js.map +1 -0
  77. package/dist/tools/mongodb/mongodbTool.js +42 -36
  78. package/dist/tools/mongodb/mongodbTool.js.map +1 -1
  79. package/dist/tools/mongodb/read/aggregate.js +4 -4
  80. package/dist/tools/mongodb/read/aggregate.js.map +1 -1
  81. package/dist/tools/mongodb/read/collectionIndexes.js +24 -5
  82. package/dist/tools/mongodb/read/collectionIndexes.js.map +1 -1
  83. package/dist/tools/mongodb/read/count.js +1 -2
  84. package/dist/tools/mongodb/read/count.js.map +1 -1
  85. package/dist/tools/mongodb/read/find.js +5 -6
  86. package/dist/tools/mongodb/read/find.js.map +1 -1
  87. package/dist/tools/mongodb/tools.js +6 -2
  88. package/dist/tools/mongodb/tools.js.map +1 -1
  89. package/dist/tools/mongodb/update/renameCollection.js +28 -4
  90. package/dist/tools/mongodb/update/renameCollection.js.map +1 -1
  91. package/dist/tools/mongodb/update/updateMany.js +7 -11
  92. package/dist/tools/mongodb/update/updateMany.js.map +1 -1
  93. package/dist/tools/tool.js +68 -15
  94. package/dist/tools/tool.js.map +1 -1
  95. package/eslint.config.js +29 -10
  96. package/global.d.ts +1 -0
  97. package/package.json +7 -3
  98. package/scripts/apply.ts +9 -7
  99. package/scripts/filter.ts +3 -2
  100. package/src/common/atlas/apiClient.ts +44 -11
  101. package/src/config.ts +16 -17
  102. package/src/errors.ts +1 -1
  103. package/src/index.ts +12 -8
  104. package/src/logger.ts +92 -29
  105. package/src/packageInfo.ts +6 -0
  106. package/src/server.ts +160 -11
  107. package/src/session.ts +102 -22
  108. package/src/telemetry/constants.ts +15 -0
  109. package/src/telemetry/eventCache.ts +62 -0
  110. package/src/telemetry/telemetry.ts +125 -0
  111. package/src/telemetry/types.ts +77 -0
  112. package/src/tools/atlas/atlasTool.ts +7 -5
  113. package/src/tools/atlas/{createAccessList.ts → create/createAccessList.ts} +2 -4
  114. package/src/tools/atlas/{createDBUser.ts → create/createDBUser.ts} +3 -5
  115. package/src/tools/atlas/{createFreeCluster.ts → create/createFreeCluster.ts} +7 -6
  116. package/src/tools/atlas/{createProject.ts → create/createProject.ts} +3 -4
  117. package/src/tools/atlas/metadata/connectCluster.ts +114 -0
  118. package/src/tools/atlas/{inspectAccessList.ts → read/inspectAccessList.ts} +2 -4
  119. package/src/tools/atlas/{inspectCluster.ts → read/inspectCluster.ts} +3 -5
  120. package/src/tools/atlas/{listClusters.ts → read/listClusters.ts} +3 -5
  121. package/src/tools/atlas/{listDBUsers.ts → read/listDBUsers.ts} +3 -5
  122. package/src/tools/atlas/{listOrgs.ts → read/listOrgs.ts} +2 -4
  123. package/src/tools/atlas/{listProjects.ts → read/listProjects.ts} +15 -7
  124. package/src/tools/atlas/tools.ts +12 -10
  125. package/src/tools/mongodb/create/insertMany.ts +1 -1
  126. package/src/tools/mongodb/delete/deleteMany.ts +1 -2
  127. package/src/tools/mongodb/metadata/collectionSchema.ts +16 -14
  128. package/src/tools/mongodb/metadata/collectionStorageSize.ts +41 -3
  129. package/src/tools/mongodb/metadata/connect.ts +78 -76
  130. package/src/tools/mongodb/metadata/dbStats.ts +6 -1
  131. package/src/tools/mongodb/metadata/explain.ts +20 -7
  132. package/src/tools/mongodb/metadata/logs.ts +55 -0
  133. package/src/tools/mongodb/mongodbTool.ts +47 -40
  134. package/src/tools/mongodb/read/aggregate.ts +4 -4
  135. package/src/tools/mongodb/read/collectionIndexes.ts +29 -5
  136. package/src/tools/mongodb/read/count.ts +1 -2
  137. package/src/tools/mongodb/read/find.ts +5 -6
  138. package/src/tools/mongodb/tools.ts +6 -2
  139. package/src/tools/mongodb/update/renameCollection.ts +33 -4
  140. package/src/tools/mongodb/update/updateMany.ts +7 -11
  141. package/src/tools/tool.ts +89 -26
  142. package/tests/integration/helpers.ts +94 -107
  143. package/tests/integration/inMemoryTransport.ts +3 -2
  144. package/tests/integration/server.test.ts +75 -23
  145. package/tests/integration/tools/atlas/accessLists.test.ts +13 -15
  146. package/tests/integration/tools/atlas/atlasHelpers.ts +10 -13
  147. package/tests/integration/tools/atlas/clusters.test.ts +79 -16
  148. package/tests/integration/tools/atlas/dbUsers.test.ts +9 -9
  149. package/tests/integration/tools/atlas/orgs.test.ts +4 -4
  150. package/tests/integration/tools/atlas/projects.test.ts +10 -12
  151. package/tests/integration/tools/mongodb/create/createCollection.test.ts +19 -62
  152. package/tests/integration/tools/mongodb/create/createIndex.test.ts +41 -87
  153. package/tests/integration/tools/mongodb/create/insertMany.test.ts +35 -78
  154. package/tests/integration/tools/mongodb/delete/deleteMany.test.ts +25 -62
  155. package/tests/integration/tools/mongodb/delete/dropCollection.test.ts +22 -71
  156. package/tests/integration/tools/mongodb/delete/dropDatabase.test.ts +29 -63
  157. package/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts +154 -0
  158. package/tests/integration/tools/mongodb/metadata/collectionStorageSize.test.ts +86 -0
  159. package/tests/integration/tools/mongodb/metadata/connect.test.ts +88 -93
  160. package/tests/integration/tools/mongodb/metadata/dbStats.test.ts +104 -0
  161. package/tests/integration/tools/mongodb/metadata/explain.test.ts +171 -0
  162. package/tests/integration/tools/mongodb/metadata/listCollections.test.ts +28 -56
  163. package/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +32 -26
  164. package/tests/integration/tools/mongodb/metadata/logs.test.ts +83 -0
  165. package/tests/integration/tools/mongodb/mongodbHelpers.ts +176 -0
  166. package/tests/integration/tools/mongodb/read/aggregate.test.ts +99 -0
  167. package/tests/integration/tools/mongodb/read/collectionIndexes.test.ts +99 -0
  168. package/tests/integration/tools/mongodb/read/count.test.ts +31 -79
  169. package/tests/integration/tools/mongodb/read/find.test.ts +182 -0
  170. package/tests/integration/tools/mongodb/update/renameCollection.test.ts +194 -0
  171. package/tests/integration/tools/mongodb/update/updateMany.test.ts +238 -0
  172. package/tests/unit/telemetry.test.ts +200 -0
  173. package/tsconfig.jest.json +2 -1
  174. package/tsconfig.json +1 -1
  175. package/tsconfig.lint.json +8 -0
  176. package/dist/tools/atlas/createAccessList.js.map +0 -1
  177. package/dist/tools/atlas/createDBUser.js.map +0 -1
  178. package/dist/tools/atlas/createFreeCluster.js.map +0 -1
  179. package/dist/tools/atlas/createProject.js.map +0 -1
  180. package/dist/tools/atlas/inspectAccessList.js.map +0 -1
  181. package/dist/tools/atlas/inspectCluster.js.map +0 -1
  182. package/dist/tools/atlas/listClusters.js.map +0 -1
  183. package/dist/tools/atlas/listDBUsers.js.map +0 -1
  184. package/dist/tools/atlas/listOrgs.js.map +0 -1
  185. package/dist/tools/atlas/listProjects.js.map +0 -1
@@ -0,0 +1,171 @@
1
+ import {
2
+ databaseCollectionParameters,
3
+ validateToolMetadata,
4
+ validateThrowsForInvalidArguments,
5
+ getResponseElements,
6
+ } from "../../../helpers.js";
7
+ import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js";
8
+
9
+ describeWithMongoDB("explain tool", (integration) => {
10
+ validateToolMetadata(
11
+ integration,
12
+ "explain",
13
+ "Returns statistics describing the execution of the winning plan chosen by the query optimizer for the evaluated method",
14
+ [
15
+ ...databaseCollectionParameters,
16
+
17
+ {
18
+ name: "method",
19
+ description: "The method and its arguments to run",
20
+ type: "array",
21
+ required: true,
22
+ },
23
+ ]
24
+ );
25
+
26
+ validateThrowsForInvalidArguments(integration, "explain", [
27
+ {},
28
+ { database: 123, collection: "bar", method: [{ name: "find", arguments: {} }] },
29
+ { database: "test", collection: true, method: [{ name: "find", arguments: {} }] },
30
+ { database: "test", collection: "bar", method: [{ name: "dnif", arguments: {} }] },
31
+ { database: "test", collection: "bar", method: "find" },
32
+ { database: "test", collection: "bar", method: { name: "find", arguments: {} } },
33
+ ]);
34
+
35
+ const testCases = [
36
+ {
37
+ method: "aggregate",
38
+ arguments: { pipeline: [{ $match: { name: "Peter" } }] },
39
+ },
40
+ {
41
+ method: "find",
42
+ arguments: { filter: { name: "Peter" } },
43
+ },
44
+ {
45
+ method: "count",
46
+ arguments: {
47
+ query: { name: "Peter" },
48
+ },
49
+ },
50
+ ];
51
+
52
+ for (const testType of ["database", "collection"] as const) {
53
+ describe(`with non-existing ${testType}`, () => {
54
+ for (const testCase of testCases) {
55
+ it(`should return the explain plan for ${testCase.method}`, async () => {
56
+ if (testType === "database") {
57
+ const { databases } = await integration.mongoClient().db("").admin().listDatabases();
58
+ expect(databases.find((db) => db.name === integration.randomDbName())).toBeUndefined();
59
+ } else if (testType === "collection") {
60
+ await integration
61
+ .mongoClient()
62
+ .db(integration.randomDbName())
63
+ .createCollection("some-collection");
64
+
65
+ const collections = await integration
66
+ .mongoClient()
67
+ .db(integration.randomDbName())
68
+ .listCollections()
69
+ .toArray();
70
+
71
+ expect(collections.find((collection) => collection.name === "coll1")).toBeUndefined();
72
+ }
73
+
74
+ await integration.connectMcpClient();
75
+
76
+ const response = await integration.mcpClient().callTool({
77
+ name: "explain",
78
+ arguments: {
79
+ database: integration.randomDbName(),
80
+ collection: "coll1",
81
+ method: [
82
+ {
83
+ name: testCase.method,
84
+ arguments: testCase.arguments,
85
+ },
86
+ ],
87
+ },
88
+ });
89
+
90
+ const content = getResponseElements(response.content);
91
+ expect(content).toHaveLength(2);
92
+ expect(content[0].text).toEqual(
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
+ );
95
+
96
+ expect(content[1].text).toContain("queryPlanner");
97
+ expect(content[1].text).toContain("winningPlan");
98
+ });
99
+ }
100
+ });
101
+ }
102
+
103
+ describe("with existing database and collection", () => {
104
+ for (const indexed of [true, false] as const) {
105
+ describe(`with ${indexed ? "an index" : "no index"}`, () => {
106
+ beforeEach(async () => {
107
+ await integration
108
+ .mongoClient()
109
+ .db(integration.randomDbName())
110
+ .collection("people")
111
+ .insertMany([{ name: "Alice" }, { name: "Bob" }, { name: "Charlie" }]);
112
+
113
+ if (indexed) {
114
+ await integration
115
+ .mongoClient()
116
+ .db(integration.randomDbName())
117
+ .collection("people")
118
+ .createIndex({ name: 1 });
119
+ }
120
+ });
121
+
122
+ for (const testCase of testCases) {
123
+ it(`should return the explain plan for ${testCase.method}`, async () => {
124
+ await integration.connectMcpClient();
125
+
126
+ const response = await integration.mcpClient().callTool({
127
+ name: "explain",
128
+ arguments: {
129
+ database: integration.randomDbName(),
130
+ collection: "people",
131
+ method: [
132
+ {
133
+ name: testCase.method,
134
+ arguments: testCase.arguments,
135
+ },
136
+ ],
137
+ },
138
+ });
139
+
140
+ const content = getResponseElements(response.content);
141
+ expect(content).toHaveLength(2);
142
+ expect(content[0].text).toEqual(
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
+ );
145
+
146
+ expect(content[1].text).toContain("queryPlanner");
147
+ expect(content[1].text).toContain("winningPlan");
148
+
149
+ if (indexed) {
150
+ if (testCase.method === "count") {
151
+ expect(content[1].text).toContain("COUNT_SCAN");
152
+ } else {
153
+ expect(content[1].text).toContain("IXSCAN");
154
+ }
155
+ expect(content[1].text).toContain("name_1");
156
+ } else {
157
+ expect(content[1].text).toContain("COLLSCAN");
158
+ }
159
+ });
160
+ }
161
+ });
162
+ }
163
+ });
164
+
165
+ validateAutoConnectBehavior(integration, "explain", () => {
166
+ return {
167
+ args: { database: integration.randomDbName(), collection: "coll1", method: [] },
168
+ expectedResponse: "No method provided. Expected one of the following: `aggregate`, `find`, or `count`",
169
+ };
170
+ });
171
+ });
@@ -1,41 +1,23 @@
1
- import { getResponseElements, getResponseContent, validateParameters, setupIntegrationTest } from "../../../helpers.js";
2
- import { toIncludeSameMembers } from "jest-extended";
3
- import { McpError } from "@modelcontextprotocol/sdk/types.js";
4
- import config from "../../../../../src/config.js";
5
- import { ObjectId } from "bson";
1
+ import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js";
6
2
 
7
- describe("listCollections tool", () => {
8
- const integration = setupIntegrationTest();
3
+ import {
4
+ getResponseElements,
5
+ getResponseContent,
6
+ validateToolMetadata,
7
+ validateThrowsForInvalidArguments,
8
+ databaseInvalidArgs,
9
+ databaseParameters,
10
+ } from "../../../helpers.js";
9
11
 
10
- it("should have correct metadata", async () => {
11
- const { tools } = await integration.mcpClient().listTools();
12
- const listCollections = tools.find((tool) => tool.name === "list-collections")!;
13
- expect(listCollections).toBeDefined();
14
- expect(listCollections.description).toBe("List all collections for a given database");
12
+ describeWithMongoDB("listCollections tool", (integration) => {
13
+ validateToolMetadata(
14
+ integration,
15
+ "list-collections",
16
+ "List all collections for a given database",
17
+ databaseParameters
18
+ );
15
19
 
16
- validateParameters(listCollections, [
17
- { name: "database", description: "Database name", type: "string", required: true },
18
- ]);
19
- });
20
-
21
- describe("with invalid arguments", () => {
22
- const args = [{}, { database: 123 }, { foo: "bar", database: "test" }, { database: [] }];
23
- for (const arg of args) {
24
- it(`throws a schema error for: ${JSON.stringify(arg)}`, async () => {
25
- await integration.connectMcpClient();
26
- try {
27
- await integration.mcpClient().callTool({ name: "list-collections", arguments: arg });
28
- expect.fail("Expected an error to be thrown");
29
- } catch (error) {
30
- expect(error).toBeInstanceOf(McpError);
31
- const mcpError = error as McpError;
32
- expect(mcpError.code).toEqual(-32602);
33
- expect(mcpError.message).toContain("Invalid arguments for tool list-collections");
34
- expect(mcpError.message).toContain('"expected": "string"');
35
- }
36
- });
37
- }
38
- });
20
+ validateThrowsForInvalidArguments(integration, "list-collections", databaseInvalidArgs);
39
21
 
40
22
  describe("with non-existent database", () => {
41
23
  it("returns no collections", async () => {
@@ -46,7 +28,7 @@ describe("listCollections tool", () => {
46
28
  });
47
29
  const content = getResponseContent(response.content);
48
30
  expect(content).toEqual(
49
- `No collections found for database "non-existent". To create a collection, use the "create-collection" tool.`
31
+ 'No collections found for database "non-existent". To create a collection, use the "create-collection" tool.'
50
32
  );
51
33
  });
52
34
  });
@@ -80,25 +62,15 @@ describe("listCollections tool", () => {
80
62
  });
81
63
  });
82
64
 
83
- describe("when not connected", () => {
84
- it("connects automatically if connection string is configured", async () => {
85
- config.connectionString = integration.connectionString();
86
-
87
- const response = await integration
88
- .mcpClient()
89
- .callTool({ name: "list-collections", arguments: { database: integration.randomDbName() } });
90
- const content = getResponseContent(response.content);
91
- expect(content).toEqual(
92
- `No collections found for database "${integration.randomDbName()}". To create a collection, use the "create-collection" tool.`
93
- );
94
- });
65
+ validateAutoConnectBehavior(
66
+ integration,
67
+ "list-collections",
95
68
 
96
- it("throws an error if connection string is not configured", async () => {
97
- const response = await integration
98
- .mcpClient()
99
- .callTool({ name: "list-collections", arguments: { database: integration.randomDbName() } });
100
- const content = getResponseContent(response.content);
101
- expect(content).toContain("You need to connect to a MongoDB instance before you can access its data.");
102
- });
103
- });
69
+ () => {
70
+ return {
71
+ args: { database: integration.randomDbName() },
72
+ expectedResponse: `No collections found for database "${integration.randomDbName()}". To create a collection, use the "create-collection" tool.`,
73
+ };
74
+ }
75
+ );
104
76
  });
@@ -1,44 +1,26 @@
1
- import config from "../../../../../src/config.js";
2
- import { getResponseElements, getParameters, setupIntegrationTest, getResponseContent } from "../../../helpers.js";
3
- import { toIncludeSameMembers } from "jest-extended";
1
+ import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js";
2
+ import { getResponseElements, getParameters, expectDefined } from "../../../helpers.js";
4
3
 
5
- describe("listDatabases tool", () => {
6
- const integration = setupIntegrationTest();
4
+ describeWithMongoDB("listDatabases tool", (integration) => {
5
+ const defaultDatabases = ["admin", "config", "local"];
7
6
 
8
7
  it("should have correct metadata", async () => {
9
8
  const { tools } = await integration.mcpClient().listTools();
10
- const listDatabases = tools.find((tool) => tool.name === "list-databases")!;
11
- expect(listDatabases).toBeDefined();
9
+ const listDatabases = tools.find((tool) => tool.name === "list-databases");
10
+ expectDefined(listDatabases);
12
11
  expect(listDatabases.description).toBe("List all databases for a MongoDB connection");
13
12
 
14
13
  const parameters = getParameters(listDatabases);
15
14
  expect(parameters).toHaveLength(0);
16
15
  });
17
16
 
18
- describe("when not connected", () => {
19
- it("connects automatically if connection string is configured", async () => {
20
- config.connectionString = integration.connectionString();
21
-
22
- const response = await integration.mcpClient().callTool({ name: "list-databases", arguments: {} });
23
- const dbNames = getDbNames(response.content);
24
-
25
- expect(dbNames).toIncludeSameMembers(["admin", "config", "local"]);
26
- });
27
-
28
- it("throws an error if connection string is not configured", async () => {
29
- const response = await integration.mcpClient().callTool({ name: "list-databases", arguments: {} });
30
- const content = getResponseContent(response.content);
31
- expect(content).toContain("You need to connect to a MongoDB instance before you can access its data.");
32
- });
33
- });
34
-
35
17
  describe("with no preexisting databases", () => {
36
18
  it("returns only the system databases", async () => {
37
19
  await integration.connectMcpClient();
38
20
  const response = await integration.mcpClient().callTool({ name: "list-databases", arguments: {} });
39
21
  const dbNames = getDbNames(response.content);
40
22
 
41
- expect(dbNames).toIncludeSameMembers(["admin", "config", "local"]);
23
+ expect(defaultDatabases).toIncludeAllMembers(dbNames);
42
24
  });
43
25
  });
44
26
 
@@ -52,9 +34,33 @@ describe("listDatabases tool", () => {
52
34
 
53
35
  const response = await integration.mcpClient().callTool({ name: "list-databases", arguments: {} });
54
36
  const dbNames = getDbNames(response.content);
55
- expect(dbNames).toIncludeSameMembers(["admin", "config", "local", "foo", "baz"]);
37
+ expect(dbNames).toIncludeSameMembers([...defaultDatabases, "foo", "baz"]);
56
38
  });
57
39
  });
40
+
41
+ validateAutoConnectBehavior(
42
+ integration,
43
+ "list-databases",
44
+ () => {
45
+ return {
46
+ args: {},
47
+ validate: (content) => {
48
+ const dbNames = getDbNames(content);
49
+
50
+ expect(defaultDatabases).toIncludeAllMembers(dbNames);
51
+ },
52
+ };
53
+ },
54
+ async () => {
55
+ const mongoClient = integration.mongoClient();
56
+ const { databases } = await mongoClient.db("admin").command({ listDatabases: 1, nameOnly: true });
57
+ for (const db of databases as { name: string }[]) {
58
+ if (!defaultDatabases.includes(db.name)) {
59
+ await mongoClient.db(db.name).dropDatabase();
60
+ }
61
+ }
62
+ }
63
+ );
58
64
  });
59
65
 
60
66
  function getDbNames(content: unknown): (string | null)[] {
@@ -0,0 +1,83 @@
1
+ import { validateToolMetadata, validateThrowsForInvalidArguments, getResponseElements } from "../../../helpers.js";
2
+ import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js";
3
+
4
+ describeWithMongoDB("logs tool", (integration) => {
5
+ validateToolMetadata(integration, "mongodb-logs", "Returns the most recent logged mongod events", [
6
+ {
7
+ type: "string",
8
+ name: "type",
9
+ description:
10
+ "The type of logs to return. Global returns all recent log entries, while startupWarnings returns only warnings and errors from when the process started.",
11
+ required: false,
12
+ },
13
+ {
14
+ type: "integer",
15
+ name: "limit",
16
+ description: "The maximum number of log entries to return.",
17
+ required: false,
18
+ },
19
+ ]);
20
+
21
+ validateThrowsForInvalidArguments(integration, "mongodb-logs", [
22
+ { type: 123 },
23
+ { type: "something" },
24
+ { limit: 0 },
25
+ { limit: true },
26
+ { limit: 1025 },
27
+ ]);
28
+
29
+ it("should return global logs", async () => {
30
+ await integration.connectMcpClient();
31
+ const response = await integration.mcpClient().callTool({
32
+ name: "mongodb-logs",
33
+ arguments: {},
34
+ });
35
+
36
+ const elements = getResponseElements(response);
37
+
38
+ // Default limit is 50
39
+ expect(elements.length).toBeLessThanOrEqual(51);
40
+ expect(elements[0].text).toMatch(/Found: \d+ messages/);
41
+
42
+ for (let i = 1; i < elements.length; i++) {
43
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
44
+ const log = JSON.parse(elements[i].text);
45
+ expect(log).toHaveProperty("t");
46
+ expect(log).toHaveProperty("msg");
47
+ }
48
+ });
49
+
50
+ it("should return startupWarnings logs", async () => {
51
+ await integration.connectMcpClient();
52
+ const response = await integration.mcpClient().callTool({
53
+ name: "mongodb-logs",
54
+ arguments: {
55
+ type: "startupWarnings",
56
+ },
57
+ });
58
+
59
+ const elements = getResponseElements(response);
60
+ expect(elements.length).toBeLessThanOrEqual(51);
61
+ for (let i = 1; i < elements.length; i++) {
62
+ const log = JSON.parse(elements[i].text) as { tags: string[] };
63
+ expect(log).toHaveProperty("t");
64
+ expect(log).toHaveProperty("msg");
65
+ expect(log).toHaveProperty("tags");
66
+ expect(log.tags).toContain("startupWarnings");
67
+ }
68
+ });
69
+
70
+ validateAutoConnectBehavior(integration, "mongodb-logs", () => {
71
+ return {
72
+ args: {
73
+ database: integration.randomDbName(),
74
+ collection: "foo",
75
+ },
76
+ validate: (content) => {
77
+ const elements = getResponseElements(content);
78
+ expect(elements.length).toBeLessThanOrEqual(51);
79
+ expect(elements[0].text).toMatch(/Found: \d+ messages/);
80
+ },
81
+ };
82
+ });
83
+ });
@@ -0,0 +1,176 @@
1
+ import { MongoCluster } from "mongodb-runner";
2
+ import path from "path";
3
+ import fs from "fs/promises";
4
+ import { MongoClient, ObjectId } from "mongodb";
5
+ import { getResponseContent, IntegrationTest, setupIntegrationTest } from "../../helpers.js";
6
+ import { config, UserConfig } from "../../../../src/config.js";
7
+
8
+ interface MongoDBIntegrationTest {
9
+ mongoClient: () => MongoClient;
10
+ connectionString: () => string;
11
+ randomDbName: () => string;
12
+ }
13
+
14
+ export function describeWithMongoDB(
15
+ name: string,
16
+ fn: (integration: IntegrationTest & MongoDBIntegrationTest & { connectMcpClient: () => Promise<void> }) => void,
17
+ getUserConfig: (mdbIntegration: MongoDBIntegrationTest) => UserConfig = () => config,
18
+ describeFn = describe
19
+ ) {
20
+ describeFn(name, () => {
21
+ const mdbIntegration = setupMongoDBIntegrationTest();
22
+ const integration = setupIntegrationTest(() => ({
23
+ ...getUserConfig(mdbIntegration),
24
+ connectionString: mdbIntegration.connectionString(),
25
+ }));
26
+
27
+ beforeEach(() => {
28
+ integration.mcpServer().userConfig.connectionString = mdbIntegration.connectionString();
29
+ });
30
+
31
+ fn({
32
+ ...integration,
33
+ ...mdbIntegration,
34
+ connectMcpClient: async () => {
35
+ // TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when
36
+ // the connect tool is reenabled
37
+ // await integration.mcpClient().callTool({
38
+ // name: "connect",
39
+ // arguments: { connectionString: mdbIntegration.connectionString() },
40
+ // });
41
+ },
42
+ });
43
+ });
44
+ }
45
+
46
+ export function setupMongoDBIntegrationTest(): MongoDBIntegrationTest {
47
+ let mongoCluster: // TODO: Fix this type once mongodb-runner is updated.
48
+ | {
49
+ connectionString: string;
50
+ close: () => Promise<void>;
51
+ }
52
+ | undefined;
53
+ let mongoClient: MongoClient | undefined;
54
+ let randomDbName: string;
55
+
56
+ beforeEach(() => {
57
+ randomDbName = new ObjectId().toString();
58
+ });
59
+
60
+ afterEach(async () => {
61
+ await mongoClient?.close();
62
+ mongoClient = undefined;
63
+ });
64
+
65
+ beforeAll(async function () {
66
+ // Downloading Windows executables in CI takes a long time because
67
+ // they include debug symbols...
68
+ const tmpDir = path.join(__dirname, "..", "..", "..", "tmp");
69
+ await fs.mkdir(tmpDir, { recursive: true });
70
+
71
+ // On Windows, we may have a situation where mongod.exe is not fully released by the OS
72
+ // before we attempt to run it again, so we add a retry.
73
+ let dbsDir = path.join(tmpDir, "mongodb-runner", "dbs");
74
+ for (let i = 0; i < 10; i++) {
75
+ try {
76
+ // TODO: Fix this type once mongodb-runner is updated.
77
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
78
+ mongoCluster = await MongoCluster.start({
79
+ tmpDir: dbsDir,
80
+ logDir: path.join(tmpDir, "mongodb-runner", "logs"),
81
+ topology: "standalone",
82
+ });
83
+
84
+ return;
85
+ } catch (err) {
86
+ if (i < 5) {
87
+ // Just wait a little bit and retry
88
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
89
+ console.error(`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}`);
90
+ await new Promise((resolve) => setTimeout(resolve, 1000));
91
+ } else {
92
+ // If we still fail after 5 seconds, try another db dir
93
+ console.error(
94
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
95
+ `Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}. Retrying with a new db dir.`
96
+ );
97
+ dbsDir = path.join(tmpDir, "mongodb-runner", `dbs${i - 5}`);
98
+ }
99
+ }
100
+ }
101
+
102
+ throw new Error("Failed to start cluster after 10 attempts");
103
+ }, 120_000);
104
+
105
+ afterAll(async function () {
106
+ await mongoCluster?.close();
107
+ mongoCluster = undefined;
108
+ });
109
+
110
+ const getConnectionString = () => {
111
+ if (!mongoCluster) {
112
+ throw new Error("beforeAll() hook not ran yet");
113
+ }
114
+
115
+ return mongoCluster.connectionString;
116
+ };
117
+
118
+ return {
119
+ mongoClient: () => {
120
+ if (!mongoClient) {
121
+ mongoClient = new MongoClient(getConnectionString());
122
+ }
123
+ return mongoClient;
124
+ },
125
+ connectionString: getConnectionString,
126
+
127
+ randomDbName: () => randomDbName,
128
+ };
129
+ }
130
+
131
+ export function validateAutoConnectBehavior(
132
+ integration: IntegrationTest & MongoDBIntegrationTest,
133
+ name: string,
134
+ validation: () => {
135
+ args: { [x: string]: unknown };
136
+ expectedResponse?: string;
137
+ validate?: (content: unknown) => void;
138
+ },
139
+ beforeEachImpl?: () => Promise<void>
140
+ ): void {
141
+ // TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled
142
+ describe.skip("when not connected", () => {
143
+ if (beforeEachImpl) {
144
+ beforeEach(() => beforeEachImpl());
145
+ }
146
+
147
+ it("connects automatically if connection string is configured", async () => {
148
+ integration.mcpServer().userConfig.connectionString = integration.connectionString();
149
+
150
+ const validationInfo = validation();
151
+
152
+ const response = await integration.mcpClient().callTool({
153
+ name,
154
+ arguments: validationInfo.args,
155
+ });
156
+
157
+ if (validationInfo.expectedResponse) {
158
+ const content = getResponseContent(response.content);
159
+ expect(content).toContain(validationInfo.expectedResponse);
160
+ }
161
+
162
+ if (validationInfo.validate) {
163
+ validationInfo.validate(response.content);
164
+ }
165
+ });
166
+
167
+ it("throws an error if connection string is not configured", async () => {
168
+ const response = await integration.mcpClient().callTool({
169
+ name,
170
+ arguments: validation().args,
171
+ });
172
+ const content = getResponseContent(response.content);
173
+ expect(content).toContain("You need to connect to a MongoDB instance before you can access its data.");
174
+ });
175
+ });
176
+ }