mongodb-mcp-server 0.0.7 → 0.0.8

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 (59) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +40 -0
  2. package/.github/workflows/code_health.yaml +21 -1
  3. package/.vscode/launch.json +1 -1
  4. package/README.md +139 -53
  5. package/dist/common/atlas/apiClient.js.map +1 -1
  6. package/dist/logger.js +3 -0
  7. package/dist/logger.js.map +1 -1
  8. package/dist/server.js +8 -19
  9. package/dist/server.js.map +1 -1
  10. package/dist/session.js +11 -12
  11. package/dist/session.js.map +1 -1
  12. package/dist/telemetry/constants.js +2 -2
  13. package/dist/telemetry/constants.js.map +1 -1
  14. package/dist/telemetry/device-id.js +20 -0
  15. package/dist/telemetry/device-id.js.map +1 -0
  16. package/dist/telemetry/telemetry.js +25 -28
  17. package/dist/telemetry/telemetry.js.map +1 -1
  18. package/dist/tools/atlas/atlasTool.js +32 -0
  19. package/dist/tools/atlas/atlasTool.js.map +1 -1
  20. package/dist/tools/atlas/metadata/connectCluster.js +21 -1
  21. package/dist/tools/atlas/metadata/connectCluster.js.map +1 -1
  22. package/dist/tools/atlas/read/inspectCluster.js +14 -3
  23. package/dist/tools/atlas/read/inspectCluster.js.map +1 -1
  24. package/dist/tools/atlas/read/listClusters.js +15 -4
  25. package/dist/tools/atlas/read/listClusters.js.map +1 -1
  26. package/dist/tools/mongodb/mongodbTool.js +10 -0
  27. package/dist/tools/mongodb/mongodbTool.js.map +1 -1
  28. package/dist/tools/tool.js +34 -25
  29. package/dist/tools/tool.js.map +1 -1
  30. package/eslint.config.js +2 -2
  31. package/{jest.config.js → jest.config.ts} +1 -1
  32. package/package.json +6 -5
  33. package/scripts/apply.ts +1 -0
  34. package/scripts/filter.ts +3 -0
  35. package/src/common/atlas/apiClient.ts +2 -2
  36. package/src/logger.ts +3 -0
  37. package/src/server.ts +9 -24
  38. package/src/session.ts +10 -11
  39. package/src/telemetry/constants.ts +2 -2
  40. package/src/telemetry/device-id.ts +21 -0
  41. package/src/telemetry/eventCache.ts +1 -1
  42. package/src/telemetry/telemetry.ts +33 -30
  43. package/src/telemetry/types.ts +27 -29
  44. package/src/tools/atlas/atlasTool.ts +46 -1
  45. package/src/tools/atlas/metadata/connectCluster.ts +30 -1
  46. package/src/tools/atlas/read/inspectCluster.ts +28 -3
  47. package/src/tools/atlas/read/listClusters.ts +32 -4
  48. package/src/tools/mongodb/mongodbTool.ts +15 -1
  49. package/src/tools/tool.ts +50 -26
  50. package/tests/integration/helpers.ts +6 -8
  51. package/tests/integration/inMemoryTransport.ts +3 -3
  52. package/tests/integration/server.test.ts +4 -5
  53. package/tests/integration/tools/atlas/atlasHelpers.ts +3 -4
  54. package/tests/integration/tools/mongodb/mongodbHelpers.ts +6 -3
  55. package/tests/unit/telemetry.test.ts +37 -9
  56. package/tsconfig.build.json +19 -0
  57. package/tsconfig.jest.json +1 -3
  58. package/tsconfig.json +5 -15
  59. package/tsconfig.lint.json +0 -8
package/src/tools/tool.ts CHANGED
@@ -11,6 +11,10 @@ export type ToolArgs<Args extends ZodRawShape> = z.objectOutputType<Args, ZodNev
11
11
 
12
12
  export type OperationType = "metadata" | "read" | "create" | "delete" | "update";
13
13
  export type ToolCategory = "mongodb" | "atlas";
14
+ export type TelemetryToolMetadata = {
15
+ projectId?: string;
16
+ orgId?: string;
17
+ };
14
18
 
