mongodb-mcp-server 0.0.5 → 0.0.7

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 (121) hide show
  1. package/.github/CODEOWNERS +3 -0
  2. package/.github/dependabot.yml +10 -0
  3. package/.github/workflows/code_health.yaml +9 -27
  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 +12 -4
  8. package/.github/workflows/publish.yaml +6 -3
  9. package/.prettierrc.json +1 -1
  10. package/README.md +16 -0
  11. package/dist/common/atlas/apiClient.js +1 -5
  12. package/dist/common/atlas/apiClient.js.map +1 -1
  13. package/dist/config.js +3 -1
  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 +2 -3
  18. package/dist/index.js.map +1 -1
  19. package/dist/logger.js +20 -1
  20. package/dist/logger.js.map +1 -1
  21. package/dist/server.js +44 -3
  22. package/dist/server.js.map +1 -1
  23. package/dist/session.js +50 -5
  24. package/dist/session.js.map +1 -1
  25. package/dist/telemetry/constants.js.map +1 -1
  26. package/dist/telemetry/telemetry.js +7 -6
  27. package/dist/telemetry/telemetry.js.map +1 -1
  28. package/dist/tools/atlas/{createAccessList.js → create/createAccessList.js} +1 -1
  29. package/dist/tools/atlas/create/createAccessList.js.map +1 -0
  30. package/dist/tools/atlas/{createDBUser.js → create/createDBUser.js} +1 -1
  31. package/dist/tools/atlas/create/createDBUser.js.map +1 -0
  32. package/dist/tools/atlas/{createFreeCluster.js → create/createFreeCluster.js} +5 -2
  33. package/dist/tools/atlas/create/createFreeCluster.js.map +1 -0
  34. package/dist/tools/atlas/{createProject.js → create/createProject.js} +1 -1
  35. package/dist/tools/atlas/create/createProject.js.map +1 -0
  36. package/dist/tools/atlas/metadata/connectCluster.js +97 -0
  37. package/dist/tools/atlas/metadata/connectCluster.js.map +1 -0
  38. package/dist/tools/atlas/{inspectAccessList.js → read/inspectAccessList.js} +1 -1
  39. package/dist/tools/atlas/read/inspectAccessList.js.map +1 -0
  40. package/dist/tools/atlas/{inspectCluster.js → read/inspectCluster.js} +1 -1
  41. package/dist/tools/atlas/read/inspectCluster.js.map +1 -0
  42. package/dist/tools/atlas/{listClusters.js → read/listClusters.js} +1 -1
  43. package/dist/tools/atlas/read/listClusters.js.map +1 -0
  44. package/dist/tools/atlas/{listDBUsers.js → read/listDBUsers.js} +1 -1
  45. package/dist/tools/atlas/read/listDBUsers.js.map +1 -0
  46. package/dist/tools/atlas/{listOrgs.js → read/listOrgs.js} +1 -1
  47. package/dist/tools/atlas/read/listOrgs.js.map +1 -0
  48. package/dist/tools/atlas/{listProjects.js → read/listProjects.js} +1 -1
  49. package/dist/tools/atlas/read/listProjects.js.map +1 -0
  50. package/dist/tools/atlas/tools.js +12 -10
  51. package/dist/tools/atlas/tools.js.map +1 -1
  52. package/dist/tools/mongodb/create/insertMany.js +1 -1
  53. package/dist/tools/mongodb/create/insertMany.js.map +1 -1
  54. package/dist/tools/mongodb/delete/deleteMany.js +1 -2
  55. package/dist/tools/mongodb/delete/deleteMany.js.map +1 -1
  56. package/dist/tools/mongodb/metadata/connect.js +59 -71
  57. package/dist/tools/mongodb/metadata/connect.js.map +1 -1
  58. package/dist/tools/mongodb/mongodbTool.js +37 -30
  59. package/dist/tools/mongodb/mongodbTool.js.map +1 -1
  60. package/dist/tools/mongodb/read/aggregate.js +1 -1
  61. package/dist/tools/mongodb/read/aggregate.js.map +1 -1
  62. package/dist/tools/mongodb/read/count.js +1 -2
  63. package/dist/tools/mongodb/read/count.js.map +1 -1
  64. package/dist/tools/mongodb/read/find.js +2 -4
  65. package/dist/tools/mongodb/read/find.js.map +1 -1
  66. package/dist/tools/mongodb/tools.js +4 -2
  67. package/dist/tools/mongodb/tools.js.map +1 -1
  68. package/dist/tools/mongodb/update/updateMany.js +2 -4
  69. package/dist/tools/mongodb/update/updateMany.js.map +1 -1
  70. package/dist/tools/tool.js +30 -6
  71. package/dist/tools/tool.js.map +1 -1
  72. package/package.json +3 -3
  73. package/src/common/atlas/apiClient.ts +3 -11
  74. package/src/config.ts +13 -8
  75. package/src/errors.ts +1 -1
  76. package/src/index.ts +3 -3
  77. package/src/logger.ts +26 -1
  78. package/src/server.ts +60 -5
  79. package/src/session.ts +69 -6
  80. package/src/telemetry/constants.ts +2 -2
  81. package/src/telemetry/telemetry.ts +9 -21
  82. package/src/telemetry/types.ts +28 -11
  83. package/src/tools/atlas/{createAccessList.ts → create/createAccessList.ts} +2 -2
  84. package/src/tools/atlas/{createDBUser.ts → create/createDBUser.ts} +3 -3
  85. package/src/tools/atlas/{createFreeCluster.ts → create/createFreeCluster.ts} +7 -4
  86. package/src/tools/atlas/{createProject.ts → create/createProject.ts} +3 -3
  87. package/src/tools/atlas/metadata/connectCluster.ts +114 -0
  88. package/src/tools/atlas/{inspectAccessList.ts → read/inspectAccessList.ts} +2 -2
  89. package/src/tools/atlas/{inspectCluster.ts → read/inspectCluster.ts} +3 -3
  90. package/src/tools/atlas/{listClusters.ts → read/listClusters.ts} +3 -3
  91. package/src/tools/atlas/{listDBUsers.ts → read/listDBUsers.ts} +3 -3
  92. package/src/tools/atlas/{listOrgs.ts → read/listOrgs.ts} +2 -2
  93. package/src/tools/atlas/{listProjects.ts → read/listProjects.ts} +3 -3
  94. package/src/tools/atlas/tools.ts +12 -10
  95. package/src/tools/mongodb/create/insertMany.ts +1 -1
  96. package/src/tools/mongodb/delete/deleteMany.ts +1 -2
  97. package/src/tools/mongodb/metadata/connect.ts +78 -75
  98. package/src/tools/mongodb/mongodbTool.ts +40 -30
  99. package/src/tools/mongodb/read/aggregate.ts +1 -1
  100. package/src/tools/mongodb/read/count.ts +1 -2
  101. package/src/tools/mongodb/read/find.ts +2 -4
  102. package/src/tools/mongodb/tools.ts +4 -2
  103. package/src/tools/mongodb/update/updateMany.ts +2 -4
  104. package/src/tools/tool.ts +40 -13
  105. package/tests/integration/helpers.ts +13 -3
  106. package/tests/integration/server.test.ts +46 -20
  107. package/tests/integration/tools/atlas/atlasHelpers.ts +7 -5
  108. package/tests/integration/tools/atlas/clusters.test.ts +68 -4
  109. package/tests/integration/tools/mongodb/metadata/connect.test.ts +85 -100
  110. package/tests/integration/tools/mongodb/mongodbHelpers.ts +32 -21
  111. package/tests/unit/telemetry.test.ts +200 -0
  112. package/dist/tools/atlas/createAccessList.js.map +0 -1
  113. package/dist/tools/atlas/createDBUser.js.map +0 -1
  114. package/dist/tools/atlas/createFreeCluster.js.map +0 -1
  115. package/dist/tools/atlas/createProject.js.map +0 -1
  116. package/dist/tools/atlas/inspectAccessList.js.map +0 -1
  117. package/dist/tools/atlas/inspectCluster.js.map +0 -1
  118. package/dist/tools/atlas/listClusters.js.map +0 -1
  119. package/dist/tools/atlas/listDBUsers.js.map +0 -1
  120. package/dist/tools/atlas/listOrgs.js.map +0 -1
  121. package/dist/tools/atlas/listProjects.js.map +0 -1
