mongodb-mcp-server 0.0.8 → 0.1.1

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 (90) hide show
  1. package/.github/CODEOWNERS +0 -2
  2. package/.github/ISSUE_TEMPLATE/bug_report.yml +8 -0
  3. package/.github/workflows/{lint.yml → check.yml} +22 -1
  4. package/.github/workflows/code_health.yaml +0 -22
  5. package/.github/workflows/code_health_fork.yaml +7 -63
  6. package/.vscode/extensions.json +9 -0
  7. package/.vscode/settings.json +11 -0
  8. package/README.md +41 -22
  9. package/dist/common/atlas/apiClient.js +141 -34
  10. package/dist/common/atlas/apiClient.js.map +1 -1
  11. package/dist/common/atlas/apiClientError.js +38 -5
  12. package/dist/common/atlas/apiClientError.js.map +1 -1
  13. package/dist/common/atlas/cluster.js +66 -0
  14. package/dist/common/atlas/cluster.js.map +1 -0
  15. package/dist/common/atlas/generatePassword.js +9 -0
  16. package/dist/common/atlas/generatePassword.js.map +1 -0
  17. package/dist/helpers/EJsonTransport.js +38 -0
  18. package/dist/helpers/EJsonTransport.js.map +1 -0
  19. package/dist/helpers/connectionOptions.js +10 -0
  20. package/dist/helpers/connectionOptions.js.map +1 -0
  21. package/dist/{packageInfo.js → helpers/packageInfo.js} +1 -1
  22. package/dist/helpers/packageInfo.js.map +1 -0
  23. package/dist/index.js +6 -3
  24. package/dist/index.js.map +1 -1
  25. package/dist/logger.js +2 -0
  26. package/dist/logger.js.map +1 -1
  27. package/dist/server.js +15 -11
  28. package/dist/server.js.map +1 -1
  29. package/dist/session.js +8 -3
  30. package/dist/session.js.map +1 -1
  31. package/dist/telemetry/constants.js +1 -3
  32. package/dist/telemetry/constants.js.map +1 -1
  33. package/dist/telemetry/eventCache.js.map +1 -1
  34. package/dist/telemetry/telemetry.js +46 -4
  35. package/dist/telemetry/telemetry.js.map +1 -1
  36. package/dist/tools/atlas/atlasTool.js +38 -0
  37. package/dist/tools/atlas/atlasTool.js.map +1 -1
  38. package/dist/tools/atlas/create/createDBUser.js +19 -2
  39. package/dist/tools/atlas/create/createDBUser.js.map +1 -1
  40. package/dist/tools/atlas/metadata/connectCluster.js +5 -22
  41. package/dist/tools/atlas/metadata/connectCluster.js.map +1 -1
  42. package/dist/tools/atlas/read/inspectCluster.js +4 -24
  43. package/dist/tools/atlas/read/inspectCluster.js.map +1 -1
  44. package/dist/tools/atlas/read/listClusters.js +9 -18
  45. package/dist/tools/atlas/read/listClusters.js.map +1 -1
  46. package/dist/tools/mongodb/tools.js +2 -4
  47. package/dist/tools/mongodb/tools.js.map +1 -1
  48. package/eslint.config.js +2 -1
  49. package/{jest.config.ts → jest.config.cjs} +1 -1
  50. package/package.json +4 -2
  51. package/scripts/apply.ts +4 -1
  52. package/scripts/filter.ts +4 -0
  53. package/src/common/atlas/apiClient.ts +179 -37
  54. package/src/common/atlas/apiClientError.ts +58 -7
  55. package/src/common/atlas/cluster.ts +95 -0
  56. package/src/common/atlas/generatePassword.ts +10 -0
  57. package/src/common/atlas/openapi.d.ts +438 -15
  58. package/src/helpers/EJsonTransport.ts +47 -0
  59. package/src/helpers/connectionOptions.ts +20 -0
  60. package/src/{packageInfo.ts → helpers/packageInfo.ts} +1 -1
  61. package/src/index.ts +7 -3
  62. package/src/logger.ts +2 -0
  63. package/src/server.ts +22 -14
  64. package/src/session.ts +8 -4
  65. package/src/telemetry/constants.ts +2 -3
  66. package/src/telemetry/eventCache.ts +1 -1
  67. package/src/telemetry/telemetry.ts +72 -6
  68. package/src/telemetry/types.ts +0 -1
  69. package/src/tools/atlas/atlasTool.ts +47 -1
  70. package/src/tools/atlas/create/createDBUser.ts +22 -2
  71. package/src/tools/atlas/metadata/connectCluster.ts +5 -27
  72. package/src/tools/atlas/read/inspectCluster.ts +4 -40
  73. package/src/tools/atlas/read/listClusters.ts +19 -36
  74. package/src/tools/mongodb/tools.ts +2 -4
  75. package/src/types/mongodb-connection-string-url.d.ts +69 -0
  76. package/tests/integration/helpers.ts +18 -2
  77. package/tests/integration/telemetry.test.ts +28 -0
  78. package/tests/integration/tools/atlas/dbUsers.test.ts +57 -32
  79. package/tests/integration/tools/mongodb/metadata/connect.test.ts +2 -6
  80. package/tests/integration/tools/mongodb/mongodbHelpers.ts +15 -24
  81. package/tests/integration/tools/mongodb/read/find.test.ts +28 -0
  82. package/tests/unit/EJsonTransport.test.ts +71 -0
  83. package/tests/unit/apiClient.test.ts +193 -0
  84. package/tests/unit/session.test.ts +65 -0
  85. package/tests/unit/telemetry.test.ts +165 -71
  86. package/tsconfig.build.json +1 -1
  87. package/dist/packageInfo.js.map +0 -1
  88. package/dist/telemetry/device-id.js +0 -20
  89. package/dist/telemetry/device-id.js.map +0 -1
  90. package/src/telemetry/device-id.ts +0 -21
