mongodb-mcp-server 0.1.0 → 0.1.2

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 (137) hide show
  1. package/.dockerignore +11 -0
  2. package/.github/CODEOWNERS +0 -2
  3. package/.github/ISSUE_TEMPLATE/bug_report.yml +8 -0
  4. package/.github/workflows/check-pr-title.yml +29 -0
  5. package/.github/workflows/{lint.yml → check.yml} +22 -1
  6. package/.github/workflows/code_health.yaml +0 -22
  7. package/.github/workflows/code_health_fork.yaml +7 -63
  8. package/.github/workflows/docker.yaml +57 -0
  9. package/.github/workflows/stale.yml +32 -0
  10. package/.smithery/Dockerfile +30 -0
  11. package/.smithery/smithery.yaml +63 -0
  12. package/.vscode/extensions.json +9 -0
  13. package/.vscode/settings.json +11 -0
  14. package/CONTRIBUTING.md +1 -1
  15. package/Dockerfile +10 -0
  16. package/README.md +173 -35
  17. package/dist/common/atlas/apiClient.js +151 -35
  18. package/dist/common/atlas/apiClient.js.map +1 -1
  19. package/dist/common/atlas/apiClientError.js +38 -5
  20. package/dist/common/atlas/apiClientError.js.map +1 -1
  21. package/dist/common/atlas/cluster.js +66 -0
  22. package/dist/common/atlas/cluster.js.map +1 -0
  23. package/dist/common/atlas/generatePassword.js +9 -0
  24. package/dist/common/atlas/generatePassword.js.map +1 -0
  25. package/dist/helpers/EJsonTransport.js +38 -0
  26. package/dist/helpers/EJsonTransport.js.map +1 -0
  27. package/dist/helpers/connectionOptions.js +10 -0
  28. package/dist/helpers/connectionOptions.js.map +1 -0
  29. package/dist/{packageInfo.js → helpers/packageInfo.js} +1 -1
  30. package/dist/helpers/packageInfo.js.map +1 -0
  31. package/dist/index.js +23 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/logger.js +7 -0
  34. package/dist/logger.js.map +1 -1
  35. package/dist/server.js +16 -12
  36. package/dist/server.js.map +1 -1
  37. package/dist/session.js +8 -3
  38. package/dist/session.js.map +1 -1
  39. package/dist/telemetry/constants.js +1 -3
  40. package/dist/telemetry/constants.js.map +1 -1
  41. package/dist/telemetry/eventCache.js.map +1 -1
  42. package/dist/telemetry/telemetry.js +126 -47
  43. package/dist/telemetry/telemetry.js.map +1 -1
  44. package/dist/tools/atlas/atlasTool.js +38 -0
  45. package/dist/tools/atlas/atlasTool.js.map +1 -1
  46. package/dist/tools/atlas/create/createDBUser.js +19 -2
  47. package/dist/tools/atlas/create/createDBUser.js.map +1 -1
  48. package/dist/tools/atlas/create/createProject.js +5 -1
  49. package/dist/tools/atlas/create/createProject.js.map +1 -1
  50. package/dist/tools/atlas/metadata/connectCluster.js +5 -22
  51. package/dist/tools/atlas/metadata/connectCluster.js.map +1 -1
  52. package/dist/tools/atlas/read/inspectCluster.js +4 -24
  53. package/dist/tools/atlas/read/inspectCluster.js.map +1 -1
  54. package/dist/tools/atlas/read/listAlerts.js +41 -0
  55. package/dist/tools/atlas/read/listAlerts.js.map +1 -0
  56. package/dist/tools/atlas/read/listClusters.js +9 -18
  57. package/dist/tools/atlas/read/listClusters.js.map +1 -1
  58. package/dist/tools/atlas/read/listProjects.js +3 -1
  59. package/dist/tools/atlas/read/listProjects.js.map +1 -1
  60. package/dist/tools/atlas/tools.js +2 -0
  61. package/dist/tools/atlas/tools.js.map +1 -1
  62. package/dist/tools/mongodb/metadata/listDatabases.js.map +1 -1
  63. package/dist/tools/mongodb/read/count.js +2 -2
  64. package/dist/tools/mongodb/read/count.js.map +1 -1
  65. package/dist/tools/mongodb/tools.js +2 -4
  66. package/dist/tools/mongodb/tools.js.map +1 -1
  67. package/dist/tools/tool.js +38 -6
  68. package/dist/tools/tool.js.map +1 -1
  69. package/eslint.config.js +2 -1
  70. package/{jest.config.ts → jest.config.cjs} +1 -1
  71. package/package.json +11 -9
  72. package/scripts/apply.ts +8 -5
  73. package/scripts/filter.ts +5 -0
  74. package/src/common/atlas/apiClient.ts +190 -38
  75. package/src/common/atlas/apiClientError.ts +58 -7
  76. package/src/common/atlas/cluster.ts +94 -0
  77. package/src/common/atlas/generatePassword.ts +10 -0
  78. package/src/common/atlas/openapi.d.ts +1876 -239
  79. package/src/helpers/EJsonTransport.ts +47 -0
  80. package/src/helpers/connectionOptions.ts +20 -0
  81. package/src/{packageInfo.ts → helpers/packageInfo.ts} +1 -1
  82. package/src/index.ts +27 -3
  83. package/src/logger.ts +8 -0
  84. package/src/server.ts +23 -15
  85. package/src/session.ts +8 -4
  86. package/src/telemetry/constants.ts +2 -3
  87. package/src/telemetry/eventCache.ts +1 -1
  88. package/src/telemetry/telemetry.ts +182 -64
  89. package/src/telemetry/types.ts +1 -1
  90. package/src/tools/atlas/atlasTool.ts +47 -1
  91. package/src/tools/atlas/create/createDBUser.ts +22 -2
  92. package/src/tools/atlas/create/createProject.ts +7 -1
  93. package/src/tools/atlas/metadata/connectCluster.ts +5 -27
  94. package/src/tools/atlas/read/inspectCluster.ts +4 -40
  95. package/src/tools/atlas/read/listAlerts.ts +45 -0
  96. package/src/tools/atlas/read/listClusters.ts +19 -36
  97. package/src/tools/atlas/read/listProjects.ts +4 -2
  98. package/src/tools/atlas/tools.ts +2 -0
  99. package/src/tools/mongodb/metadata/listDatabases.ts +0 -1
  100. package/src/tools/mongodb/read/count.ts +3 -2
  101. package/src/tools/mongodb/tools.ts +2 -4
  102. package/src/tools/tool.ts +45 -8
  103. package/src/types/mongodb-connection-string-url.d.ts +69 -0
  104. package/tests/integration/helpers.ts +41 -2
  105. package/tests/integration/tools/atlas/accessLists.test.ts +2 -2
  106. package/tests/integration/tools/atlas/alerts.test.ts +42 -0
  107. package/tests/integration/tools/atlas/atlasHelpers.ts +5 -3
  108. package/tests/integration/tools/atlas/clusters.test.ts +4 -4
  109. package/tests/integration/tools/atlas/dbUsers.test.ts +58 -33
  110. package/tests/integration/tools/atlas/orgs.test.ts +2 -2
  111. package/tests/integration/tools/atlas/projects.test.ts +3 -3
  112. package/tests/integration/tools/mongodb/create/createCollection.test.ts +2 -2
  113. package/tests/integration/tools/mongodb/create/createIndex.test.ts +2 -2
  114. package/tests/integration/tools/mongodb/create/insertMany.test.ts +1 -1
  115. package/tests/integration/tools/mongodb/delete/dropCollection.test.ts +1 -1
  116. package/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts +2 -2
  117. package/tests/integration/tools/mongodb/metadata/connect.test.ts +2 -6
  118. package/tests/integration/tools/mongodb/metadata/dbStats.test.ts +4 -4
  119. package/tests/integration/tools/mongodb/metadata/explain.test.ts +10 -10
  120. package/tests/integration/tools/mongodb/metadata/listCollections.test.ts +1 -1
  121. package/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +9 -5
  122. package/tests/integration/tools/mongodb/metadata/logs.test.ts +4 -4
  123. package/tests/integration/tools/mongodb/mongodbHelpers.ts +15 -24
  124. package/tests/integration/tools/mongodb/read/aggregate.test.ts +22 -7
  125. package/tests/integration/tools/mongodb/read/collectionIndexes.test.ts +5 -5
  126. package/tests/integration/tools/mongodb/read/count.test.ts +15 -10
  127. package/tests/integration/tools/mongodb/read/find.test.ts +32 -4
  128. package/tests/integration/tools/mongodb/update/renameCollection.test.ts +4 -4
  129. package/tests/unit/EJsonTransport.test.ts +71 -0
  130. package/tests/unit/apiClient.test.ts +193 -0
  131. package/tests/unit/session.test.ts +65 -0
  132. package/tests/unit/telemetry.test.ts +222 -80
  133. package/tsconfig.build.json +2 -1
  134. package/dist/packageInfo.js.map +0 -1
  135. package/dist/telemetry/device-id.js +0 -20
  136. package/dist/telemetry/device-id.js.map +0 -1
  137. package/src/telemetry/device-id.ts +0 -21