15
19
  export abstract class ToolBase {
16
20
  protected abstract name: string;
@@ -31,29 +35,6 @@ export abstract class ToolBase {
31
35
  protected readonly telemetry: Telemetry
32
36
  ) {}
33
37
 
34
- /**
35
- * Creates and emits a tool telemetry event
36
- * @param startTime - Start time in milliseconds
37
- * @param result - Whether the command succeeded or failed
38
- * @param error - Optional error if the command failed
39
- */
40
- private async emitToolEvent(startTime: number, result: CallToolResult): Promise<void> {
41
- const duration = Date.now() - startTime;
42
- const event: ToolEvent = {
43
- timestamp: new Date().toISOString(),
44
- source: "mdbmcp",
45
- properties: {
46
- ...this.telemetry.getCommonProperties(),
47
- command: this.name,
48
- category: this.category,
49
- component: "tool",
50
- duration_ms: duration,
51
- result: result.isError ? "failure" : "success",
52
- },
53
- };
54
- await this.telemetry.emitEvents([event]);
55
- }
56
-
57
38
  public register(server: McpServer): void {
58
39
  if (!this.verifyAllowed()) {
59
40
  return;
@@ -65,12 +46,12 @@ export abstract class ToolBase {
65
46
  logger.debug(LogId.toolExecute, "tool", `Executing ${this.name} with args: ${JSON.stringify(args)}`);
66
47
 
67
48
  const result = await this.execute(...args);
68
- await this.emitToolEvent(startTime, result);
49
+ await this.emitToolEvent(startTime, result, ...args).catch(() => {});
69
50
  return result;
70
51
  } catch (error: unknown) {
71
52
  logger.error(LogId.toolExecuteFailure, "tool", `Error executing ${this.name}: ${error as string}`);
72
53
  const toolResult = await this.handleError(error, args[0] as ToolArgs<typeof this.argsShape>);
73
- await this.emitToolEvent(startTime, toolResult).catch(() => {});
54
+ await this.emitToolEvent(startTime, toolResult, ...args).catch(() => {});
74
55
  return toolResult;
75
56
  }
76
57
  };
@@ -145,9 +126,52 @@ export abstract class ToolBase {
145
126
  {
146
127
  type: "text",
147
128
  text: `Error running ${this.name}: ${error instanceof Error ? error.message : String(error)}`,
148
- isError: true,
149
129
  },
150
130
  ],
131
+ isError: true,
132
+ };
133
+ }
134
+
135
+ protected abstract resolveTelemetryMetadata(
136
+ ...args: Parameters<ToolCallback<typeof this.argsShape>>
137
+ ): TelemetryToolMetadata;
138
+
139
+ /**
140
+ * Creates and emits a tool telemetry event
141
+ * @param startTime - Start time in milliseconds
142
+ * @param result - Whether the command succeeded or failed
143
+ * @param args - The arguments passed to the tool
144
+ */
145
+ private async emitToolEvent(
146
+ startTime: number,
147
+ result: CallToolResult,
148
+ ...args: Parameters<ToolCallback<typeof this.argsShape>>
149
+ ): Promise<void> {
150
+ if (!this.telemetry.isTelemetryEnabled()) {
151
+ return;
152
+ }
153
+ const duration = Date.now() - startTime;
154
+ const metadata = this.resolveTelemetryMetadata(...args);
155
+ const event: ToolEvent = {
156
+ timestamp: new Date().toISOString(),
157
+ source: "mdbmcp",
158
+ properties: {
159
+ command: this.name,
160
+ category: this.category,
161
+ component: "tool",
162
+ duration_ms: duration,
163
+ result: result.isError ? "failure" : "success",
164
+ },
151
165
  };
166
+
167
+ if (metadata?.orgId) {
168
+ event.properties.org_id = metadata.orgId;
169
+ }
170
+
171
+ if (metadata?.projectId) {
172
+ event.properties.project_id = metadata.projectId;
173
+ }
174
+
175
+ await this.telemetry.emitEvents([event]);
152
176
  }
153
177
  }
@@ -5,6 +5,7 @@ import { UserConfig } from "../../src/config.js";
5
5
  import { McpError } from "@modelcontextprotocol/sdk/types.js";
6
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
7
  import { Session } from "../../src/session.js";
8
+ import { config } from "../../src/config.js";
8
9
 
9
10
  interface ParameterInfo {
10
11
  name: string;
@@ -19,6 +20,10 @@ export interface IntegrationTest {
19
20
  mcpClient: () => Client;
20
21
  mcpServer: () => Server;
21
22
  }
23
+ export const defaultTestConfig: UserConfig = {
24
+ ...config,
25
+ telemetry: "disabled",
26
+ };
22
27
 
23
28
  export function setupIntegrationTest(getUserConfig: () => UserConfig): IntegrationTest {
24
29
  let mcpClient: Client | undefined;
@@ -51,25 +56,18 @@ export function setupIntegrationTest(getUserConfig: () => UserConfig): Integrati
51
56
  apiClientSecret: userConfig.apiClientSecret,
52
57
  });