@@ -2,16 +2,18 @@ import { ObjectId } from "mongodb";
2
2
  import { Group } from "../../../../src/common/atlas/openapi.js";
3
3
  import { ApiClient } from "../../../../src/common/atlas/apiClient.js";
4
4
  import { setupIntegrationTest, IntegrationTest } from "../../helpers.js";
5
+ import { config } from "../../../../src/config.js";
5
6
 
6
7
  export type IntegrationTestFunction = (integration: IntegrationTest) => void;
7
8
 
8
- export function sleep(ms: number) {
9
- return new Promise((resolve) => setTimeout(resolve, ms));
10
- }
11
-
12
9
  export function describeWithAtlas(name: string, fn: IntegrationTestFunction) {
13
10
  const testDefinition = () => {
14
- const integration = setupIntegrationTest();
11
+ const integration = setupIntegrationTest(() => ({
12
+ ...config,
13
+ apiClientId: process.env.MDB_MCP_API_CLIENT_ID,
14
+ apiClientSecret: process.env.MDB_MCP_API_CLIENT_SECRET,
15
+ }));
16
+
15
17
  describe(name, () => {
16
18
  fn(integration);
17
19
  });
@@ -1,14 +1,18 @@
1
1
  import { Session } from "../../../../src/session.js";
2
2
  import { expectDefined } from "../../helpers.js";
3
- import { describeWithAtlas, withProject, sleep, randomId } from "./atlasHelpers.js";
3
+ import { describeWithAtlas, withProject, randomId } from "./atlasHelpers.js";
4
4
  import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
5
5
 
6
+ function sleep(ms: number) {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+
6
10
  async function deleteAndWaitCluster(session: Session, projectId: string, clusterName: string) {
7
11
  await session.apiClient.deleteCluster({
8
12
  params: {
9
13
  path: {
10
14
  groupId: projectId,
11
- clusterName: clusterName,
15
+ clusterName,
12
16
  },
13
17
  },
14
18
  });
@@ -18,7 +22,7 @@ async function deleteAndWaitCluster(session: Session, projectId: string, cluster
18
22
  params: {
19
23
  path: {
20
24
  groupId: projectId,
21
- clusterName: clusterName,
25
+ clusterName,
22
26
  },
23
27
  },
24
28
  });
@@ -29,6 +33,23 @@ async function deleteAndWaitCluster(session: Session, projectId: string, cluster
29
33
  }
30
34
  }
31
35
 
36
+ async function waitClusterState(session: Session, projectId: string, clusterName: string, state: string) {
37
+ while (true) {
38
+ const cluster = await session.apiClient.getCluster({
39
+ params: {
40
+ path: {
41
+ groupId: projectId,
42
+ clusterName,
43
+ },
44
+ },
45
+ });
46
+ if (cluster?.stateName === state) {
47
+ return;
48
+ }
49
+ await sleep(1000);
50
+ }
51
+ }
52
+
32
53
  describeWithAtlas("clusters", (integration) => {
33
54
  withProject(integration, ({ getProjectId }) => {
34
55
  const clusterName = "ClusterTest-" + randomId;
@@ -66,7 +87,7 @@ describeWithAtlas("clusters", (integration) => {
66
87
  },
67
88
  })) as CallToolResult;
68
89
  expect(response.content).toBeArray();
69
- expect(response.content).toHaveLength(1);
90
+ expect(response.content).toHaveLength(2);
70
91
  expect(response.content[0].text).toContain("has been created");
71
92
  });
72
93
  });
