From 4900b02513633c290a7d583f8a7ffaca7de3f4d2 Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Thu, 4 Sep 2025 16:07:00 +0100 Subject: [PATCH 01/14] Implemented 'atlas-local-list-deployments' --- package-lock.json | 97 +++++++++++++++++++ package.json | 1 + src/server.ts | 9 +- src/tools/atlasLocal/atlasLocalTool.ts | 17 +++- src/tools/atlasLocal/read/listDeployments.ts | 58 +++++++++++ src/tools/atlasLocal/tools.ts | 34 ++++++- .../tools/atlas-local/listDeployments.test.ts | 31 ++++++ 7 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 src/tools/atlasLocal/read/listDeployments.ts create mode 100644 tests/integration/tools/atlas-local/listDeployments.test.ts diff --git a/package-lock.json b/package-lock.json index 89d96637..489ed142 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.4", + "@mongodb-js-preview/atlas-local": "^0.0.0-preview.1", "@mongodb-js/device-id": "^0.3.1", "@mongodb-js/devtools-connect": "^3.9.3", "@mongodb-js/devtools-proxy-support": "^0.5.2", @@ -2152,6 +2153,102 @@ "node": ">=16.20.0" } }, + "node_modules/@mongodb-js-preview/atlas-local": { + "version": "0.0.0-preview.1", + "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local/-/atlas-local-0.0.0-preview.1.tgz", + "integrity": "sha512-py3roloK+dyq9bCU139f3JdFykige1kWwUli9qWE4daODFdJ0mvQPN1EChw3lzI4rv53cX1CvApbh20liOukoQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 12.22.0 < 13 ||>= 14.17.0 < 15 ||>= 15.12.0 < 16 ||>= 16.0.0" + }, + "optionalDependencies": { + "@mongodb-js-preview/atlas-local-darwin-arm64": "0.0.0-preview.1", + "@mongodb-js-preview/atlas-local-darwin-x64": "0.0.0-preview.1", + "@mongodb-js-preview/atlas-local-linux-arm64-gnu": "0.0.0-preview.1", + "@mongodb-js-preview/atlas-local-linux-x64-gnu": "0.0.0-preview.1", + "@mongodb-js-preview/atlas-local-win32-x64-msvc": "0.0.0-preview.1" + } + }, + "node_modules/@mongodb-js-preview/atlas-local-darwin-arm64": { + "version": "0.0.0-preview.1", + "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-darwin-arm64/-/atlas-local-darwin-arm64-0.0.0-preview.1.tgz", + "integrity": "sha512-TcH7CFCg6pAx0KPhTUOyaZRwXOOTb5WCo9on12GqEk/oM+vERwfK5ztGSZns45IvxgN6zUsERoV+O6SEoK1gsA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.22.0 < 13 ||>= 14.17.0 < 15 ||>= 15.12.0 < 16 ||>= 16.0.0" + } + }, + "node_modules/@mongodb-js-preview/atlas-local-darwin-x64": { + "version": "0.0.0-preview.1", + "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-darwin-x64/-/atlas-local-darwin-x64-0.0.0-preview.1.tgz", + "integrity": "sha512-KmG+xKCS5f3adhznYH569mq0PHrFoGuqsGN5XEtVtUEYgv/gQicgJ0voWMrwTHu3jIFFQeGEvMgKsciyXAlVaQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.22.0 < 13 ||>= 14.17.0 < 15 ||>= 15.12.0 < 16 ||>= 16.0.0" + } + }, + "node_modules/@mongodb-js-preview/atlas-local-linux-arm64-gnu": { + "version": "0.0.0-preview.1", + "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-linux-arm64-gnu/-/atlas-local-linux-arm64-gnu-0.0.0-preview.1.tgz", + "integrity": "sha512-0ImE3RUdWiO38JWXiG6xAZzpz3CA2MHfpQdcwIomF0ldw/14ofRYkH31KX0M444j2rS2/AHBa+zdswYqFZCQbg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.22.0 < 13 ||>= 14.17.0 < 15 ||>= 15.12.0 < 16 ||>= 16.0.0" + } + }, + "node_modules/@mongodb-js-preview/atlas-local-linux-x64-gnu": { + "version": "0.0.0-preview.1", + "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-linux-x64-gnu/-/atlas-local-linux-x64-gnu-0.0.0-preview.1.tgz", + "integrity": "sha512-AM+s8ygWU5gkNm6rDkLnWueOIon9T3kaeSleo4qgeAt4rgA7C9f2XUkQmFsv8b1E9g6CbNYrrbAAcnc0xVMtLQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.22.0 < 13 ||>= 14.17.0 < 15 ||>= 15.12.0 < 16 ||>= 16.0.0" + } + }, + "node_modules/@mongodb-js-preview/atlas-local-win32-x64-msvc": { + "version": "0.0.0-preview.1", + "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-win32-x64-msvc/-/atlas-local-win32-x64-msvc-0.0.0-preview.1.tgz", + "integrity": "sha512-1oEsFgKz4Hatp+lD4pIB5EcrCqSwAx3p4FEJqwCKHbYGIsSVuvjhvN28hZHPHqkQhM9l8ZWFa8Z87/LR/Hy8FQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.22.0 < 13 ||>= 14.17.0 < 15 ||>= 15.12.0 < 16 ||>= 16.0.0" + } + }, "node_modules/@mongodb-js/device-id": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@mongodb-js/device-id/-/device-id-0.3.1.tgz", diff --git a/package.json b/package.json index 67fbb68b..6245c183 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.17.4", + "@mongodb-js-preview/atlas-local": "^0.0.0-preview.1", "@mongodb-js/device-id": "^0.3.1", "@mongodb-js/devtools-connect": "^3.9.3", "@mongodb-js/devtools-proxy-support": "^0.5.2", diff --git a/src/server.ts b/src/server.ts index 006dc7cd..468a3203 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,7 +2,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { Session } from "./common/session.js"; import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { AtlasTools } from "./tools/atlas/tools.js"; -import { AtlasLocalTools } from "./tools/atlasLocal/tools.js"; +import { BuildAtlasLocalTools } from "./tools/atlasLocal/tools.js"; import { MongoDbTools } from "./tools/mongodb/tools.js"; import { Resources } from "./resources/resources.js"; import type { LogLevel } from "./common/logger.js"; @@ -62,7 +62,7 @@ export class Server { this.mcpServer.server.registerCapabilities({ logging: {}, resources: { listChanged: true, subscribe: true } }); // TODO: Eventually we might want to make tools reactive too instead of relying on custom logic. - this.registerTools(); + await this.registerTools(); // This is a workaround for an issue we've seen with some models, where they'll see that everything in the `arguments` // object is optional, and then not pass it at all. However, the MCP server expects the `arguments` object to be if @@ -193,8 +193,9 @@ export class Server { this.telemetry.emitEvents([event]).catch(() => {}); } - private registerTools(): void { - for (const toolConstructor of [...AtlasTools, ...AtlasLocalTools, ...MongoDbTools]) { + private async registerTools(): Promise { + const atlasLocalTools = await BuildAtlasLocalTools(); + for (const toolConstructor of [...AtlasTools, ...atlasLocalTools, ...MongoDbTools]) { const tool = new toolConstructor(this.session, this.userConfig, this.telemetry); if (tool.register(this)) { this.tools.push(tool); diff --git a/src/tools/atlasLocal/atlasLocalTool.ts b/src/tools/atlasLocal/atlasLocalTool.ts index b5c7899f..efd016e9 100644 --- a/src/tools/atlasLocal/atlasLocalTool.ts +++ b/src/tools/atlasLocal/atlasLocalTool.ts @@ -1,9 +1,17 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import type { ToolArgs, ToolCategory } from "../tool.js"; +import type { TelemetryToolMetadata, ToolArgs, ToolCategory } from "../tool.js"; import { ToolBase } from "../tool.js"; +import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type AtlasLocal from "@mongodb-js-preview/atlas-local"; export abstract class AtlasLocalToolBase extends ToolBase { public category: ToolCategory = "atlas-local"; + // Will be injected by BuildAtlasLocalTools() in atlasLocal/tools.ts + public client?: AtlasLocal.Client; + + protected verifyAllowed(): boolean { + return this.client !== undefined && super.verifyAllowed(); + } protected handleError( error: unknown, @@ -14,4 +22,11 @@ export abstract class AtlasLocalToolBase extends ToolBase { // For other types of errors, use the default error handling from the base class return super.handleError(error, args); } + + protected resolveTelemetryMetadata( + ...args: Parameters> + ): TelemetryToolMetadata { + // TODO: include deployment id in the metadata where possible + return {}; + } } diff --git a/src/tools/atlasLocal/read/listDeployments.ts b/src/tools/atlasLocal/read/listDeployments.ts new file mode 100644 index 00000000..62a569a4 --- /dev/null +++ b/src/tools/atlasLocal/read/listDeployments.ts @@ -0,0 +1,58 @@ +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { AtlasLocalToolBase } from "../atlasLocalTool.js"; +import type { ToolArgs, OperationType, TelemetryToolMetadata } from "../../tool.js"; +import { formatUntrustedData } from "../../tool.js"; +import type { Deployment } from "@mongodb-js-preview/atlas-local"; + +export class ListDeploymentsTool extends AtlasLocalToolBase { + public name = "atlas-local-list-deployments"; + protected description = "List MongoDB Atlas local deployments"; + public operationType: OperationType = "read"; + protected argsShape = {}; + + protected async execute({}: ToolArgs): Promise { + // Get the client + const client = this.client; + + // If the client is not found, throw an error + // This should never happen, because the tool should have been disabled. + // verifyAllowed in the base class returns false if the client is not found + if (!client) { + throw new Error("Atlas Local client not found, tool should have been disabled."); + } + + // List the deployments + const deployments = await client.listDeployments(); + + // Format the deployments + return this.formatDeploymentsTable(deployments); + } + + + private formatDeploymentsTable( + deployments: Deployment[] + ): CallToolResult { + // Check if deployments are absent + if (!deployments?.length) { + return { + content: [{ type: "text", text: "No deployments found." }], + }; + } + + // Turn the deployments into a markdown table + const rows = deployments + .map((deployment) => { + return `${deployment.name || "Unknown"} | ${deployment.state} | ${deployment.mongodbVersion}` + }) + .join("\n"); + + return { + content: formatUntrustedData( + `Found ${deployments.length} deployments:`, + `Deployment Name | State | MongoDB Version +----------------|----------------|---------------- +${rows}` + ), + }; + } +} diff --git a/src/tools/atlasLocal/tools.ts b/src/tools/atlasLocal/tools.ts index be1a9552..b4595db8 100644 --- a/src/tools/atlasLocal/tools.ts +++ b/src/tools/atlasLocal/tools.ts @@ -1 +1,33 @@ -export const AtlasLocalTools = []; +import { ListDeploymentsTool } from "./read/listDeployments.js"; +import type AtlasLocal from "@mongodb-js-preview/atlas-local"; + +// Don't use this directly, use BuildAtlasLocalTools instead +const atlasLocalTools = [ListDeploymentsTool]; + +// Build the Atlas Local tools +export const BuildAtlasLocalTools = async () => { + // Initialize the Atlas Local client + const client = await GetAtlasLocalClient(); + + // If the client is found, set it on the tools + // On unsupported platforms, the client will be undefined + if (client) { + // Set the client on the tools + atlasLocalTools.forEach(tool => { + tool.prototype.client = client; + }); + } + + return atlasLocalTools; +}; + +export const GetAtlasLocalClient = async (): Promise => { + try { + const { Client: AtlasLocalClient } = await import("@mongodb-js-preview/atlas-local"); + return AtlasLocalClient.connect(); + } catch (error) { + // We only get here if the user is running atlas-local on a unsupported platform + console.warn("Atlas Local native binding not available:", error); + return undefined; + } +} diff --git a/tests/integration/tools/atlas-local/listDeployments.test.ts b/tests/integration/tools/atlas-local/listDeployments.test.ts new file mode 100644 index 00000000..a79434da --- /dev/null +++ b/tests/integration/tools/atlas-local/listDeployments.test.ts @@ -0,0 +1,31 @@ +import { defaultDriverOptions, defaultTestConfig, expectDefined, getResponseElements, setupIntegrationTest } from "../../helpers.js"; +import { describe, expect, it } from "vitest"; + + + +describe("atlas-local-list-deployments", () => { + const integration = setupIntegrationTest( + () => defaultTestConfig, + () => defaultDriverOptions + ); + + it("should have correct metadata", async () => { + const { tools } = await integration.mcpClient().listTools(); + const listDeployments = tools.find((tool) => tool.name === "atlas-local-list-deployments"); + expectDefined(listDeployments); + expect(listDeployments.inputSchema.type).toBe("object"); + expectDefined(listDeployments.inputSchema.properties); + expect(listDeployments.inputSchema.properties).toEqual({}); + }); + + it("should not crash when calling the tool", async () => { + const response = await integration.mcpClient().callTool({ + name: "atlas-local-list-deployments", + arguments: {}, + }); + const elements = getResponseElements(response.content); + expect(elements).toHaveLength(2); + expect(elements[0]?.text).toMatch(/Found \d+ deployments/); + expect(elements[1]?.text).toContain("Deployment Name | State | MongoDB Version\n----------------|----------------|----------------\n"); + }); +}); \ No newline at end of file From 5c660dadbd8ccb0663f969eebbf5bcd2d80d32b8 Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Thu, 4 Sep 2025 16:21:29 +0100 Subject: [PATCH 02/14] chore: made eslint happy --- src/tools/atlasLocal/atlasLocalTool.ts | 3 ++- src/tools/atlasLocal/read/listDeployments.ts | 15 ++++++--------- src/tools/atlasLocal/tools.ts | 6 +++--- .../tools/atlas-local/listDeployments.test.ts | 18 ++++++++++++------ 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/tools/atlasLocal/atlasLocalTool.ts b/src/tools/atlasLocal/atlasLocalTool.ts index efd016e9..c46bc070 100644 --- a/src/tools/atlasLocal/atlasLocalTool.ts +++ b/src/tools/atlasLocal/atlasLocalTool.ts @@ -1,7 +1,7 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import type { TelemetryToolMetadata, ToolArgs, ToolCategory } from "../tool.js"; import { ToolBase } from "../tool.js"; -import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; import type AtlasLocal from "@mongodb-js-preview/atlas-local"; export abstract class AtlasLocalToolBase extends ToolBase { @@ -27,6 +27,7 @@ export abstract class AtlasLocalToolBase extends ToolBase { ...args: Parameters> ): TelemetryToolMetadata { // TODO: include deployment id in the metadata where possible + void args; // this shuts up the eslint rule until we implement the TODO above return {}; } } diff --git a/src/tools/atlasLocal/read/listDeployments.ts b/src/tools/atlasLocal/read/listDeployments.ts index 62a569a4..a58fb5f5 100644 --- a/src/tools/atlasLocal/read/listDeployments.ts +++ b/src/tools/atlasLocal/read/listDeployments.ts @@ -1,6 +1,6 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { AtlasLocalToolBase } from "../atlasLocalTool.js"; -import type { ToolArgs, OperationType, TelemetryToolMetadata } from "../../tool.js"; +import type { OperationType } from "../../tool.js"; import { formatUntrustedData } from "../../tool.js"; import type { Deployment } from "@mongodb-js-preview/atlas-local"; @@ -10,7 +10,7 @@ export class ListDeploymentsTool extends AtlasLocalToolBase { public operationType: OperationType = "read"; protected argsShape = {}; - protected async execute({}: ToolArgs): Promise { + protected async execute(): Promise { // Get the client const client = this.client; @@ -20,7 +20,7 @@ export class ListDeploymentsTool extends AtlasLocalToolBase { if (!client) { throw new Error("Atlas Local client not found, tool should have been disabled."); } - + // List the deployments const deployments = await client.listDeployments(); @@ -28,10 +28,7 @@ export class ListDeploymentsTool extends AtlasLocalToolBase { return this.formatDeploymentsTable(deployments); } - - private formatDeploymentsTable( - deployments: Deployment[] - ): CallToolResult { + private formatDeploymentsTable(deployments: Deployment[]): CallToolResult { // Check if deployments are absent if (!deployments?.length) { return { @@ -42,10 +39,10 @@ export class ListDeploymentsTool extends AtlasLocalToolBase { // Turn the deployments into a markdown table const rows = deployments .map((deployment) => { - return `${deployment.name || "Unknown"} | ${deployment.state} | ${deployment.mongodbVersion}` + return `${deployment.name || "Unknown"} | ${deployment.state} | ${deployment.mongodbVersion}`; }) .join("\n"); - + return { content: formatUntrustedData( `Found ${deployments.length} deployments:`, diff --git a/src/tools/atlasLocal/tools.ts b/src/tools/atlasLocal/tools.ts index b4595db8..801b63f7 100644 --- a/src/tools/atlasLocal/tools.ts +++ b/src/tools/atlasLocal/tools.ts @@ -5,7 +5,7 @@ import type AtlasLocal from "@mongodb-js-preview/atlas-local"; const atlasLocalTools = [ListDeploymentsTool]; // Build the Atlas Local tools -export const BuildAtlasLocalTools = async () => { +export const BuildAtlasLocalTools = async (): Promise => { // Initialize the Atlas Local client const client = await GetAtlasLocalClient(); @@ -13,7 +13,7 @@ export const BuildAtlasLocalTools = async () => { // On unsupported platforms, the client will be undefined if (client) { // Set the client on the tools - atlasLocalTools.forEach(tool => { + atlasLocalTools.forEach((tool) => { tool.prototype.client = client; }); } @@ -30,4 +30,4 @@ export const GetAtlasLocalClient = async (): Promise { const integration = setupIntegrationTest( () => defaultTestConfig, () => defaultDriverOptions ); - + it("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); const listDeployments = tools.find((tool) => tool.name === "atlas-local-list-deployments"); @@ -26,6 +30,8 @@ describe("atlas-local-list-deployments", () => { const elements = getResponseElements(response.content); expect(elements).toHaveLength(2); expect(elements[0]?.text).toMatch(/Found \d+ deployments/); - expect(elements[1]?.text).toContain("Deployment Name | State | MongoDB Version\n----------------|----------------|----------------\n"); + expect(elements[1]?.text).toContain( + "Deployment Name | State | MongoDB Version\n----------------|----------------|----------------\n" + ); }); -}); \ No newline at end of file +}); From 9e4e3c5ba7c46c45999790993cff92bb321b7243 Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Thu, 4 Sep 2025 16:37:26 +0100 Subject: [PATCH 03/14] Fix MacOS tests + tool counting tests --- tests/integration/server.test.ts | 2 +- .../tools/atlas-local/listDeployments.test.ts | 18 ++++++++++++++++-- tests/integration/transports/stdio.test.ts | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/tests/integration/server.test.ts b/tests/integration/server.test.ts index ef98075a..b6fad4bc 100644 --- a/tests/integration/server.test.ts +++ b/tests/integration/server.test.ts @@ -11,7 +11,7 @@ describe("Server integration test", () => { expectDefined(tools); expect(tools.tools.length).toBeGreaterThan(0); - const atlasTools = tools.tools.filter((tool) => tool.name.startsWith("atlas-")); + const atlasTools = tools.tools.filter((tool) => tool.name.startsWith("atlas-") && !tool.name.startsWith("atlas-local-")); expect(atlasTools.length).toBeLessThanOrEqual(0); }); }, diff --git a/tests/integration/tools/atlas-local/listDeployments.test.ts b/tests/integration/tools/atlas-local/listDeployments.test.ts index 400aeacf..db77b46d 100644 --- a/tests/integration/tools/atlas-local/listDeployments.test.ts +++ b/tests/integration/tools/atlas-local/listDeployments.test.ts @@ -7,13 +7,27 @@ import { } from "../../helpers.js"; import { describe, expect, it } from "vitest"; +const isMacOSInGitHubActions = process.platform === 'darwin' && process.env.GITHUB_ACTIONS === 'true' + describe("atlas-local-list-deployments", () => { const integration = setupIntegrationTest( () => defaultTestConfig, () => defaultDriverOptions ); - it("should have correct metadata", async () => { + it.skipIf(isMacOSInGitHubActions)("should have the atlas-local-list-deployments tool", async () => { + const { tools } = await integration.mcpClient().listTools(); + const listDeployments = tools.find((tool) => tool.name === "atlas-local-list-deployments"); + expectDefined(listDeployments); + }); + + it.skipIf(!isMacOSInGitHubActions)("[MacOS in GitHub Actions] should not have the atlas-local-list-deployments tool", async () => { + const { tools } = await integration.mcpClient().listTools(); + const listDeployments = tools.find((tool) => tool.name === "atlas-local-list-deployments"); + expect(listDeployments).toBeUndefined(); + }); + + it.skipIf(isMacOSInGitHubActions)("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); const listDeployments = tools.find((tool) => tool.name === "atlas-local-list-deployments"); expectDefined(listDeployments); @@ -22,7 +36,7 @@ describe("atlas-local-list-deployments", () => { expect(listDeployments.inputSchema.properties).toEqual({}); }); - it("should not crash when calling the tool", async () => { + it.skipIf(isMacOSInGitHubActions)("should not crash when calling the tool", async () => { const response = await integration.mcpClient().callTool({ name: "atlas-local-list-deployments", arguments: {}, diff --git a/tests/integration/transports/stdio.test.ts b/tests/integration/transports/stdio.test.ts index aaa61d63..7dfa6dfa 100644 --- a/tests/integration/transports/stdio.test.ts +++ b/tests/integration/transports/stdio.test.ts @@ -10,7 +10,7 @@ describeWithMongoDB("StdioRunner", (integration) => { beforeAll(async () => { transport = new StdioClientTransport({ command: "node", - args: ["dist/index.js"], + args: ["dist/index.js", "--disableTools", "atlas-local"], env: { MDB_MCP_TRANSPORT: "stdio", MDB_MCP_CONNECTION_STRING: integration.connectionString(), From 374355794a6f79f0d5ad5537dfb44de9217a0446 Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Fri, 5 Sep 2025 07:59:15 +0100 Subject: [PATCH 04/14] Fix integration test when there's not deployment available --- tests/integration/server.test.ts | 4 ++- .../tools/atlas-local/listDeployments.test.ts | 32 ++++++++++++------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/tests/integration/server.test.ts b/tests/integration/server.test.ts index b6fad4bc..446c3121 100644 --- a/tests/integration/server.test.ts +++ b/tests/integration/server.test.ts @@ -11,7 +11,9 @@ describe("Server integration test", () => { expectDefined(tools); expect(tools.tools.length).toBeGreaterThan(0); - const atlasTools = tools.tools.filter((tool) => tool.name.startsWith("atlas-") && !tool.name.startsWith("atlas-local-")); + const atlasTools = tools.tools.filter( + (tool) => tool.name.startsWith("atlas-") && !tool.name.startsWith("atlas-local-") + ); expect(atlasTools.length).toBeLessThanOrEqual(0); }); }, diff --git a/tests/integration/tools/atlas-local/listDeployments.test.ts b/tests/integration/tools/atlas-local/listDeployments.test.ts index db77b46d..bced623f 100644 --- a/tests/integration/tools/atlas-local/listDeployments.test.ts +++ b/tests/integration/tools/atlas-local/listDeployments.test.ts @@ -7,7 +7,7 @@ import { } from "../../helpers.js"; import { describe, expect, it } from "vitest"; -const isMacOSInGitHubActions = process.platform === 'darwin' && process.env.GITHUB_ACTIONS === 'true' +const isMacOSInGitHubActions = process.platform === "darwin" && process.env.GITHUB_ACTIONS === "true"; describe("atlas-local-list-deployments", () => { const integration = setupIntegrationTest( @@ -21,11 +21,14 @@ describe("atlas-local-list-deployments", () => { expectDefined(listDeployments); }); - it.skipIf(!isMacOSInGitHubActions)("[MacOS in GitHub Actions] should not have the atlas-local-list-deployments tool", async () => { - const { tools } = await integration.mcpClient().listTools(); - const listDeployments = tools.find((tool) => tool.name === "atlas-local-list-deployments"); - expect(listDeployments).toBeUndefined(); - }); + it.skipIf(!isMacOSInGitHubActions)( + "[MacOS in GitHub Actions] should not have the atlas-local-list-deployments tool", + async () => { + const { tools } = await integration.mcpClient().listTools(); + const listDeployments = tools.find((tool) => tool.name === "atlas-local-list-deployments"); + expect(listDeployments).toBeUndefined(); + } + ); it.skipIf(isMacOSInGitHubActions)("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); @@ -42,10 +45,17 @@ describe("atlas-local-list-deployments", () => { arguments: {}, }); const elements = getResponseElements(response.content); - expect(elements).toHaveLength(2); - expect(elements[0]?.text).toMatch(/Found \d+ deployments/); - expect(elements[1]?.text).toContain( - "Deployment Name | State | MongoDB Version\n----------------|----------------|----------------\n" - ); + expect(elements.length).toBeGreaterThanOrEqual(1); + + if (elements.length === 1) { + expect(elements[0]?.text).toContain("No deployments found."); + } + + if (elements.length> 1) { + expect(elements[0]?.text).toMatch(/Found \d+ deployments/); + expect(elements[1]?.text).toContain( + "Deployment Name | State | MongoDB Version\n----------------|----------------|----------------\n" + ); + } }); }); From f3e1064f45e484abb3932c59e83e09f420a94449 Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Fri, 5 Sep 2025 08:09:25 +0100 Subject: [PATCH 05/14] Updated tool count --- tests/integration/transports/stdio.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/transports/stdio.test.ts b/tests/integration/transports/stdio.test.ts index 7dfa6dfa..81c21ccc 100644 --- a/tests/integration/transports/stdio.test.ts +++ b/tests/integration/transports/stdio.test.ts @@ -32,7 +32,7 @@ describeWithMongoDB("StdioRunner", (integration) => { const response = await client.listTools(); expect(response).toBeDefined(); expect(response.tools).toBeDefined(); - expect(response.tools).toHaveLength(21); + expect(response.tools).toHaveLength(22); const sortedTools = response.tools.sort((a, b) => a.name.localeCompare(b.name)); expect(sortedTools[0]?.name).toBe("aggregate"); From e6b0d156c9165cb03c96838fd77f3b3015157afa Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Fri, 5 Sep 2025 08:19:48 +0100 Subject: [PATCH 06/14] fixed typo and count --- tests/integration/transports/stdio.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/transports/stdio.test.ts b/tests/integration/transports/stdio.test.ts index 81c21ccc..a5ba4259 100644 --- a/tests/integration/transports/stdio.test.ts +++ b/tests/integration/transports/stdio.test.ts @@ -10,7 +10,7 @@ describeWithMongoDB("StdioRunner", (integration) => { beforeAll(async () => { transport = new StdioClientTransport({ command: "node", - args: ["dist/index.js", "--disableTools", "atlas-local"], + args: ["dist/index.js", "--disabledTools", "atlas-local"], env: { MDB_MCP_TRANSPORT: "stdio", MDB_MCP_CONNECTION_STRING: integration.connectionString(), @@ -32,7 +32,7 @@ describeWithMongoDB("StdioRunner", (integration) => { const response = await client.listTools(); expect(response).toBeDefined(); expect(response.tools).toBeDefined(); - expect(response.tools).toHaveLength(22); + expect(response.tools).toHaveLength(21); const sortedTools = response.tools.sort((a, b) => a.name.localeCompare(b.name)); expect(sortedTools[0]?.name).toBe("aggregate"); From 06da4c5a1af87b5a51d19b0b4d7609b3e2f4ef5c Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Fri, 5 Sep 2025 10:54:26 +0100 Subject: [PATCH 07/14] adressed PR remarks --- src/common/session.ts | 6 ++++ src/server.ts | 26 ++++++++++++--- src/tools/atlasLocal/atlasLocalTool.ts | 22 ++++++++++--- src/tools/atlasLocal/read/listDeployments.ts | 13 ++------ src/tools/atlasLocal/tools.ts | 32 +------------------ .../tools/atlas-local/listDeployments.test.ts | 2 ++ 6 files changed, 50 insertions(+), 51 deletions(-) diff --git a/src/common/session.ts b/src/common/session.ts index c1f7b5a1..a36ee778 100644 --- a/src/common/session.ts +++ b/src/common/session.ts @@ -15,6 +15,7 @@ import type { import type { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver"; import { ErrorCodes, MongoDBError } from "./errors.js"; import type { ExportsManager } from "./exportsManager.js"; +import type { Client } from "@mongodb-js-preview/atlas-local"; export interface SessionOptions { apiBaseUrl: string; @@ -42,6 +43,7 @@ export class Session extends EventEmitter { version?: string; title?: string; }; + atlasLocalClient?: Client; public logger: CompositeLogger; @@ -93,6 +95,10 @@ export class Session extends EventEmitter { this.connectionManager.setClientName(this.mcpClient.name || "unknown"); } + setAtlasLocalClient(atlasLocalClient: Client): void { + this.atlasLocalClient = atlasLocalClient; + } + async disconnect(): Promise { const atlasCluster = this.connectedAtlasCluster; diff --git a/src/server.ts b/src/server.ts index 468a3203..c8fb6319 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,7 +2,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { Session } from "./common/session.js"; import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { AtlasTools } from "./tools/atlas/tools.js"; -import { BuildAtlasLocalTools } from "./tools/atlasLocal/tools.js"; +import { AtlasLocalTools } from "./tools/atlasLocal/tools.js"; import { MongoDbTools } from "./tools/mongodb/tools.js"; import { Resources } from "./resources/resources.js"; import type { LogLevel } from "./common/logger.js"; @@ -61,8 +61,10 @@ export class Server { this.mcpServer.server.registerCapabilities({ logging: {}, resources: { listChanged: true, subscribe: true } }); + await this.tryInitializeAtlasLocalClient(); + // TODO: Eventually we might want to make tools reactive too instead of relying on custom logic. - await this.registerTools(); + this.registerTools(); // This is a workaround for an issue we've seen with some models, where they'll see that everything in the `arguments` // object is optional, and then not pass it at all. However, the MCP server expects the `arguments` object to be if @@ -193,9 +195,23 @@ export class Server { this.telemetry.emitEvents([event]).catch(() => {}); } - private async registerTools(): Promise { - const atlasLocalTools = await BuildAtlasLocalTools(); - for (const toolConstructor of [...AtlasTools, ...atlasLocalTools, ...MongoDbTools]) { + private async tryInitializeAtlasLocalClient(): Promise { + try { + const { Client: AtlasLocalClient } = await import("@mongodb-js-preview/atlas-local"); + + const client = AtlasLocalClient.connect(); + this.session.setAtlasLocalClient(client); + } catch (error) { + console.warn( + "Failed to initialize Atlas Local client, atlas-local tools will be disabled (error: ", + error, + ")" + ); + } + } + + private registerTools(): void { + for (const toolConstructor of [...AtlasTools, ...AtlasLocalTools, ...MongoDbTools]) { const tool = new toolConstructor(this.session, this.userConfig, this.telemetry); if (tool.register(this)) { this.tools.push(tool); diff --git a/src/tools/atlasLocal/atlasLocalTool.ts b/src/tools/atlasLocal/atlasLocalTool.ts index c46bc070..c2c400d1 100644 --- a/src/tools/atlasLocal/atlasLocalTool.ts +++ b/src/tools/atlasLocal/atlasLocalTool.ts @@ -2,17 +2,31 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import type { TelemetryToolMetadata, ToolArgs, ToolCategory } from "../tool.js"; import { ToolBase } from "../tool.js"; import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type AtlasLocal from "@mongodb-js-preview/atlas-local"; +import type { Client } from "@mongodb-js-preview/atlas-local"; export abstract class AtlasLocalToolBase extends ToolBase { public category: ToolCategory = "atlas-local"; - // Will be injected by BuildAtlasLocalTools() in atlasLocal/tools.ts - public client?: AtlasLocal.Client; protected verifyAllowed(): boolean { - return this.client !== undefined && super.verifyAllowed(); + return this.session.atlasLocalClient !== undefined && super.verifyAllowed(); } + protected async execute(): Promise { + // Get the client + const client = this.session.atlasLocalClient; + + // If the client is not found, throw an error + // This should never happen, because the tool should have been disabled. + // verifyAllowed in the base class returns false if the client is not found + if (!client) { + throw new Error("Atlas Local client not found, tool should have been disabled."); + } + + return this.executeWithAtlasLocalClient(client); + } + + protected abstract executeWithAtlasLocalClient(client: Client): Promise; + protected handleError( error: unknown, args: ToolArgs diff --git a/src/tools/atlasLocal/read/listDeployments.ts b/src/tools/atlasLocal/read/listDeployments.ts index a58fb5f5..3716efd7 100644 --- a/src/tools/atlasLocal/read/listDeployments.ts +++ b/src/tools/atlasLocal/read/listDeployments.ts @@ -3,6 +3,7 @@ import { AtlasLocalToolBase } from "../atlasLocalTool.js"; import type { OperationType } from "../../tool.js"; import { formatUntrustedData } from "../../tool.js"; import type { Deployment } from "@mongodb-js-preview/atlas-local"; +import type { Client } from "@mongodb-js-preview/atlas-local"; export class ListDeploymentsTool extends AtlasLocalToolBase { public name = "atlas-local-list-deployments"; @@ -10,17 +11,7 @@ export class ListDeploymentsTool extends AtlasLocalToolBase { public operationType: OperationType = "read"; protected argsShape = {}; - protected async execute(): Promise { - // Get the client - const client = this.client; - - // If the client is not found, throw an error - // This should never happen, because the tool should have been disabled. - // verifyAllowed in the base class returns false if the client is not found - if (!client) { - throw new Error("Atlas Local client not found, tool should have been disabled."); - } - + protected async executeWithAtlasLocalClient(client: Client): Promise { // List the deployments const deployments = await client.listDeployments(); diff --git a/src/tools/atlasLocal/tools.ts b/src/tools/atlasLocal/tools.ts index 801b63f7..6d8cf7a5 100644 --- a/src/tools/atlasLocal/tools.ts +++ b/src/tools/atlasLocal/tools.ts @@ -1,33 +1,3 @@ import { ListDeploymentsTool } from "./read/listDeployments.js"; -import type AtlasLocal from "@mongodb-js-preview/atlas-local"; -// Don't use this directly, use BuildAtlasLocalTools instead -const atlasLocalTools = [ListDeploymentsTool]; - -// Build the Atlas Local tools -export const BuildAtlasLocalTools = async (): Promise => { - // Initialize the Atlas Local client - const client = await GetAtlasLocalClient(); - - // If the client is found, set it on the tools - // On unsupported platforms, the client will be undefined - if (client) { - // Set the client on the tools - atlasLocalTools.forEach((tool) => { - tool.prototype.client = client; - }); - } - - return atlasLocalTools; -}; - -export const GetAtlasLocalClient = async (): Promise => { - try { - const { Client: AtlasLocalClient } = await import("@mongodb-js-preview/atlas-local"); - return AtlasLocalClient.connect(); - } catch (error) { - // We only get here if the user is running atlas-local on a unsupported platform - console.warn("Atlas Local native binding not available:", error); - return undefined; - } -}; +export const AtlasLocalTools = [ListDeploymentsTool]; diff --git a/tests/integration/tools/atlas-local/listDeployments.test.ts b/tests/integration/tools/atlas-local/listDeployments.test.ts index bced623f..3083ccd5 100644 --- a/tests/integration/tools/atlas-local/listDeployments.test.ts +++ b/tests/integration/tools/atlas-local/listDeployments.test.ts @@ -9,6 +9,8 @@ import { describe, expect, it } from "vitest"; const isMacOSInGitHubActions = process.platform === "darwin" && process.env.GITHUB_ACTIONS === "true"; +// Docker is not available on macOS in GitHub Actions +// That's why we skip the tests on macOS in GitHub Actions describe("atlas-local-list-deployments", () => { const integration = setupIntegrationTest( () => defaultTestConfig, From 50637ea6589571d7c5682c3b8e8d2853d061a0af Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Fri, 5 Sep 2025 10:59:41 +0100 Subject: [PATCH 08/14] Marked atlas-local as an optional dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6245c183..bcb01e8c 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,6 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.17.4", - "@mongodb-js-preview/atlas-local": "^0.0.0-preview.1", "@mongodb-js/device-id": "^0.3.1", "@mongodb-js/devtools-connect": "^3.9.3", "@mongodb-js/devtools-proxy-support": "^0.5.2", @@ -122,6 +121,7 @@ "node": "^20.19.0 || ^22.12.0 ||>= 23.0.0" }, "optionalDependencies": { + "@mongodb-js-preview/atlas-local": "^0.0.0-preview.1", "kerberos": "^2.2.2" } } From da63baf7829ee2c7937c855369d6688aaf6c94d2 Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Fri, 5 Sep 2025 11:22:19 +0100 Subject: [PATCH 09/14] Load atlas-local tools async --- src/server.ts | 29 ++++++++++++++++++++++++----- src/tools/tool.ts | 2 ++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/server.ts b/src/server.ts index c8fb6319..3972e744 100644 --- a/src/server.ts +++ b/src/server.ts @@ -19,7 +19,7 @@ import { UnsubscribeRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import assert from "assert"; -import type { ToolBase } from "./tools/tool.js"; +import type { ToolBase, ToolConstructor } from "./tools/tool.js"; import { validateConnectionString } from "./helpers/connectionOptions.js"; export interface ServerOptions { @@ -61,11 +61,12 @@ export class Server { this.mcpServer.server.registerCapabilities({ logging: {}, resources: { listChanged: true, subscribe: true } }); - await this.tryInitializeAtlasLocalClient(); - // TODO: Eventually we might want to make tools reactive too instead of relying on custom logic. this.registerTools(); + // Atlas Local tools are optional and require async initialization + void this.registerAtlasLocalTools(); + // This is a workaround for an issue we've seen with some models, where they'll see that everything in the `arguments` // object is optional, and then not pass it at all. However, the MCP server expects the `arguments` object to be if // the tool accepts any arguments, even if they're all optional. @@ -195,13 +196,27 @@ export class Server { this.telemetry.emitEvents([event]).catch(() => {}); } - private async tryInitializeAtlasLocalClient(): Promise { + private async registerAtlasLocalTools(): Promise { try { + // Import Atlas Local client asyncronously + // This will fail on unsupported platforms const { Client: AtlasLocalClient } = await import("@mongodb-js-preview/atlas-local"); + // Connect to Atlas Local client + // This will fail if docker is not running const client = AtlasLocalClient.connect(); + + // Set Atlas Local client this.session.setAtlasLocalClient(client); + + // Register Atlas Local tools + this.registerToolInstances(AtlasLocalTools); } catch (error) { + // If Atlas Local tools are disabled, don't log an error + if (this.userConfig.disabledTools.includes("atlas-local")) { + return; + } + console.warn( "Failed to initialize Atlas Local client, atlas-local tools will be disabled (error: ", error, @@ -211,7 +226,11 @@ export class Server { } private registerTools(): void { - for (const toolConstructor of [...AtlasTools, ...AtlasLocalTools, ...MongoDbTools]) { + this.registerToolInstances([...AtlasTools, ...MongoDbTools]); + } + + private registerToolInstances(tools: Array): void { + for (const toolConstructor of tools) { const tool = new toolConstructor(this.session, this.userConfig, this.telemetry); if (tool.register(this)) { this.tools.push(tool); diff --git a/src/tools/tool.ts b/src/tools/tool.ts index 2c3587ca..6e972c22 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -18,6 +18,8 @@ export type TelemetryToolMetadata = { orgId?: string; }; +export type ToolConstructor = new (session: Session, config: UserConfig, telemetry: Telemetry) => ToolBase; + export abstract class ToolBase { public abstract name: string; From db2f0490243c7f0a1e66d6e71645a9694d678491 Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Mon, 8 Sep 2025 08:27:53 +0100 Subject: [PATCH 10/14] created waitUntilMcpClientIsSet and use test helper --- package-lock.json | 3 +- tests/integration/helpers.ts | 32 +++++++++++++++++++ .../tools/atlas-local/listDeployments.test.ts | 15 ++++++--- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 49bd6af5..229e3081 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.4", - "@mongodb-js-preview/atlas-local": "^0.0.0-preview.1", "@mongodb-js/device-id": "^0.3.1", "@mongodb-js/devtools-connect": "^3.9.3", "@mongodb-js/devtools-proxy-support": "^0.5.2", @@ -76,6 +75,7 @@ "node": "^20.19.0 || ^22.12.0 ||>= 23.0.0" }, "optionalDependencies": { + "@mongodb-js-preview/atlas-local": "^0.0.0-preview.1", "kerberos": "^2.2.2" } }, @@ -2054,6 +2054,7 @@ "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local/-/atlas-local-0.0.0-preview.1.tgz", "integrity": "sha512-py3roloK+dyq9bCU139f3JdFykige1kWwUli9qWE4daODFdJ0mvQPN1EChw3lzI4rv53cX1CvApbh20liOukoQ==", "license": "Apache-2.0", + "optional": true, "engines": { "node": ">= 12.22.0 < 13 ||>= 14.17.0 < 15 ||>= 15.12.0 < 16 ||>= 16.0.0" }, diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 1f28995d..e2a7bce9 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -15,6 +15,7 @@ import { MCPConnectionManager } from "../../src/common/connectionManager.js"; import { DeviceId } from "../../src/helpers/deviceId.js"; import { connectionErrorHandler } from "../../src/common/connectionErrorHandler.js"; import { Keychain } from "../../src/common/keychain.js"; +import type { Client as AtlasLocalClient } from "@mongodb-js-preview/atlas-local"; interface ParameterInfo { name: string; @@ -345,6 +346,37 @@ export function waitUntil( }); } +export function waitUntilMcpClientIsSet( + mcpServer: Server, + signal: AbortSignal, + timeout: number = 5000 +): Promise { + let ts: NodeJS.Timeout | undefined; + + const timeoutSignal = AbortSignal.timeout(timeout); + const combinedSignal = AbortSignal.any([signal, timeoutSignal]); + + return new Promise((resolve, reject) => { + ts = setInterval(() => { + if (combinedSignal.aborted) { + return reject(new Error(`Aborted: ${combinedSignal.reason}`)); + } + + // wait until session.client != undefined + // do not wait more than 1 second, should take a few milliseconds at most + // try every 50ms to see if the client is set, if it's not set after 1 second, throw an error + const client = mcpServer.session.atlasLocalClient; + if (client) { + return resolve(client); + } + }, 100); + }).finally(() => { + if (ts !== undefined) { + clearInterval(ts); + } + }); +} + export function getDataFromUntrustedContent(content: string): string { const regex = /^[ \t]*(?.*)^[ \t]*<\/untrusted-user-data-[0-9a-f\\-]*>/gms; const match = regex.exec(content); diff --git a/tests/integration/tools/atlas-local/listDeployments.test.ts b/tests/integration/tools/atlas-local/listDeployments.test.ts index 3083ccd5..ac41cf8c 100644 --- a/tests/integration/tools/atlas-local/listDeployments.test.ts +++ b/tests/integration/tools/atlas-local/listDeployments.test.ts @@ -4,6 +4,7 @@ import { expectDefined, getResponseElements, setupIntegrationTest, + waitUntilMcpClientIsSet, } from "../../helpers.js"; import { describe, expect, it } from "vitest"; @@ -17,7 +18,9 @@ describe("atlas-local-list-deployments", () => { () => defaultDriverOptions ); - it.skipIf(isMacOSInGitHubActions)("should have the atlas-local-list-deployments tool", async () => { + it.skipIf(isMacOSInGitHubActions)("should have the atlas-local-list-deployments tool", async ({ signal }) => { + await waitUntilMcpClientIsSet(integration.mcpServer(), signal); + const { tools } = await integration.mcpClient().listTools(); const listDeployments = tools.find((tool) => tool.name === "atlas-local-list-deployments"); expectDefined(listDeployments); @@ -25,14 +28,16 @@ describe("atlas-local-list-deployments", () => { it.skipIf(!isMacOSInGitHubActions)( "[MacOS in GitHub Actions] should not have the atlas-local-list-deployments tool", - async () => { + async ({ signal }) => { + await waitUntilMcpClientIsSet(integration.mcpServer(), signal); const { tools } = await integration.mcpClient().listTools(); const listDeployments = tools.find((tool) => tool.name === "atlas-local-list-deployments"); expect(listDeployments).toBeUndefined(); } ); - it.skipIf(isMacOSInGitHubActions)("should have correct metadata", async () => { + it.skipIf(isMacOSInGitHubActions)("should have correct metadata", async ({ signal }) => { + await waitUntilMcpClientIsSet(integration.mcpServer(), signal); const { tools } = await integration.mcpClient().listTools(); const listDeployments = tools.find((tool) => tool.name === "atlas-local-list-deployments"); expectDefined(listDeployments); @@ -41,7 +46,9 @@ describe("atlas-local-list-deployments", () => { expect(listDeployments.inputSchema.properties).toEqual({}); }); - it.skipIf(isMacOSInGitHubActions)("should not crash when calling the tool", async () => { + it.skipIf(isMacOSInGitHubActions)("should not crash when calling the tool", async ({ signal }) => { + await waitUntilMcpClientIsSet(integration.mcpServer(), signal); + const response = await integration.mcpClient().callTool({ name: "atlas-local-list-deployments", arguments: {}, From c4362048f1e8db9560a31941202ec7a62b00e99c Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Mon, 8 Sep 2025 08:37:53 +0100 Subject: [PATCH 11/14] Expect timeout on MACOs on Github Actions --- tests/integration/tools/atlas-local/listDeployments.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/tools/atlas-local/listDeployments.test.ts b/tests/integration/tools/atlas-local/listDeployments.test.ts index ac41cf8c..38d2446c 100644 --- a/tests/integration/tools/atlas-local/listDeployments.test.ts +++ b/tests/integration/tools/atlas-local/listDeployments.test.ts @@ -29,7 +29,9 @@ describe("atlas-local-list-deployments", () => { it.skipIf(!isMacOSInGitHubActions)( "[MacOS in GitHub Actions] should not have the atlas-local-list-deployments tool", async ({ signal }) => { - await waitUntilMcpClientIsSet(integration.mcpServer(), signal); + // This should throw an error because the client is not set within the timeout of 5 seconds (default) + await expect(waitUntilMcpClientIsSet(integration.mcpServer(), signal)).rejects.toThrow(); + const { tools } = await integration.mcpClient().listTools(); const listDeployments = tools.find((tool) => tool.name === "atlas-local-list-deployments"); expect(listDeployments).toBeUndefined(); From 558411c5898ff38f39f4b3505f382cd69cb1e15f Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Mon, 8 Sep 2025 08:40:13 +0100 Subject: [PATCH 12/14] move include 'atlas-local' enabled check earlier --- src/server.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/server.ts b/src/server.ts index ec075b76..74afc93b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -201,6 +201,11 @@ export class Server { } private async registerAtlasLocalTools(): Promise { + // If Atlas Local tools are disabled, don't attempt to connect to the client + if (this.userConfig.disabledTools.includes("atlas-local")) { + return; + } + try { // Import Atlas Local client asyncronously // This will fail on unsupported platforms @@ -216,11 +221,6 @@ export class Server { // Register Atlas Local tools this.registerToolInstances(AtlasLocalTools); } catch (error) { - // If Atlas Local tools are disabled, don't log an error - if (this.userConfig.disabledTools.includes("atlas-local")) { - return; - } - console.warn( "Failed to initialize Atlas Local client, atlas-local tools will be disabled (error: ", error, From c79cb482b334de07fc5f6ad2381c0f13e37ae4eb Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Mon, 8 Sep 2025 08:47:52 +0100 Subject: [PATCH 13/14] Improve throw error comment --- src/tools/atlasLocal/atlasLocalTool.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/tools/atlasLocal/atlasLocalTool.ts b/src/tools/atlasLocal/atlasLocalTool.ts index c2c400d1..9780b791 100644 --- a/src/tools/atlasLocal/atlasLocalTool.ts +++ b/src/tools/atlasLocal/atlasLocalTool.ts @@ -16,8 +16,12 @@ export abstract class AtlasLocalToolBase extends ToolBase { const client = this.session.atlasLocalClient; // If the client is not found, throw an error - // This should never happen, because the tool should have been disabled. - // verifyAllowed in the base class returns false if the client is not found + // This should never happen: + // - atlas-local tools are only added after the client is set + // this means that if we were unable to get the client, the tool will not be registered + // - in case the tool was registered by accident + // verifyAllowed in the base class would still return false preventing the tool from being registered, + // preventing the tool from being executed if (!client) { throw new Error("Atlas Local client not found, tool should have been disabled."); } From e4fb2827453ee51e8399c788f797f277fc74e6d6 Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Mon, 8 Sep 2025 09:30:33 +0100 Subject: [PATCH 14/14] handle potential future developer error more gracefully --- src/tools/atlasLocal/atlasLocalTool.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/tools/atlasLocal/atlasLocalTool.ts b/src/tools/atlasLocal/atlasLocalTool.ts index 9780b791..8aca9d55 100644 --- a/src/tools/atlasLocal/atlasLocalTool.ts +++ b/src/tools/atlasLocal/atlasLocalTool.ts @@ -23,7 +23,16 @@ export abstract class AtlasLocalToolBase extends ToolBase { // verifyAllowed in the base class would still return false preventing the tool from being registered, // preventing the tool from being executed if (!client) { - throw new Error("Atlas Local client not found, tool should have been disabled."); + return { + content: [ + { + type: "text", + text: `Something went wrong on our end, this tool should have been disabled but it was not. +please log a ticket here: https://github.com/mongodb-js/mongodb-mcp-server/issues/new?template=bug_report.yml`, + }, + ], + isError: true, + }; } return this.executeWithAtlasLocalClient(client);

AltStyle によって変換されたページ (->オリジナル) /