53
58
 
54
- userConfig.telemetry = "disabled";
55
59
  mcpServer = new Server({
56
60
  session,
57
61
  userConfig,
58
62
  mcpServer: new McpServer({
59
63
  name: "test-server",
60
- version: "1.2.3",
64
+ version: "5.2.3",
61
65
  }),
62
66
  });
63
67
  await mcpServer.connect(serverTransport);
64
68
  await mcpClient.connect(clientTransport);
65
69
  });
66
70
 
67
- beforeEach(() => {
68
- if (mcpServer) {
69
- mcpServer.userConfig.telemetry = "disabled";
70
- }
71
- });
72
-
73
71
  afterEach(async () => {
74
72
  if (mcpServer) {
75
73
  await mcpServer.session.close();
@@ -2,7 +2,7 @@ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
2
2
  import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
3
3
 
4
4
  export class InMemoryTransport implements Transport {
5
- private outputController: ReadableStreamDefaultController<JSONRPCMessage>;
5
+ private outputController: ReadableStreamDefaultController<JSONRPCMessage> | undefined;
6
6
 
7
7
  private startPromise: Promise<unknown>;
8
8
 
@@ -35,13 +35,13 @@ export class InMemoryTransport implements Transport {
35
35
  }
36
36
 
37
37
  send(message: JSONRPCMessage): Promise<void> {
38
- this.outputController.enqueue(message);
38
+ this.outputController?.enqueue(message);
39
39
  return Promise.resolve();
40
40
  }
41
41
 
42
42
  // eslint-disable-next-line @typescript-eslint/require-await
43
43
  async close(): Promise<void> {
44
- this.outputController.close();
44
+ this.outputController?.close();
45
45
  this.onclose?.();
46
46
  }
47
47
  onclose?: (() => void) | undefined;
@@ -1,5 +1,4 @@
1
- import { expectDefined, setupIntegrationTest } from "./helpers.js";
2
- import { config } from "../../src/config.js";
1
+ import { defaultTestConfig, expectDefined, setupIntegrationTest } from "./helpers.js";
3
2
  import { describeWithMongoDB } from "./tools/mongodb/mongodbHelpers.js";
4
3
 
5
4
  describe("Server integration test", () => {
@@ -16,7 +15,7 @@ describe("Server integration test", () => {
16
15
  });
17
16
  },
18
17
  () => ({
19
- ...config,
18
+ ...defaultTestConfig,
20
19
  apiClientId: undefined,
21
20
  apiClientSecret: undefined,
22
21
  })
@@ -24,7 +23,7 @@ describe("Server integration test", () => {
24
23
 
25
24
  describe("with atlas", () => {
26
25
  const integration = setupIntegrationTest(() => ({
27
- ...config,
26
+ ...defaultTestConfig,
28
27
  apiClientId: "test",
29
28
  apiClientSecret: "test",
30
29
  }));
@@ -59,7 +58,7 @@ describe("Server integration test", () => {
59
58
 
60
59
  describe("with read-only mode", () => {
61
60
  const integration = setupIntegrationTest(() => ({
62
- ...config,
61
+ ...defaultTestConfig,
63
62
  readOnly: true,
64
63
  apiClientId: "test",
65
64
  apiClientSecret: "test",
@@ -1,15 +1,14 @@
1
1
  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
- import { setupIntegrationTest, IntegrationTest } from "../../helpers.js";
5
- import { config } from "../../../../src/config.js";
4
+ import { setupIntegrationTest, IntegrationTest, defaultTestConfig } from "../../helpers.js";
6
5
 
7
6
  export type IntegrationTestFunction = (integration: IntegrationTest) => void;
8
7
 
9
8
  export function describeWithAtlas(name: string, fn: IntegrationTestFunction) {
10
9
  const testDefinition = () => {
11
10
  const integration = setupIntegrationTest(() => ({
12
- ...config,
11
+ ...defaultTestConfig,
13
12
  apiClientId: process.env.MDB_MCP_API_CLIENT_ID,
14
13
  apiClientSecret: process.env.MDB_MCP_API_CLIENT_SECRET,
15
14
  }));
@@ -74,7 +73,7 @@ export function parseTable(text: string): Record<string, string>[] {
74
73
  return data
75
74
  .filter((_, index) => index >= 2)
76
75
  .map((cells) => {
77
- const row = {};
76
+ const row: Record<string, string> = {};
78
77
  cells.forEach((cell, index) => {
79
78
  row[headers[index]] = cell;
80
79
  });
@@ -1,9 +1,12 @@
1
1
  import { MongoCluster } from "mongodb-runner";
2
2
  import path from "path";
3
+ import { fileURLToPath } from "url";
3
4
  import fs from "fs/promises";
4
5
  import { MongoClient, ObjectId } from "mongodb";
5
- import { getResponseContent, IntegrationTest, setupIntegrationTest } from "../../helpers.js";
6
- import { config, UserConfig } from "../../../../src/config.js";
6
+ import { getResponseContent, IntegrationTest, setupIntegrationTest, defaultTestConfig } from "../../helpers.js";
7
+ import { UserConfig } from "../../../../src/config.js";
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
10
 
8
11
  interface MongoDBIntegrationTest {
9
12
  mongoClient: () => MongoClient;
@@ -14,7 +17,7 @@ interface MongoDBIntegrationTest {
14
17
  export function describeWithMongoDB(
15
18
  name: string,
16
19
  fn: (integration: IntegrationTest & MongoDBIntegrationTest & { connectMcpClient: () => Promise<void> }) => void,
17
- getUserConfig: (mdbIntegration: MongoDBIntegrationTest) => UserConfig = () => config,
20
+ getUserConfig: (mdbIntegration: MongoDBIntegrationTest) => UserConfig = () => defaultTestConfig,
18
21
  describeFn = describe
19
22
  ) {
20
23
  describeFn(name, () => {
@@ -4,6 +4,7 @@ import { Telemetry } from "../../src/telemetry/telemetry.js";
4
4
  import { BaseEvent, TelemetryResult } from "../../src/telemetry/types.js";
5
5
  import { EventCache } from "../../src/telemetry/eventCache.js";
6
6
  import { config } from "../../src/config.js";
7
+ import { jest } from "@jest/globals";
7
8
 
8
9
  // Mock the ApiClient to avoid real API calls
9
10
  jest.mock("../../src/common/atlas/apiClient.js");
@@ -21,16 +22,23 @@ describe("Telemetry", () => {
21
22
 
22
23
  // Helper function to create properly typed test events
23
24
  function createTestEvent(options?: {
24
- source?: string;
25
25
  result?: TelemetryResult;
26
26
  component?: string;
27
27
  category?: string;
28
28
  command?: string;
29
29
  duration_ms?: number;
30
- }): BaseEvent {
30
+ }): Omit<BaseEvent, "properties"> & {
31
+ properties: {
32
+ component: string;
33
+ duration_ms: number;
34
+ result: TelemetryResult;
35
+ category: string;
36
+ command: string;
37
+ };
38
+ } {
31
39
  return {
32
40
  timestamp: new Date().toISOString(),
33
- source: options?.source || "mdbmcp",
41
+ source: "mdbmcp",
34
42
  properties: {
35
43
  component: options?.component || "test-component",
36
44
  duration_ms: options?.duration_ms || 100,
@@ -48,6 +56,12 @@ describe("Telemetry", () => {
48
56
  appendEventsCalls = 0,
49
57
  sendEventsCalledWith = undefined,
50
58
  appendEventsCalledWith = undefined,
59
+ }: {
60
+ sendEventsCalls?: number;
61
+ clearEventsCalls?: number;
62
+ appendEventsCalls?: number;
63
+ sendEventsCalledWith?: BaseEvent[] | undefined;
64
+ appendEventsCalledWith?: BaseEvent[] | undefined;
51
65
  } = {}) {
52
66
  const { calls: sendEvents } = mockApiClient.sendEvents.mock;
53
67
  const { calls: clearEvents } = mockEventCache.clearEvents.mock;
@@ -58,7 +72,15 @@ describe("Telemetry", () => {
58
72
  expect(appendEvents.length).toBe(appendEventsCalls);
59
73
 
60
74
  if (sendEventsCalledWith) {
61
- expect(sendEvents[0]?.[0]).toEqual(sendEventsCalledWith);
75
+ expect(sendEvents[0]?.[0]).toEqual(
76
+ sendEventsCalledWith.map((event) => ({
77
+ ...event,
78
+ properties: {
79
+ ...telemetry.getCommonProperties(),
80
+ ...event.properties,
81
+ },
82
+ }))
83
+ );
62
84
  }
63
85
 
64
86
  if (appendEventsCalledWith) {
@@ -71,15 +93,20 @@ describe("Telemetry", () => {
71
93
  jest.clearAllMocks();
72
94
 
73
95
  // 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);
96
+ mockApiClient = new MockApiClient({ baseUrl: "" }) as jest.Mocked<ApiClient>;
97
+ //@ts-expect-error This is a workaround
98
+ mockApiClient.sendEvents = jest.fn<() => undefined>().mockResolvedValue(undefined);
99
+ mockApiClient.hasCredentials = jest.fn<() => boolean>().mockReturnValue(true);
77
100
 
78
101
  // Setup mocked EventCache
79
102
  mockEventCache = new MockEventCache() as jest.Mocked<EventCache>;
103
+ //@ts-expect-error This is a workaround
80
104
  mockEventCache.getEvents = jest.fn().mockReturnValue([]);
105
+ //@ts-expect-error This is a workaround
81
106
  mockEventCache.clearEvents = jest.fn().mockResolvedValue(undefined);
107
+ //@ts-expect-error This is a workaround
82
108
  mockEventCache.appendEvents = jest.fn().mockResolvedValue(undefined);
109
+ //@ts-expect-error This is a workaround
83
110
  MockEventCache.getInstance = jest.fn().mockReturnValue(mockEventCache);
84
111
 
85
112
  // Create a simplified session with our mocked API client
@@ -87,13 +114,14 @@ describe("Telemetry", () => {
87
114
  apiClient: mockApiClient,
88
115
  sessionId: "test-session-id",
89
116
  agentRunner: { name: "test-agent", version: "1.0.0" } as const,
117
+ //@ts-expect-error This is a workaround
90
118
  close: jest.fn().mockResolvedValue(undefined),
119
+ //@ts-expect-error This is a workaround
91
120
  setAgentRunner: jest.fn().mockResolvedValue(undefined),
92
121
  } as unknown as Session;
93
122
 
94
123
  // Create the telemetry instance with mocked dependencies
95
- telemetry = new Telemetry(session, mockEventCache);
96
-
124
+ telemetry = new Telemetry(session, config, mockEventCache);
97
125
  config.telemetry = "enabled";
98
126
  });
99
127
 
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2020",
4
+ "module": "nodenext",
5
+ "moduleResolution": "nodenext",
6
+ "rootDir": "./src",
7
+ "outDir": "./dist",
8
+ "strict": true,
9
+ "strictNullChecks": true,
10
+ "esModuleInterop": true,
11
+ "types": ["node", "jest"],
12
+ "sourceMap": true,
13
+ "skipLibCheck": true,
14
+ "resolveJsonModule": true,
15
+ "allowSyntheticDefaultImports": true,
16
+ "typeRoots": ["./node_modules/@types", "./src/types"]
17
+ },
18
+ "include": ["src/**/*.ts"]
19
+ }
@@ -1,8 +1,6 @@
1
1
  {
2
- "extends": "./tsconfig.json",
2
+ "extends": "./tsconfig.build.json",
3
3
  "compilerOptions": {
4
- "module": "esnext",
5
- "target": "esnext",
6
4
  "isolatedModules": true,
7
5
  "allowSyntheticDefaultImports": true,
8
6
  "types": ["jest", "jest-extended"]
package/tsconfig.json CHANGED
@@ -1,19 +1,9 @@
1
1
  {
2
+ "extends": "./tsconfig.build.json",
2
3
  "compilerOptions": {
3
- "target": "es2020",
4
- "module": "nodenext",
5
- "moduleResolution": "nodenext",
6
- "rootDir": "./src",
7
- "outDir": "./dist",
8
- "strict": true,
9
- "strictNullChecks": true,
10
- "esModuleInterop": true,
11
- "types": ["node", "jest"],
12
- "sourceMap": true,
13
- "skipLibCheck": true,
14
- "resolveJsonModule": true,
15
- "allowSyntheticDefaultImports": true,
16
- "typeRoots": ["./node_modules/@types", "./src/types"]
4
+ "rootDir": ".",
5
+ "types": ["jest"],
6
+ "skipLibCheck": true
17
7
  },
18
- "include": ["src/**/*.ts"]
8
+ "include": ["**/*"]
19
9
  }
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "rootDir": ".",
5
- "types": ["jest"]
6
- },
7
- "include": ["**/*"]
8
- }