@@ -2,7 +2,13 @@ import { z } from "zod";
2
2
  import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
3
  import { AtlasToolBase } from "../atlasTool.js";
4
4
  import { ToolArgs, OperationType } from "../../tool.js";
5
- import { PaginatedClusterDescription20240805, PaginatedOrgGroupView, Group } from "../../../common/atlas/openapi.js";
5
+ import {
6
+ PaginatedClusterDescription20240805,
7
+ PaginatedOrgGroupView,
8
+ Group,
9
+ PaginatedFlexClusters20241113,
10
+ } from "../../../common/atlas/openapi.js";
11
+ import { formatCluster, formatFlexCluster } from "../../../common/atlas/cluster.js";
6
12
 
7
13
  export class ListClustersTool extends AtlasToolBase {
8
14
  protected name = "atlas-list-clusters";
@@ -73,43 +79,20 @@ ${rows}`,
73
79
  };
74
80
  }
75
81
 
76
- private formatClustersTable(project: Group, clusters?: PaginatedClusterDescription20240805): CallToolResult {
77
- if (!clusters?.results?.length) {
82
+ private formatClustersTable(
83
+ project: Group,
84
+ clusters?: PaginatedClusterDescription20240805,
85
+ flexClusters?: PaginatedFlexClusters20241113
86
+ ): CallToolResult {
87
+ // Check if both traditional clusters and flex clusters are absent
88
+ if (!clusters?.results?.length && !flexClusters?.results?.length) {
78
89
  throw new Error("No clusters found.");
79
90
  }
80
- const rows = clusters.results
81
- .map((cluster) => {
82
- const connectionString =
83
- cluster.connectionStrings?.standardSrv || cluster.connectionStrings?.standard || "N/A";
84
- const mongoDBVersion = cluster.mongoDBVersion || "N/A";
85
- const regionConfigs = (cluster.replicationSpecs || [])
86
- .map(
87
- (replicationSpec) =>
88
- (replicationSpec.regionConfigs || []) as {
89
- providerName: string;
90
- electableSpecs?: {
91
- instanceSize: string;
92
- };
93
- readOnlySpecs?: {
94
- instanceSize: string;
95
- };
96
- }[]
97
- )
98
- .flat()
99
- .map((regionConfig) => {
100
- return {
101
- providerName: regionConfig.providerName,
102
- instanceSize:
103
- regionConfig.electableSpecs?.instanceSize || regionConfig.readOnlySpecs?.instanceSize,
104
- };
105
- });
106
-
107
- const instanceSize =
108
- (regionConfigs.length <= 0 ? undefined : regionConfigs[0].instanceSize) || "UNKNOWN";
109
-
110
- const clusterInstanceType = instanceSize == "M0" ? "FREE" : "DEDICATED";
111
-
112
- return `${cluster.name} | ${clusterInstanceType} | ${clusterInstanceType == "DEDICATED" ? instanceSize : "N/A"} | ${cluster.stateName} | ${mongoDBVersion} | ${connectionString}`;
91
+ const formattedClusters = clusters?.results?.map((cluster) => formatCluster(cluster)) || [];
92
+ const formattedFlexClusters = flexClusters?.results?.map((cluster) => formatFlexCluster(cluster)) || [];
93
+ const rows = [...formattedClusters, ...formattedFlexClusters]
94
+ .map((formattedCluster) => {
95
+ return `${formattedCluster.name || "Unknown"} | ${formattedCluster.instanceType} | ${formattedCluster.instanceSize || "N/A"} | ${formattedCluster.state || "UNKNOWN"} | ${formattedCluster.mongoDBVersion || "N/A"} | ${formattedCluster.connectionString || "N/A"}`;
113
96
  })
114
97
  .join("\n");
115
98
  return {
@@ -1,5 +1,4 @@
1
- // TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled
2
- // import { ConnectTool } from "./metadata/connect.js";
1
+ import { ConnectTool } from "./metadata/connect.js";
3
2
  import { ListCollectionsTool } from "./metadata/listCollections.js";
4
3
  import { CollectionIndexesTool } from "./read/collectionIndexes.js";
5
4
  import { ListDatabasesTool } from "./metadata/listDatabases.js";
@@ -21,8 +20,7 @@ import { CreateCollectionTool } from "./create/createCollection.js";
21
20
  import { LogsTool } from "./metadata/logs.js";
22
21
 
23
22
  export const MongoDbTools = [
24
- // TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled
25
- // ConnectTool,
23
+ ConnectTool,
26
24
  ListCollectionsTool,
27
25
  ListDatabasesTool,
28
26
  CollectionIndexesTool,
@@ -0,0 +1,69 @@
1
+ declare module "mongodb-connection-string-url" {
2
+ import { URL } from "whatwg-url";
3
+ import { redactConnectionString, ConnectionStringRedactionOptions } from "./redact";
4
+ export { redactConnectionString, ConnectionStringRedactionOptions };
5
+ declare class CaseInsensitiveMap<K extends string = string> extends Map<K, string> {
6
+ delete(name: K): boolean;
7
+ get(name: K): string | undefined;
8
+ has(name: K): boolean;
9
+ set(name: K, value: any): this;
10
+ _normalizeKey(name: any): K;
11
+ }
12
+ declare abstract class URLWithoutHost extends URL {
13
+ abstract get host(): never;
14
+ abstract set host(value: never);
15
+ abstract get hostname(): never;
16
+ abstract set hostname(value: never);
17
+ abstract get port(): never;
18
+ abstract set port(value: never);
19
+ abstract get href(): string;
20
+ abstract set href(value: string);
21
+ }
22
+ export interface ConnectionStringParsingOptions {
23
+ looseValidation?: boolean;
24
+ }
25
+ export declare class ConnectionString extends URLWithoutHost {
26
+ _hosts: string[];
27
+ constructor(uri: string, options?: ConnectionStringParsingOptions);
28
+ get host(): never;
29
+ set host(_ignored: never);
30
+ get hostname(): never;
31
+ set hostname(_ignored: never);
32
+ get port(): never;
33
+ set port(_ignored: never);
34
+ get href(): string;
35
+ set href(_ignored: string);
36
+ get isSRV(): boolean;
37
+ get hosts(): string[];
38
+ set hosts(list: string[]);
39
+ toString(): string;
40
+ clone(): ConnectionString;
41
+ redact(options?: ConnectionStringRedactionOptions): ConnectionString;
42
+ typedSearchParams<T extends {}>(): {
43
+ append(name: keyof T & string, value: any): void;
44
+ delete(name: keyof T & string): void;
45
+ get(name: keyof T & string): string | null;
46
+ getAll(name: keyof T & string): string[];
47
+ has(name: keyof T & string): boolean;
48
+ set(name: keyof T & string, value: any): void;
49
+ keys(): IterableIterator<keyof T & string>;
50
+ values(): IterableIterator<string>;
51
+ entries(): IterableIterator<[keyof T & string, string]>;
52
+ _normalizeKey(name: keyof T & string): string;
53
+ [Symbol.iterator](): IterableIterator<[keyof T & string, string]>;
54
+ sort(): void;
55
+ forEach<THIS_ARG = void>(
56
+ callback: (this: THIS_ARG, value: string, name: string, searchParams: any) => void,
57
+ thisArg?: THIS_ARG | undefined
58
+ ): void;
59
+ readonly [Symbol.toStringTag]: "URLSearchParams";
60
+ };
61
+ }
62
+ export declare class CommaAndColonSeparatedRecord<
63
+ K extends {} = Record<string, unknown>,
64
+ > extends CaseInsensitiveMap<keyof K & string> {
65
+ constructor(from?: string | null);
66
+ toString(): string;
67
+ }
68
+ export default ConnectionString;
69
+ }
@@ -5,7 +5,9 @@ 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 { Telemetry } from "../../src/telemetry/telemetry.js";
8
9
  import { config } from "../../src/config.js";
10
+ import { jest } from "@jest/globals";
9
11
 
10
12
  interface ParameterInfo {
11
13
  name: string;
@@ -56,14 +58,27 @@ export function setupIntegrationTest(getUserConfig: () => UserConfig): Integrati
56
58
  apiClientSecret: userConfig.apiClientSecret,
57
59
  });
58
60
 
61
+ // Mock hasValidAccessToken for tests
62
+ if (userConfig.apiClientId && userConfig.apiClientSecret) {
63
+ const mockFn = jest.fn<() => Promise<boolean>>().mockResolvedValue(true);
64
+ // @ts-expect-error accessing private property for testing
65
+ session.apiClient.validateAccessToken = mockFn;
66
+ }
67
+
68
+ userConfig.telemetry = "disabled";
69
+
70
+ const telemetry = Telemetry.create(session, userConfig);
71
+
59
72
  mcpServer = new Server({
60
73
  session,
61
74
  userConfig,
75
+ telemetry,
62
76
  mcpServer: new McpServer({
63
77
  name: "test-server",
64
78
  version: "5.2.3",
65
79
  }),
66
80
  });
81
+
67
82
  await mcpServer.connect(serverTransport);
68
83
  await mcpClient.connect(clientTransport);
69
84
  });
@@ -117,7 +132,7 @@ export function getResponseElements(content: unknown | { content: unknown }): {
117
132
  content = (content as { content: unknown }).content;
118
133
  }
119
134
 
120
- expect(Array.isArray(content)).toBe(true);
135
+ expect(content).toBeArray();
121
136
 
122
137
  const response = content as { type: string; text: string }[];
123
138
  for (const item of response) {
@@ -221,6 +236,7 @@ export function validateThrowsForInvalidArguments(
221
236
  }
222
237
 
223
238
  /** Expects the argument being defined and asserts it */
224
- export function expectDefined<T>(arg: T): asserts arg is Exclude<T, undefined> {
239
+ export function expectDefined<T>(arg: T): asserts arg is Exclude<T, undefined | null> {
225
240
  expect(arg).toBeDefined();
241
+ expect(arg).not.toBeNull();
226
242
  }
@@ -0,0 +1,28 @@
1
+ import { createHmac } from "crypto";
2
+ import { Telemetry } from "../../src/telemetry/telemetry.js";
3
+ import { Session } from "../../src/session.js";
4
+ import { config } from "../../src/config.js";
5
+ import nodeMachineId from "node-machine-id";
6
+
7
+ describe("Telemetry", () => {
8
+ it("should resolve the actual machine ID", async () => {
9
+ const actualId: string = await nodeMachineId.machineId(true);
10
+
11
+ const actualHashedId = createHmac("sha256", actualId.toUpperCase()).update("atlascli").digest("hex");
12
+
13
+ const telemetry = Telemetry.create(
14
+ new Session({
15
+ apiBaseUrl: "",
16
+ }),
17
+ config
18
+ );
19
+
20
+ expect(telemetry.getCommonProperties().device_id).toBe(undefined);
21
+ expect(telemetry["isBufferingEvents"]).toBe(true);
22
+
23
+ await telemetry.deviceIdPromise;
24
+
25
+ expect(telemetry.getCommonProperties().device_id).toBe(actualHashedId);
26
+ expect(telemetry["isBufferingEvents"]).toBe(false);
27
+ });
28
+ });
@@ -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,6 +91,8 @@ 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;
@@ -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
  );
@@ -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
 
@@ -4,6 +4,7 @@ import {
4
4
  validateToolMetadata,
5
5
  validateThrowsForInvalidArguments,
6
6
  getResponseElements,
7
+ expectDefined,
7
8
  } from "../../../helpers.js";
8
9
  import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js";
9
10
 
@@ -171,6 +172,33 @@ describeWithMongoDB("find tool", (integration) => {
171
172
  expect(JSON.parse(elements[i + 1].text).value).toEqual(i);
172
173
  }
173
174
  });
175
+
176
+ it("can find objects by $oid", async () => {
177
+ await integration.connectMcpClient();
178
+
179
+ const fooObject = await integration
180
+ .mongoClient()
181
+ .db(integration.randomDbName())
182
+ .collection("foo")
183
+ .findOne();
184
+ expectDefined(fooObject);
185
+
186
+ const response = await integration.mcpClient().callTool({
187
+ name: "find",
188
+ arguments: {
189
+ database: integration.randomDbName(),
190
+ collection: "foo",
191
+ filter: { _id: fooObject._id },
192
+ },
193
+ });
194
+
195
+ const elements = getResponseElements(response.content);
196
+ expect(elements).toHaveLength(2);
197
+ expect(elements[0].text).toEqual('Found 1 documents in the collection "foo":');
198
+
199
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
200
+ expect(JSON.parse(elements[1].text).value).toEqual(fooObject.value);
201
+ });
174
202
  });
175
203
 
176
204
  validateAutoConnectBehavior(integration, "find", () => {
@@ -0,0 +1,71 @@
1
+ import { Decimal128, MaxKey, MinKey, ObjectId, Timestamp, UUID } from "bson";
2
+ import { createEJsonTransport, EJsonReadBuffer } from "../../src/helpers/EJsonTransport.js";
3
+ import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
4
+ import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { Readable } from "stream";
7
+ import { ReadBuffer } from "@modelcontextprotocol/sdk/shared/stdio.js";
8
+
9
+ describe("EJsonTransport", () => {
10
+ let transport: StdioServerTransport;
11
+ beforeEach(async () => {
12
+ transport = createEJsonTransport();
13
+ await transport.start();
14
+ });
15
+
16
+ afterEach(async () => {
17
+ await transport.close();
18
+ });
19
+
20
+ it("ejson deserializes messages", () => {
21
+ const messages: { message: JSONRPCMessage; extra?: { authInfo?: AuthInfo } }[] = [];
22
+ transport.onmessage = (
23
+ message,
24
+ extra?: {
25
+ authInfo?: AuthInfo;
26
+ }
27
+ ) => {
28
+ messages.push({ message, extra });
29
+ };
30
+
31
+ (transport["_stdin"] as Readable).emit(
32
+ "data",
33
+ Buffer.from(
34
+ '{"jsonrpc":"2.0","id":1,"method":"testMethod","params":{"oid":{"$oid":"681b741f13aa74a0687b5110"},"uuid":{"$uuid":"f81d4fae-7dec-11d0-a765-00a0c91e6bf6"},"date":{"$date":"2025-05-07T14:54:23.973Z"},"decimal":{"$numberDecimal":"1234567890987654321"},"int32":123,"maxKey":{"$maxKey":1},"minKey":{"$minKey":1},"timestamp":{"$timestamp":{"t":123,"i":456}}}}\n',
35
+ "utf-8"
36
+ )
37
+ );
38
+
39
+ expect(messages.length).toBe(1);
40
+ const message = messages[0].message;
41
+
42
+ expect(message).toEqual({
43
+ jsonrpc: "2.0",
44
+ id: 1,
45
+ method: "testMethod",
46
+ params: {
47
+ oid: new ObjectId("681b741f13aa74a0687b5110"),
48
+ uuid: new UUID("f81d4fae-7dec-11d0-a765-00a0c91e6bf6"),
49
+ date: new Date(Date.parse("2025-05-07T14:54:23.973Z")),
50
+ decimal: new Decimal128("1234567890987654321"),
51
+ int32: 123,
52
+ maxKey: new MaxKey(),
53
+ minKey: new MinKey(),
54
+ timestamp: new Timestamp({ t: 123, i: 456 }),
55
+ },
56
+ });
57
+ });
58
+
59
+ it("has _readBuffer field of type EJsonReadBuffer", () => {
60
+ expect(transport["_readBuffer"]).toBeDefined();
61
+ expect(transport["_readBuffer"]).toBeInstanceOf(EJsonReadBuffer);
62
+ });
63
+
64
+ describe("standard StdioServerTransport", () => {
65
+ it("has a _readBuffer field", () => {
66
+ const standardTransport = new StdioServerTransport();
67
+ expect(standardTransport["_readBuffer"]).toBeDefined();
68
+ expect(standardTransport["_readBuffer"]).toBeInstanceOf(ReadBuffer);
69
+ });
70
+ });
71
+ });