@@ -67,7 +67,7 @@ describeWithAtlas("ip access lists", (integration) => {
67
67
  })) as CallToolResult;
68
68
  expect(response.content).toBeArray();
69
69
  expect(response.content).toHaveLength(1);
70
- expect(response.content[0].text).toContain("IP/CIDR ranges added to access list");
70
+ expect(response.content[0]?.text).toContain("IP/CIDR ranges added to access list");
71
71
  });
72
72
  });
73
73
 
@@ -90,7 +90,7 @@ describeWithAtlas("ip access lists", (integration) => {
90
90
  expect(response.content).toBeArray();
91
91
  expect(response.content).toHaveLength(1);
92
92
  for (const value of values) {
93
- expect(response.content[0].text).toContain(value);
93
+ expect(response.content[0]?.text).toContain(value);
94
94
  }
95
95
  });
96
96
  });
@@ -0,0 +1,42 @@
1
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
+ import { expectDefined } from "../../helpers.js";
3
+ import { parseTable, describeWithAtlas, withProject } from "./atlasHelpers.js";
4
+
5
+ describeWithAtlas("alerts", (integration) => {
6
+ describe("atlas-list-alerts", () => {
7
+ it("should have correct metadata", async () => {
8
+ const { tools } = await integration.mcpClient().listTools();
9
+ const listAlerts = tools.find((tool) => tool.name === "atlas-list-alerts");
10
+ expectDefined(listAlerts);
11
+ expect(listAlerts.inputSchema.type).toBe("object");
12
+ expectDefined(listAlerts.inputSchema.properties);
13
+ expect(listAlerts.inputSchema.properties).toHaveProperty("projectId");
14
+ });
15
+
16
+ withProject(integration, ({ getProjectId }) => {
17
+ it("returns alerts in table format", async () => {
18
+ const response = (await integration.mcpClient().callTool({
19
+ name: "atlas-list-alerts",
20
+ arguments: { projectId: getProjectId() },
21
+ })) as CallToolResult;
22
+
23
+ expect(response.content).toBeArray();
24
+ expect(response.content).toHaveLength(1);
25
+
26
+ const data = parseTable(response.content[0]?.text as string);
27
+ expect(data).toBeArray();
28
+
29
+ // Since we can't guarantee alerts will exist, we just verify the table structure
30
+ if (data.length > 0) {
31
+ const alert = data[0];
32
+ expect(alert).toHaveProperty("Alert ID");
33
+ expect(alert).toHaveProperty("Status");
34
+ expect(alert).toHaveProperty("Created");
35
+ expect(alert).toHaveProperty("Updated");
36
+ expect(alert).toHaveProperty("Type");
37
+ expect(alert).toHaveProperty("Comment");
38
+ }
39
+ });
40
+ });
41
+ });
42
+ });
@@ -75,7 +75,9 @@ export function parseTable(text: string): Record<string, string>[] {
75
75
  .map((cells) => {
76
76
  const row: Record<string, string> = {};
77
77
  cells.forEach((cell, index) => {
78
- row[headers[index]] = cell;
78
+ if (headers) {
79
+ row[headers[index] ?? ""] = cell;
80
+ }
79
81
  });
80
82
  return row;
81
83
  });
@@ -87,14 +89,14 @@ async function createProject(apiClient: ApiClient): Promise<Group> {
87
89
  const projectName: string = `testProj-` + randomId;
88
90
 
89
91
  const orgs = await apiClient.listOrganizations();
90
- if (!orgs?.results?.length || !orgs.results[0].id) {
92
+ if (!orgs?.results?.length || !orgs.results[0]?.id) {
91
93
  throw new Error("No orgs found");
92
94
  }
93
95
 
94
96
  const group = await apiClient.createProject({
95
97
  body: {
96
98
  name: projectName,
97
- orgId: orgs.results[0].id,
99
+ orgId: orgs.results[0]?.id ?? "",
98
100
  } as Group,
99
101
  });
100
102
 
@@ -88,7 +88,7 @@ describeWithAtlas("clusters", (integration) => {
88
88
  })) as CallToolResult;
89
89
  expect(response.content).toBeArray();
90
90
  expect(response.content).toHaveLength(2);
91
- expect(response.content[0].text).toContain("has been created");
91
+ expect(response.content[0]?.text).toContain("has been created");
92
92
  });