@@ -117,5 +138,48 @@ describeWithAtlas("clusters", (integration) => {
117
138
  expect(response.content[1].text).toContain(`${clusterName} | `);
118
139
  });
119
140
  });
141
+
142
+ describe("atlas-connect-cluster", () => {
143
+ beforeAll(async () => {
144
+ const projectId = getProjectId();
145
+ await waitClusterState(integration.mcpServer().session, projectId, clusterName, "IDLE");
146
+ await integration.mcpServer().session.apiClient.createProjectIpAccessList({
147
+ params: {
148
+ path: {
149
+ groupId: projectId,
150
+ },
151
+ },
152
+ body: [
153
+ {
154
+ comment: "MCP test",
155
+ cidrBlock: "0.0.0.0/0",
156
+ },
157
+ ],
158
+ });
159
+ });
160
+
161
+ it("should have correct metadata", async () => {
162
+ const { tools } = await integration.mcpClient().listTools();
163
+ const connectCluster = tools.find((tool) => tool.name === "atlas-connect-cluster");
164
+
165
+ expectDefined(connectCluster);
166
+ expect(connectCluster.inputSchema.type).toBe("object");
167
+ expectDefined(connectCluster.inputSchema.properties);
168
+ expect(connectCluster.inputSchema.properties).toHaveProperty("projectId");
169
+ expect(connectCluster.inputSchema.properties).toHaveProperty("clusterName");
170
+ });
171
+
172
+ it("connects to cluster", async () => {
173
+ const projectId = getProjectId();
174
+
175
+ const response = (await integration.mcpClient().callTool({
176
+ name: "atlas-connect-cluster",
177
+ arguments: { projectId, clusterName },
178
+ })) as CallToolResult;
179
+ expect(response.content).toBeArray();
180
+ expect(response.content).toHaveLength(1);
181
+ expect(response.content[0].text).toContain(`Connected to cluster "${clusterName}"`);
182
+ });
183
+ });
120
184
  });