93
93
  });
94
94
 
@@ -113,7 +113,7 @@ describeWithAtlas("clusters", (integration) => {
113
113
  })) as CallToolResult;
114
114
  expect(response.content).toBeArray();
115
115
  expect(response.content).toHaveLength(1);
116
- expect(response.content[0].text).toContain(`${clusterName} | `);
116
+ expect(response.content[0]?.text).toContain(`${clusterName} | `);
117
117
  });
118
118
  });
119
119
 
@@ -135,7 +135,7 @@ describeWithAtlas("clusters", (integration) => {
135
135
  .callTool({ name: "atlas-list-clusters", arguments: { projectId } })) as CallToolResult;
136
136
  expect(response.content).toBeArray();
137
137
  expect(response.content).toHaveLength(2);
138
- expect(response.content[1].text).toContain(`${clusterName} | `);
138
+ expect(response.content[1]?.text).toContain(`${clusterName} | `);
139
139
  });
140
140
  });
141
141
 
@@ -178,7 +178,7 @@ describeWithAtlas("clusters", (integration) => {
178
178
  })) as CallToolResult;
179
179
  expect(response.content).toBeArray();
180
180
  expect(response.content).toHaveLength(1);
181
- expect(response.content[0].text).toContain(`Connected to cluster "${clusterName}"`);
181
+ expect(response.content[0]?.text).toContain(`Connected to cluster "${clusterName}"`);
182
182
  });
183
183
  });
184
184
  });
@@ -1,24 +1,49 @@
1
1
  import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
- import { Session } from "../../../../src/session.js";
3
2
  import { describeWithAtlas, withProject, randomId } from "./atlasHelpers.js";
4
- import { expectDefined } from "../../helpers.js";
3
+ import { expectDefined, getResponseElements } from "../../helpers.js";
4
+ import { ApiClientError } from "../../../../src/common/atlas/apiClientError.js";
5
5
 
6
6
  describeWithAtlas("db users", (integration) => {
7
- const userName = "testuser-" + randomId;
8
7
  withProject(integration, ({ getProjectId }) => {
9
- afterAll(async () => {
10
- const projectId = getProjectId();
8
+ let userName: string;
9
+ beforeEach(() => {
10
+ userName = "testuser-" + randomId;
11
+ });
11
12
 
12
- const session: Session = integration.mcpServer().session;
13
- await session.apiClient.deleteDatabaseUser({
14
- params: {
15
- path: {
16
- groupId: projectId,
17
- username: userName,
18
- databaseName: "admin",
19
- },
13
+ const createUserWithMCP = async (password?: string): Promise<unknown> => {
14
+ return await integration.mcpClient().callTool({
15
+ name: "atlas-create-db-user",
16
+ arguments: {
17
+ projectId: getProjectId(),
18
+ username: userName,
19
+ password,
20
+ roles: [
21
+ {
22
+ roleName: "readWrite",
23
+ databaseName: "admin",
24
+ },
25
+ ],
20
26
  },
21
27
  });
28
+ };
29
+
30
+ afterEach(async () => {
31
+ try {
32
+ await integration.mcpServer().session.apiClient.deleteDatabaseUser({
33
+ params: {
34
+ path: {
35
+ groupId: getProjectId(),
36
+ username: userName,
37
+ databaseName: "admin",
38
+ },
39
+ },
40
+ });
41
+ } catch (error) {
42
+ // Ignore 404 errors when deleting the user
43
+ if (!(error instanceof ApiClientError) || error.response?.status !== 404) {
44
+ throw error;
45
+ }
46
+ }
22
47
  });
23
48
 
24
49
  describe("atlas-create-db-user", () => {
@@ -34,26 +59,24 @@ describeWithAtlas("db users", (integration) => {
34
59
  expect(createDbUser.inputSchema.properties).toHaveProperty("roles");
35
60
  expect(createDbUser.inputSchema.properties).toHaveProperty("clusters");
36
61
  });
37
- it("should create a database user", async () => {
38
- const projectId = getProjectId();
39
62
 
40
- const response = (await integration.mcpClient().callTool({
41
- name: "atlas-create-db-user",
42
- arguments: {
43
- projectId,
44
- username: userName,
45
- password: "testpassword",
46
- roles: [
47
- {
48
- roleName: "readWrite",
49
- databaseName: "admin",
50
- },
51
- ],
52
- },
53
- })) as CallToolResult;
54
- expect(response.content).toBeArray();
55
- expect(response.content).toHaveLength(1);
56
- expect(response.content[0].text).toContain("created sucessfully");
63
+ it("should create a database user with supplied password", async () => {
64
+ const response = await createUserWithMCP("testpassword");
65
+
66
+ const elements = getResponseElements(response);
67
+ expect(elements).toHaveLength(1);
68
+ expect(elements[0]?.text).toContain("created successfully");
69
+ expect(elements[0]?.text).toContain(userName);
70
+ expect(elements[0]?.text).not.toContain("testpassword");
71
+ });
72
+
73
+ it("should create a database user with generated password", async () => {
74
+ const response = await createUserWithMCP();
75
+ const elements = getResponseElements(response);
76
+ expect(elements).toHaveLength(1);
77
+ expect(elements[0]?.text).toContain("created successfully");
78
+ expect(elements[0]?.text).toContain(userName);
79
+ expect(elements[0]?.text).toContain("with password: `");
57
80
  });
58
81
  });
59
82
  describe("atlas-list-db-users", () => {
@@ -68,12 +91,14 @@ describeWithAtlas("db users", (integration) => {
68
91
  it("returns database users by project", async () => {
69
92
  const projectId = getProjectId();
70
93
 
94
+ await createUserWithMCP();
95
+
71
96
  const response = (await integration
72
97
  .mcpClient()
73
98
  .callTool({ name: "atlas-list-db-users", arguments: { projectId } })) as CallToolResult;
74
99
  expect(response.content).toBeArray();
75
100
  expect(response.content).toHaveLength(1);
76
- expect(response.content[0].text).toContain(userName);
101
+ expect(response.content[0]?.text).toContain(userName);
77
102
  });
78
103
  });
79
104
  });
@@ -16,9 +16,9 @@ describeWithAtlas("orgs", (integration) => {
16
16
  .callTool({ name: "atlas-list-orgs", arguments: {} })) as CallToolResult;
17
17
  expect(response.content).toBeArray();
18
18
  expect(response.content).toHaveLength(1);
19
- const data = parseTable(response.content[0].text as string);
19
+ const data = parseTable(response.content[0]?.text as string);
20
20
  expect(data).toHaveLength(1);
21
- expect(data[0]["Organization Name"]).toEqual("MongoDB MCP Test");
21
+ expect(data[0]?.["Organization Name"]).toEqual("MongoDB MCP Test");
22
22
  });
23
23
  });
24
24
  });
@@ -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
  }
@@ -2,8 +2,6 @@ import { describeWithMongoDB } from "../mongodbHelpers.js";
2
2
  import { getResponseContent, validateThrowsForInvalidArguments, validateToolMetadata } from "../../../helpers.js";
3
3
  import { config } from "../../../../../src/config.js";
4
4
 
5
- // These tests are temporarily skipped because the connect tool is disabled for the initial release.
6
- // TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled
7
5
  describeWithMongoDB(
8
6
  "switchConnection tool",
9
7
  (integration) => {
@@ -77,8 +75,7 @@ describeWithMongoDB(
77
75
  (mdbIntegration) => ({
78
76
  ...config,
79
77
  connectionString: mdbIntegration.connectionString(),
80
- }),
81
- describe.skip
78
+ })
82
79
  );
83
80
  describeWithMongoDB(
84
81
  "Connect tool",
@@ -127,6 +124,5 @@ describeWithMongoDB(
127
124
  });
128
125
  });
129
126
  },
130
- () => config,
131
- describe.skip
127
+ () => config
132
128
  );
@@ -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
  });