121
185
  });
@@ -1,147 +1,132 @@
1
1
  import { describeWithMongoDB } from "../mongodbHelpers.js";
2
-
3
- import { getResponseContent, validateToolMetadata } from "../../../helpers.js";
4
-
2
+ import { getResponseContent, validateThrowsForInvalidArguments, validateToolMetadata } from "../../../helpers.js";
5
3
  import { config } from "../../../../../src/config.js";
6
4
 
7
- describeWithMongoDB("Connect tool", (integration) => {
8
- validateToolMetadata(integration, "connect", "Connect to a MongoDB instance", [
9
- {
10
- name: "options",
11
- description:
12
- "Options for connecting to MongoDB. If not provided, the connection string from the config://connection-string resource will be used. If the user hasn't specified Atlas cluster name or a connection string explicitly and the `config://connection-string` resource is present, always invoke this with no arguments.",
13
- type: "array",
14
- required: false,
15
- },
16
- ]);
17
-
18
- describe("without arguments", () => {
19
- it("prompts for connection string if not set", async () => {
20
- const response = await integration.mcpClient().callTool({ name: "connect" });
21
- const content = getResponseContent(response.content);
22
- expect(content).toContain("No connection details provided");
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
+ describeWithMongoDB(
8
+ "switchConnection tool",
9
+ (integration) => {
10
+ beforeEach(() => {
11
+ integration.mcpServer().userConfig.connectionString = integration.connectionString();
23
12
  });
24
13
 
25
- it("connects to the database if connection string is set", async () => {
26
- config.connectionString = integration.connectionString();
27
-
28
- const response = await integration.mcpClient().callTool({ name: "connect" });
29
- const content = getResponseContent(response.content);
30
- expect(content).toContain("Successfully connected");
31
- expect(content).toContain(integration.connectionString());
32
- });
33
- });
14
+ validateToolMetadata(
15
+ integration,
16
+ "switch-connection",
17
+ "Switch to a different MongoDB connection. If the user has configured a connection string or has previously called the connect tool, a connection is already established and there's no need to call this tool unless the user has explicitly requested to switch to a new instance.",
18
+ [
19
+ {
20
+ name: "connectionString",
21
+ description: "MongoDB connection string to switch to (in the mongodb:// or mongodb+srv:// format)",
22
+ type: "string",
23
+ required: false,
24
+ },
25
+ ]
26
+ );
34
27
 
35
- describe("with default config", () => {
36
- describe("without connection string", () => {
37
- it("prompts for connection string", async () => {
38
- const response = await integration.mcpClient().callTool({ name: "connect", arguments: {} });
39
- const content = getResponseContent(response.content);
40
- expect(content).toContain("No connection details provided");
41
- });
42
- });
28
+ validateThrowsForInvalidArguments(integration, "switch-connection", [{ connectionString: 123 }]);
43
29
 
44
- describe("with connection string", () => {
30
+ describe("without arguments", () => {
45
31
  it("connects to the database", async () => {
46
- const response = await integration.mcpClient().callTool({
47
- name: "connect",
48
- arguments: {
49
- options: [
50
- {
51
- connectionString: integration.connectionString(),
52
- },
53
- ],
54
- },
55
- });
32
+ const response = await integration.mcpClient().callTool({ name: "switch-connection" });
56
33
  const content = getResponseContent(response.content);
57
34
  expect(content).toContain("Successfully connected");
58
- expect(content).toContain(integration.connectionString());
59
35
  });
60
36
  });
61
37
 
62
- describe("with invalid connection string", () => {
63
- it("returns error message", async () => {
64
- const response = await integration.mcpClient().callTool({
65
- name: "connect",
66
- arguments: { options: [{ connectionString: "mongodb://localhost:12345" }] },
67
- });
68
- const content = getResponseContent(response.content);
69
- expect(content).toContain("Error running connect");
70
-
71
- // Should not suggest using the config connection string (because we don't have one)
72
- expect(content).not.toContain("Your config lists a different connection string");
73
- });
38
+ it("doesn't have the connect tool registered", async () => {
39
+ const { tools } = await integration.mcpClient().listTools();
40
+ const tool = tools.find((tool) => tool.name === "connect");
41
+ expect(tool).toBeUndefined();
74
42
  });
75
- });
76
43
 
77
- describe("with connection string in config", () => {
78
- beforeEach(() => {
79
- config.connectionString = integration.connectionString();
80
- });
81
-
82
- it("uses the connection string from config", async () => {
83
- const response = await integration.mcpClient().callTool({ name: "connect", arguments: {} });
44
+ it("defaults to the connection string from config", async () => {
45
+ const response = await integration.mcpClient().callTool({ name: "switch-connection", arguments: {} });
84
46
  const content = getResponseContent(response.content);
85
47
  expect(content).toContain("Successfully connected");
86
- expect(content).toContain(integration.connectionString());
87
48
  });
88
49
 
89
- it("prefers connection string from arguments", async () => {
50
+ it("switches to the connection string from the arguments", async () => {
90
51
  const newConnectionString = `${integration.connectionString()}?appName=foo-bar`;
91
52
  const response = await integration.mcpClient().callTool({
92
- name: "connect",
53
+ name: "switch-connection",
93
54
  arguments: {
94
- options: [
95
- {
96
- connectionString: newConnectionString,
97
- },
98
- ],
55
+ connectionString: newConnectionString,
99
56
  },
100
57
  });
101
58
  const content = getResponseContent(response.content);
102
59
  expect(content).toContain("Successfully connected");
103
- expect(content).toContain(newConnectionString);
104
60
  });
105
61
 
106
62
  describe("when the arugment connection string is invalid", () => {
107
- it("suggests the config connection string if set", async () => {
63
+ it("returns error message", async () => {
108
64
  const response = await integration.mcpClient().callTool({
109
- name: "connect",
65
+ name: "switch-connection",
110
66
  arguments: {
111
- options: [
112
- {
113
- connectionString: "mongodb://localhost:12345",
114
- },
115
- ],
67
+ connectionString: "mongodb://localhost:12345",
116
68
  },
117
69
  });
70
+
118
71
  const content = getResponseContent(response.content);
119
- expect(content).toContain("Failed to connect to MongoDB at 'mongodb://localhost:12345'");
120
- expect(content).toContain(
121
- `Your config lists a different connection string: '${config.connectionString}' - do you want to try connecting to it instead?`
122
- );
72
+
73
+ expect(content).toContain("Error running switch-connection");
123
74
  });
75
+ });
76
+ },
77
+ (mdbIntegration) => ({
78
+ ...config,
79
+ connectionString: mdbIntegration.connectionString(),
80
+ }),
81
+ describe.skip
82
+ );
83
+ describeWithMongoDB(
84
+ "Connect tool",
85
+ (integration) => {
86
+ validateToolMetadata(integration, "connect", "Connect to a MongoDB instance", [
87
+ {
88
+ name: "connectionString",
89
+ description: "MongoDB connection string (in the mongodb:// or mongodb+srv:// format)",
90
+ type: "string",
91
+ required: true,
92
+ },
93
+ ]);
94
+
95
+ validateThrowsForInvalidArguments(integration, "connect", [{}, { connectionString: 123 }]);
96
+
97
+ it("doesn't have the switch-connection tool registered", async () => {
98
+ const { tools } = await integration.mcpClient().listTools();
99
+ const tool = tools.find((tool) => tool.name === "switch-connection");
100
+ expect(tool).toBeUndefined();
101
+ });
124
102
 
125
- it("returns error message if the config connection string matches the argument", async () => {
126
- config.connectionString = "mongodb://localhost:12345";
103
+ describe("with connection string", () => {
104
+ it("connects to the database", async () => {
127
105
  const response = await integration.mcpClient().callTool({
128
106
  name: "connect",
129
107
  arguments: {
130
- options: [
131
- {
132
- connectionString: "mongodb://localhost:12345",
133
- },
134
- ],
108
+ connectionString: integration.connectionString(),
135
109
  },
136
110
  });
137
-
138
111
  const content = getResponseContent(response.content);
112
+ expect(content).toContain("Successfully connected");
113
+ });
114
+ });
139
115
 
140
- // Should be handled by default error handler and not suggest the config connection string
141
- // because it matches the argument connection string
116
+ describe("with invalid connection string", () => {
117
+ it("returns error message", async () => {
118
+ const response = await integration.mcpClient().callTool({
119
+ name: "connect",
120
+ arguments: { connectionString: "mongodb://localhost:12345" },
121
+ });
122
+ const content = getResponseContent(response.content);
142
123
  expect(content).toContain("Error running connect");
124
+
125
+ // Should not suggest using the config connection string (because we don't have one)
143
126
  expect(content).not.toContain("Your config lists a different connection string");
144
127
  });
145
128
  });
146
- });
147
- });
129
+ },
130
+ () => config,
131
+ describe.skip
132
+ );
@@ -3,29 +3,47 @@ import path from "path";
3
3
  import fs from "fs/promises";
4
4
  import { MongoClient, ObjectId } from "mongodb";
5
5
  import { getResponseContent, IntegrationTest, setupIntegrationTest } from "../../helpers.js";
6
- import { config } from "../../../../src/config.js";
6
+ import { config, UserConfig } from "../../../../src/config.js";
7
7
 
8
8
  interface MongoDBIntegrationTest {
9
9
  mongoClient: () => MongoClient;
10
10
  connectionString: () => string;
11
- connectMcpClient: () => Promise<void>;
12
11
  randomDbName: () => string;
13
12
  }
14
13
 
15
14
  export function describeWithMongoDB(
16
15
  name: string,
17
- fn: (integration: IntegrationTest & MongoDBIntegrationTest) => void
18
- ): void {
19
- describe("mongodb", () => {
20
- const integration = setupIntegrationTest();
21
- const mdbIntegration = setupMongoDBIntegrationTest(integration);
22
- describe(name, () => {
23
- fn({ ...integration, ...mdbIntegration });
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
+ },
24
42
  });
25
43
  });
26
44
  }
27
45
 
28
- export function setupMongoDBIntegrationTest(integration: IntegrationTest): MongoDBIntegrationTest {
46
+ export function setupMongoDBIntegrationTest(): MongoDBIntegrationTest {
29
47
  let mongoCluster: // TODO: Fix this type once mongodb-runner is updated.
30
48
  | {
31
49
  connectionString: string;
@@ -40,9 +58,6 @@ export function setupMongoDBIntegrationTest(integration: IntegrationTest): Mongo
40
58
  });
41
59
 
42
60
  afterEach(async () => {
43
- await integration.mcpServer().session.close();
44
- config.connectionString = undefined;
45
-
46
61
  await mongoClient?.close();
47
62
  mongoClient = undefined;
48
63
  });
@@ -108,12 +123,7 @@ export function setupMongoDBIntegrationTest(integration: IntegrationTest): Mongo
108
123
  return mongoClient;
109
124
  },
110
125
  connectionString: getConnectionString,
111
- connectMcpClient: async () => {
112
- await integration.mcpClient().callTool({
113
- name: "connect",
114
- arguments: { options: [{ connectionString: getConnectionString() }] },
115
- });
116
- },
126
+
117
127
  randomDbName: () => randomDbName,
118
128
  };
119
129
  }
@@ -128,13 +138,14 @@ export function validateAutoConnectBehavior(
128
138
  },
129
139
  beforeEachImpl?: () => Promise<void>
130
140
  ): void {
131
- describe("when not connected", () => {
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", () => {
132
143
  if (beforeEachImpl) {
133
144
  beforeEach(() => beforeEachImpl());
134
145
  }
135
146
 
136
147
  it("connects automatically if connection string is configured", async () => {
137
- config.connectionString = integration.connectionString();
148
+ integration.mcpServer().userConfig.connectionString = integration.connectionString();
138
149
 
139
150
  const validationInfo = validation();
140
151
 
@@ -0,0 +1,200 @@
1
+ import { ApiClient } from "../../src/common/atlas/apiClient.js";
2
+ import { Session } from "../../src/session.js";
3
+ import { Telemetry } from "../../src/telemetry/telemetry.js";
4
+ import { BaseEvent, TelemetryResult } from "../../src/telemetry/types.js";
5
+ import { EventCache } from "../../src/telemetry/eventCache.js";
6
+ import { config } from "../../src/config.js";
7
+
8
+ // Mock the ApiClient to avoid real API calls
9
+ jest.mock("../../src/common/atlas/apiClient.js");
10
+ const MockApiClient = ApiClient as jest.MockedClass<typeof ApiClient>;
11
+
12
+ // Mock EventCache to control and verify caching behavior
13
+ jest.mock("../../src/telemetry/eventCache.js");
14
+ const MockEventCache = EventCache as jest.MockedClass<typeof EventCache>;
15
+
16
+ describe("Telemetry", () => {
17
+ let mockApiClient: jest.Mocked<ApiClient>;
18
+ let mockEventCache: jest.Mocked<EventCache>;
19
+ let session: Session;
20
+ let telemetry: Telemetry;
21
+
22
+ // Helper function to create properly typed test events
23
+ function createTestEvent(options?: {
24
+ source?: string;
25
+ result?: TelemetryResult;
26
+ component?: string;
27
+ category?: string;
28
+ command?: string;
29
+ duration_ms?: number;
30
+ }): BaseEvent {
31
+ return {
32
+ timestamp: new Date().toISOString(),
33
+ source: options?.source || "mdbmcp",
34
+ properties: {
35
+ component: options?.component || "test-component",
36
+ duration_ms: options?.duration_ms || 100,
37
+ result: options?.result || "success",
38
+ category: options?.category || "test",
39
+ command: options?.command || "test-command",
40
+ },
41
+ };
42
+ }
43
+
44
+ // Helper function to verify mock calls to reduce duplication
45
+ function verifyMockCalls({
46
+ sendEventsCalls = 0,
47
+ clearEventsCalls = 0,
48
+ appendEventsCalls = 0,
49
+ sendEventsCalledWith = undefined,
50
+ appendEventsCalledWith = undefined,
51
+ } = {}) {
52
+ const { calls: sendEvents } = mockApiClient.sendEvents.mock;
53
+ const { calls: clearEvents } = mockEventCache.clearEvents.mock;
54
+ const { calls: appendEvents } = mockEventCache.appendEvents.mock;
55
+
56
+ expect(sendEvents.length).toBe(sendEventsCalls);
57
+ expect(clearEvents.length).toBe(clearEventsCalls);
58
+ expect(appendEvents.length).toBe(appendEventsCalls);
59
+
60
+ if (sendEventsCalledWith) {
61
+ expect(sendEvents[0]?.[0]).toEqual(sendEventsCalledWith);
62
+ }
63
+
64
+ if (appendEventsCalledWith) {
65
+ expect(appendEvents[0]?.[0]).toEqual(appendEventsCalledWith);
66
+ }
67
+ }
68
+
69
+ beforeEach(() => {
70
+ // Reset mocks before each test
71
+ jest.clearAllMocks();
72
+
73
+ // Setup mocked API client
74
+ mockApiClient = new MockApiClient() as jest.Mocked<ApiClient>;
75
+ mockApiClient.sendEvents = jest.fn().mockResolvedValue(undefined);
76
+ mockApiClient.hasCredentials = jest.fn().mockReturnValue(true);
77
+
78
+ // Setup mocked EventCache
79
+ mockEventCache = new MockEventCache() as jest.Mocked<EventCache>;
80
+ mockEventCache.getEvents = jest.fn().mockReturnValue([]);
81
+ mockEventCache.clearEvents = jest.fn().mockResolvedValue(undefined);
82
+ mockEventCache.appendEvents = jest.fn().mockResolvedValue(undefined);
83
+ MockEventCache.getInstance = jest.fn().mockReturnValue(mockEventCache);
84
+
85
+ // Create a simplified session with our mocked API client
86
+ session = {
87
+ apiClient: mockApiClient,
88
+ sessionId: "test-session-id",
89
+ agentRunner: { name: "test-agent", version: "1.0.0" } as const,
90
+ close: jest.fn().mockResolvedValue(undefined),
91
+ setAgentRunner: jest.fn().mockResolvedValue(undefined),
92
+ } as unknown as Session;
93
+
94
+ // Create the telemetry instance with mocked dependencies
95
+ telemetry = new Telemetry(session, mockEventCache);
96
+
97
+ config.telemetry = "enabled";
98
+ });
99
+
100
+ describe("when telemetry is enabled", () => {
101
+ it("should send events successfully", async () => {
102
+ const testEvent = createTestEvent();
103
+
104
+ await telemetry.emitEvents([testEvent]);
105
+
106
+ verifyMockCalls({
107
+ sendEventsCalls: 1,
108
+ clearEventsCalls: 1,
109
+ sendEventsCalledWith: [testEvent],
110
+ });
111
+ });
112
+
113
+ it("should cache events when sending fails", async () => {
114
+ mockApiClient.sendEvents.mockRejectedValueOnce(new Error("API error"));
115
+
116
+ const testEvent = createTestEvent();
117
+
118
+ await telemetry.emitEvents([testEvent]);
119
+
120
+ verifyMockCalls({
121
+ sendEventsCalls: 1,
122
+ appendEventsCalls: 1,
123
+ appendEventsCalledWith: [testEvent],
124
+ });
125
+ });
126
+
127
+ it("should include cached events when sending", async () => {
128
+ const cachedEvent = createTestEvent({
129
+ command: "cached-command",
130
+ component: "cached-component",
131
+ });
132
+
133
+ const newEvent = createTestEvent({
134
+ command: "new-command",
135
+ component: "new-component",
136
+ });
137
+
138
+ // Set up mock to return cached events
139
+ mockEventCache.getEvents.mockReturnValueOnce([cachedEvent]);
140
+
141
+ await telemetry.emitEvents([newEvent]);
142
+
143
+ verifyMockCalls({
144
+ sendEventsCalls: 1,
145
+ clearEventsCalls: 1,
146
+ sendEventsCalledWith: [cachedEvent, newEvent],
147
+ });
148
+ });
149
+ });
150
+
151
+ describe("when telemetry is disabled", () => {
152
+ beforeEach(() => {
153
+ config.telemetry = "disabled";
154
+ });
155
+
156
+ it("should not send events", async () => {
157
+ const testEvent = createTestEvent();
158
+
159
+ await telemetry.emitEvents([testEvent]);
160
+
161
+ verifyMockCalls();
162
+ });
163
+ });
164
+
165
+ it("should correctly add common properties to events", () => {
166
+ const commonProps = telemetry.getCommonProperties();
167
+
168
+ // Use explicit type assertion
169
+ const expectedProps: Record<string, string> = {
170
+ mcp_client_version: "1.0.0",
171
+ mcp_client_name: "test-agent",
172
+ session_id: "test-session-id",
173
+ config_atlas_auth: "true",
174
+ config_connection_string: expect.any(String) as unknown as string,
175
+ };
176
+
177
+ expect(commonProps).toMatchObject(expectedProps);
178
+ });
179
+
180
+ describe("when DO_NOT_TRACK environment variable is set", () => {
181
+ let originalEnv: string | undefined;
182
+
183
+ beforeEach(() => {
184
+ originalEnv = process.env.DO_NOT_TRACK;
185
+ process.env.DO_NOT_TRACK = "1";
186
+ });
187
+
188
+ afterEach(() => {
189
+ process.env.DO_NOT_TRACK = originalEnv;
190
+ });
191
+
192
+ it("should not send events", async () => {
193
+ const testEvent = createTestEvent();
194
+
195
+ await telemetry.emitEvents([testEvent]);
196
+
197
+ verifyMockCalls();
198
+ });
199
+ });
200
+ });