@@ -17,42 +17,32 @@ interface MongoDBIntegrationTest {
17
17
  export function describeWithMongoDB(
18
18
  name: string,
19
19
  fn: (integration: IntegrationTest & MongoDBIntegrationTest & { connectMcpClient: () => Promise<void> }) => void,
20
- getUserConfig: (mdbIntegration: MongoDBIntegrationTest) => UserConfig = () => defaultTestConfig,
21
- describeFn = describe
20
+ getUserConfig: (mdbIntegration: MongoDBIntegrationTest) => UserConfig = () => defaultTestConfig
22
21
  ) {
23
- describeFn(name, () => {
22
+ describe(name, () => {
24
23
  const mdbIntegration = setupMongoDBIntegrationTest();
25
24
  const integration = setupIntegrationTest(() => ({
26
25
  ...getUserConfig(mdbIntegration),
27
- connectionString: mdbIntegration.connectionString(),
28
26
  }));
29
27
 
30
- beforeEach(() => {
31
- integration.mcpServer().userConfig.connectionString = mdbIntegration.connectionString();
32
- });
33
-
34
28
  fn({
35
29
  ...integration,
36
30
  ...mdbIntegration,
37
31
  connectMcpClient: async () => {
38
- // TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when
39
- // the connect tool is reenabled
40
- // await integration.mcpClient().callTool({
41
- // name: "connect",
42
- // arguments: { connectionString: mdbIntegration.connectionString() },
43
- // });
32
+ const { tools } = await integration.mcpClient().listTools();
33
+ if (tools.find((tool) => tool.name === "connect")) {
34
+ await integration.mcpClient().callTool({
35
+ name: "connect",
36
+ arguments: { connectionString: mdbIntegration.connectionString() },
37
+ });
38
+ }
44
39
  },
45
40
  });
46
41
  });
47
42
  }
48
43
 
49
44
  export function setupMongoDBIntegrationTest(): MongoDBIntegrationTest {
50
- let mongoCluster: // TODO: Fix this type once mongodb-runner is updated.
51
- | {
52
- connectionString: string;
53
- close: () => Promise<void>;
54
- }
55
- | undefined;
45
+ let mongoCluster: MongoCluster | undefined;
56
46
  let mongoClient: MongoClient | undefined;
57
47
  let randomDbName: string;
58
48
 
@@ -76,8 +66,6 @@ export function setupMongoDBIntegrationTest(): MongoDBIntegrationTest {
76
66
  let dbsDir = path.join(tmpDir, "mongodb-runner", "dbs");
77
67
  for (let i = 0; i < 10; i++) {
78
68
  try {
79
- // TODO: Fix this type once mongodb-runner is updated.
80
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
81
69
  mongoCluster = await MongoCluster.start({
82
70
  tmpDir: dbsDir,
83
71
  logDir: path.join(tmpDir, "mongodb-runner", "logs"),
@@ -141,12 +129,15 @@ export function validateAutoConnectBehavior(
141
129
  },
142
130
  beforeEachImpl?: () => Promise<void>
143
131
  ): void {
144
- // TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled
145
- describe.skip("when not connected", () => {
132
+ describe("when not connected", () => {
146
133
  if (beforeEachImpl) {
147
134
  beforeEach(() => beforeEachImpl());
148
135
  }
149
136
 
137
+ afterEach(() => {
138
+ integration.mcpServer().userConfig.connectionString = undefined;
139
+ });
140
+
150
141
  it("connects automatically if connection string is configured", async () => {
151
142
  integration.mcpServer().userConfig.connectionString = integration.connectionString();
152
143