diff --git a/package-lock.json b/package-lock.json index d5fdf990..747320d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,7 +75,7 @@ "node": "^20.19.0 || ^22.12.0 || >= 23.0.0" }, "optionalDependencies": { - "@mongodb-js-preview/atlas-local": "^0.0.0-preview.3", + "@mongodb-js-preview/atlas-local": "^0.0.0-preview.6", "kerberos": "^2.2.2" } }, @@ -2050,26 +2050,26 @@ } }, "node_modules/@mongodb-js-preview/atlas-local": { - "version": "0.0.0-preview.3", - "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local/-/atlas-local-0.0.0-preview.3.tgz", - "integrity": "sha512-Rq1xITOqTlGxr2mIQ4Ig0ugOs5cNzILN5g/zTm5RoXE6NHPY+qi86aNpQnJp/bQa4XR5BRvm4ztzFtBk1OGTvg==", + "version": "0.0.0-preview.6", + "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local/-/atlas-local-0.0.0-preview.6.tgz", + "integrity": "sha512-UoLhTzyrL+99hGwcQyjjtOuWC66zQHgWhRXEj+tQVfDP97v0aKWctRigGVSmMPWnc2r5JdAlfR2M+sULfl7eCg==", "license": "Apache-2.0", "optional": true, "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.3", - "@mongodb-js-preview/atlas-local-darwin-x64": "0.0.0-preview.3", - "@mongodb-js-preview/atlas-local-linux-arm64-gnu": "0.0.0-preview.3", - "@mongodb-js-preview/atlas-local-linux-x64-gnu": "0.0.0-preview.3", - "@mongodb-js-preview/atlas-local-win32-x64-msvc": "0.0.0-preview.3" + "@mongodb-js-preview/atlas-local-darwin-arm64": "0.0.0-preview.6", + "@mongodb-js-preview/atlas-local-darwin-x64": "0.0.0-preview.6", + "@mongodb-js-preview/atlas-local-linux-arm64-gnu": "0.0.0-preview.6", + "@mongodb-js-preview/atlas-local-linux-x64-gnu": "0.0.0-preview.6", + "@mongodb-js-preview/atlas-local-win32-x64-msvc": "0.0.0-preview.6" } }, "node_modules/@mongodb-js-preview/atlas-local-darwin-arm64": { - "version": "0.0.0-preview.3", - "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-darwin-arm64/-/atlas-local-darwin-arm64-0.0.0-preview.3.tgz", - "integrity": "sha512-qEuXvFr1JtEdaPb85jP+69yCJIiXZHsQegOmlexpcrJwO6HXsn0JXryvO0wgay3BTiHmtUkmPvFcl2K4b6Q2rw==", + "version": "0.0.0-preview.6", + "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-darwin-arm64/-/atlas-local-darwin-arm64-0.0.0-preview.6.tgz", + "integrity": "sha512-xB2h87mdq0W1tq7wZmpUIzS/wmUOrEKRqUYKNbv1g6M8pY/1ln2OCPi1OGj1LUnT8dC/jvklh92Ittj6eb5leg==", "cpu": [ "arm64" ], @@ -2083,9 +2083,9 @@ } }, "node_modules/@mongodb-js-preview/atlas-local-darwin-x64": { - "version": "0.0.0-preview.3", - "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-darwin-x64/-/atlas-local-darwin-x64-0.0.0-preview.3.tgz", - "integrity": "sha512-QghS4XmDpaPZdtMev1XKMfFdJ3Tvhfaaa8ZTV3mIQOFuy200eBwTM/xQaZtBLw9TQUqK7pvxH+nvv+iBeNMK1A==", + "version": "0.0.0-preview.6", + "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-darwin-x64/-/atlas-local-darwin-x64-0.0.0-preview.6.tgz", + "integrity": "sha512-5o4fXyXm6lOB1vCOIxRiF2p4NpChFZtjLSvsal+nxHBHJb/tevl7k8kE+dt7a5guO7CW1TCfy71E58NQxxaxkQ==", "cpu": [ "x64" ], @@ -2099,9 +2099,9 @@ } }, "node_modules/@mongodb-js-preview/atlas-local-linux-arm64-gnu": { - "version": "0.0.0-preview.3", - "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-linux-arm64-gnu/-/atlas-local-linux-arm64-gnu-0.0.0-preview.3.tgz", - "integrity": "sha512-b7IqwkrZ7VL8zDJhu79hY6hj7RqVcFxCF/QV5xR2tsfzIvoqChBilw7AcsuqGS+vws2aBhMp7qKl+YkaSuRblg==", + "version": "0.0.0-preview.6", + "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-linux-arm64-gnu/-/atlas-local-linux-arm64-gnu-0.0.0-preview.6.tgz", + "integrity": "sha512-ul8odgYVF5jpW56u2WHMs82Pmaf/6giIjQxHbMMafcHUPC87R08+Y0qIp8iY0vl4lTE5CBPXNzIICyPp+Snw9Q==", "cpu": [ "arm64" ], @@ -2115,9 +2115,9 @@ } }, "node_modules/@mongodb-js-preview/atlas-local-linux-x64-gnu": { - "version": "0.0.0-preview.3", - "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-linux-x64-gnu/-/atlas-local-linux-x64-gnu-0.0.0-preview.3.tgz", - "integrity": "sha512-oR8D5u5+CSYfS206Mw4MkFy5HQS6H7+uGnIgBCE/qK7OQ/WVi9TZIfD+hXrtoSLPOlitmcyODdWGcBfBmb3C/Q==", + "version": "0.0.0-preview.6", + "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-linux-x64-gnu/-/atlas-local-linux-x64-gnu-0.0.0-preview.6.tgz", + "integrity": "sha512-90t5SynjHFvhEgh6xXEr86tN5VnKHDK6LrOb9Yuf/IlMHNmFm85Cr/OD/B5jREGxlLl+rWWZE7f+EZn/5pWVgw==", "cpu": [ "x64" ], @@ -2131,9 +2131,9 @@ } }, "node_modules/@mongodb-js-preview/atlas-local-win32-x64-msvc": { - "version": "0.0.0-preview.3", - "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-win32-x64-msvc/-/atlas-local-win32-x64-msvc-0.0.0-preview.3.tgz", - "integrity": "sha512-epjn0O61f9hKhyTyR8fhYkhEEAJI8kZARBuO4bdvbVJOQf6i/v1fY0OCaPLARznHj1ap1IXlQFax+gSF/4wMPQ==", + "version": "0.0.0-preview.6", + "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-win32-x64-msvc/-/atlas-local-win32-x64-msvc-0.0.0-preview.6.tgz", + "integrity": "sha512-sxwT5NMMffrHNuPbaywuuu91OZC0CTSJaTQpjdjYWQgdeQDMDyWFNWS6k6irgxbgEMwacv6HUKPuOE/fC+/uNA==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index 58004422..4a685ad1 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "node": "^20.19.0 || ^22.12.0 || >= 23.0.0" }, "optionalDependencies": { - "@mongodb-js-preview/atlas-local": "^0.0.0-preview.3", + "@mongodb-js-preview/atlas-local": "^0.0.0-preview.6", "kerberos": "^2.2.2" } } diff --git a/src/common/connectionErrorHandler.ts b/src/common/connectionErrorHandler.ts index 9de63bef..30b63796 100644 --- a/src/common/connectionErrorHandler.ts +++ b/src/common/connectionErrorHandler.ts @@ -17,12 +17,29 @@ export const connectionErrorHandler: ConnectionErrorHandler = (error, { availabl .filter((t) => t.operationType === "connect") .sort((a, b) => a.category.localeCompare(b.category)); // Sort Atlas tools before MongoDB tools - // Find the first Atlas connect tool if available and suggest to the LLM to use it. - // Note: if we ever have multiple Atlas connect tools, we may want to refine this logic to select the most appropriate one. + // Find what Atlas connect tools are available and suggest when the LLM should to use each. If no Atlas tools are found, return a suggestion for the MongoDB connect tool. const atlasConnectTool = connectTools?.find((t) => t.category === "atlas"); - const llmConnectHint = atlasConnectTool - ? `Note to LLM: prefer using the "${atlasConnectTool.name}" tool to connect to an Atlas cluster over using a connection string. Make sure to ask the user to specify a cluster name they want to connect to or ask them if they want to use the "list-clusters" tool to list all their clusters. Do not invent cluster names or connection strings unless the user has explicitly specified them. If they've previously connected to MongoDB using MCP, you can ask them if they want to reconnect using the same cluster/connection.` - : "Note to LLM: do not invent connection strings and explicitly ask the user to provide one. If they have previously connected to MongoDB using MCP, you can ask them if they want to reconnect using the same connection string."; + const atlasLocalConnectTool = connectTools?.find((t) => t.category === "atlas-local"); + + const llmConnectHint = ((): string => { + const hints: string[] = []; + + if (atlasConnectTool) { + hints.push( + `Note to LLM: prefer using the "${atlasConnectTool.name}" tool to connect to an Atlas cluster over using a connection string. Make sure to ask the user to specify a cluster name they want to connect to or ask them if they want to use the "list-clusters" tool to list all their clusters. Do not invent cluster names or connection strings unless the user has explicitly specified them. If they've previously connected to MongoDB using MCP, you can ask them if they want to reconnect using the same cluster/connection.` + ); + } + + if (atlasLocalConnectTool) { + hints.push( + `Note to LLM: For MongoDB Atlas Local deployments, ask the user to either provide a connection string, specify a deployment name, or use "atlas-local-list-deployments" to show available local deployments. If a deployment name is provided, prefer using the "${atlasLocalConnectTool.name}" tool. If a connection string is provided, prefer using the "connect" tool. Do not invent deployment names or connection strings unless the user has explicitly specified them. If they've previously connected to a MongoDB Atlas Local deployment using MCP, you can ask them if they want to reconnect using the same deployment.` + ); + } + + return hints.length > 0 + ? hints.join("\n") + : "Note to LLM: do not invent connection strings and explicitly ask the user to provide one. If they have previously connected to MongoDB using MCP, you can ask them if they want to reconnect using the same connection string."; + })(); const connectToolsNames = connectTools?.map((t) => `"${t.name}"`).join(", "); const additionalPromptForConnectivity: { type: "text"; text: string }[] = []; diff --git a/src/tools/atlasLocal/atlasLocalTool.ts b/src/tools/atlasLocal/atlasLocalTool.ts index b67e83bd..4c97f3e7 100644 --- a/src/tools/atlasLocal/atlasLocalTool.ts +++ b/src/tools/atlasLocal/atlasLocalTool.ts @@ -48,6 +48,20 @@ please log a ticket here: https://github.com/mongodb-js/mongodb-mcp-server/issue args: ToolArgs ): Promise | CallToolResult { // Error Handling for expected Atlas Local errors go here + const errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes("No such container")) { + const deploymentName = + "deploymentName" in args ? (args.deploymentName as string) : "the specified deployment"; + return { + content: [ + { + type: "text", + text: `The Atlas Local deployment "${deploymentName}" was not found. Please check the deployment name or use "atlas-local-list-deployments" to see available deployments.`, + }, + ], + isError: true, + }; + } // For other types of errors, use the default error handling from the base class return super.handleError(error, args); diff --git a/src/tools/atlasLocal/connect/connectDeployment.ts b/src/tools/atlasLocal/connect/connectDeployment.ts new file mode 100644 index 00000000..e2420aef --- /dev/null +++ b/src/tools/atlasLocal/connect/connectDeployment.ts @@ -0,0 +1,34 @@ +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { AtlasLocalToolBase } from "../atlasLocalTool.js"; +import type { OperationType, ToolArgs } from "../../tool.js"; +import type { Client } from "@mongodb-js-preview/atlas-local"; +import { z } from "zod"; + +export class ConnectDeploymentTool extends AtlasLocalToolBase { + public name = "atlas-local-connect-deployment"; + protected description = "Connect to a MongoDB Atlas Local deployment"; + public operationType: OperationType = "connect"; + protected argsShape = { + deploymentName: z.string().describe("Name of the deployment to connect to"), + }; + + protected async executeWithAtlasLocalClient( + client: Client, + { deploymentName }: ToolArgs + ): Promise { + // Get the connection string for the deployment + const connectionString = await client.getConnectionString(deploymentName); + + // Connect to the deployment + await this.session.connectToMongoDB({ connectionString }); + + return { + content: [ + { + type: "text", + text: `Successfully connected to Atlas Local deployment "${deploymentName}".`, + }, + ], + }; + } +} diff --git a/src/tools/atlasLocal/tools.ts b/src/tools/atlasLocal/tools.ts index 655ae1dc..451362ce 100644 --- a/src/tools/atlasLocal/tools.ts +++ b/src/tools/atlasLocal/tools.ts @@ -1,5 +1,6 @@ import { DeleteDeploymentTool } from "./delete/deleteDeployment.js"; import { ListDeploymentsTool } from "./read/listDeployments.js"; import { CreateDeploymentTool } from "./create/createDeployment.js"; +import { ConnectDeploymentTool } from "./connect/connectDeployment.js"; -export const AtlasLocalTools = [ListDeploymentsTool, DeleteDeploymentTool, CreateDeploymentTool]; +export const AtlasLocalTools = [ListDeploymentsTool, DeleteDeploymentTool, CreateDeploymentTool, ConnectDeploymentTool]; diff --git a/tests/accuracy/connectDeployment.test.ts b/tests/accuracy/connectDeployment.test.ts new file mode 100644 index 00000000..57ca7a05 --- /dev/null +++ b/tests/accuracy/connectDeployment.test.ts @@ -0,0 +1,70 @@ +import { describeAccuracyTests } from "./sdk/describeAccuracyTests.js"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; + +describeAccuracyTests([ + { + prompt: "Connect to the local MongoDB cluster called 'my-database'", + expectedToolCalls: [ + { + toolName: "atlas-local-connect-deployment", + parameters: { + deploymentIdOrName: "my-database", + }, + }, + ], + }, + { + prompt: "Connect to the local MongoDB atlas database called 'my-instance'", + expectedToolCalls: [ + { + toolName: "atlas-local-connect-deployment", + parameters: { + deploymentIdOrName: "my-instance", + }, + }, + ], + }, + { + prompt: "If and only if, the local MongoDB deployment 'local-mflix' exists, then connect to it", + mockedTools: { + "atlas-local-list-deployments": (): CallToolResult => ({ + content: [ + { type: "text", text: "Found 1 deployment:" }, + { + type: "text", + text: "Deployment Name | State | MongoDB Version\n----------------|----------------|----------------\nlocal-mflix | Running | 6.0", + }, + ], + }), + }, + expectedToolCalls: [ + { + toolName: "atlas-local-list-deployments", + parameters: {}, + }, + { + toolName: "atlas-local-connect-deployment", + parameters: { + deploymentIdOrName: "local-mflix", + }, + }, + ], + }, + { + prompt: "Connect to a new local MongoDB cluster named 'local-mflix'", + expectedToolCalls: [ + { + toolName: "atlas-local-create-deployment", + parameters: { + deploymentName: "local-mflix", + }, + }, + { + toolName: "atlas-local-connect-deployment", + parameters: { + deploymentIdOrName: "local-mflix", + }, + }, + ], + }, +]); diff --git a/tests/integration/tools/atlas-local/connectDeployment.test.ts b/tests/integration/tools/atlas-local/connectDeployment.test.ts new file mode 100644 index 00000000..16368211 --- /dev/null +++ b/tests/integration/tools/atlas-local/connectDeployment.test.ts @@ -0,0 +1,114 @@ +import { beforeEach } from "vitest"; +import { + defaultDriverOptions, + defaultTestConfig, + expectDefined, + getResponseElements, + setupIntegrationTest, + validateToolMetadata, + waitUntilMcpClientIsSet, +} from "../../helpers.js"; +import { afterEach, describe, expect, it } from "vitest"; + +const isMacOSInGitHubActions = process.platform === "darwin" && process.env.GITHUB_ACTIONS === "true"; +const integration = setupIntegrationTest( + () => defaultTestConfig, + () => defaultDriverOptions +); + +// Docker is not available on macOS in GitHub Actions +// That's why we skip the tests on macOS in GitHub Actions +describe.skipIf(isMacOSInGitHubActions)("atlas-local-connect-deployment", () => { + beforeEach(async ({ signal }) => { + await waitUntilMcpClientIsSet(integration.mcpServer(), signal); + }); + + validateToolMetadata(integration, "atlas-local-connect-deployment", "Connect to a MongoDB Atlas Local deployment", [ + { + name: "deploymentName", + type: "string", + description: "Name of the deployment to connect to", + required: true, + }, + ]); + + it("should have the atlas-local-connect-deployment tool", async () => { + const { tools } = await integration.mcpClient().listTools(); + const connectDeployment = tools.find((tool) => tool.name === "atlas-local-connect-deployment"); + expectDefined(connectDeployment); + }); + + it("should return 'no such container' error when connecting to non-existent deployment", async () => { + const deploymentName = "non-existent"; + const response = await integration.mcpClient().callTool({ + name: "atlas-local-connect-deployment", + arguments: { deploymentName }, + }); + const elements = getResponseElements(response.content); + expect(elements.length).toBeGreaterThanOrEqual(1); + expect(elements[0]?.text).toContain( + `The Atlas Local deployment "${deploymentName}" was not found. Please check the deployment name or use "atlas-local-list-deployments" to see available deployments.` + ); + }); +}); + +describe.skipIf(isMacOSInGitHubActions)("atlas-local-connect-deployment with deployments", () => { + let deploymentName: string = ""; + let deploymentNamesToCleanup: string[] = []; + + beforeEach(async ({ signal }) => { + await waitUntilMcpClientIsSet(integration.mcpServer(), signal); + + // Create deployments + deploymentName = `test-deployment-1-${Date.now()}`; + deploymentNamesToCleanup.push(deploymentName); + await integration.mcpClient().callTool({ + name: "atlas-local-create-deployment", + arguments: { deploymentName }, + }); + + const anotherDeploymentName = `test-deployment-2-${Date.now()}`; + deploymentNamesToCleanup.push(anotherDeploymentName); + await integration.mcpClient().callTool({ + name: "atlas-local-create-deployment", + arguments: { deploymentName: anotherDeploymentName }, + }); + }); + + afterEach(async () => { + // Delete all created deployments + for (const deploymentNameToCleanup of deploymentNamesToCleanup) { + try { + await integration.mcpClient().callTool({ + name: "atlas-local-delete-deployment", + arguments: { deploymentName: deploymentNameToCleanup }, + }); + } catch (error) { + console.warn(`Failed to delete deployment ${deploymentNameToCleanup}:`, error); + } + } + deploymentNamesToCleanup = []; + }); + + it("should connect to correct deployment when calling the tool", async () => { + // Connect to the deployment + const response = await integration.mcpClient().callTool({ + name: "atlas-local-connect-deployment", + arguments: { deploymentName }, + }); + const elements = getResponseElements(response.content); + expect(elements.length).toBeGreaterThanOrEqual(1); + expect(elements[0]?.text).toContain(`Successfully connected to Atlas Local deployment "${deploymentName}".`); + }); +}); + +describe.skipIf(!isMacOSInGitHubActions)("atlas-local-connect-deployment [MacOS in GitHub Actions]", () => { + it("should not have the atlas-local-connect-deployment tool", async ({ 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 connectDeployment = tools.find((tool) => tool.name === "atlas-local-connect-deployment"); + expect(connectDeployment).toBeUndefined(); + }); +}); diff --git a/tests/integration/tools/atlas-local/deleteDeployment.test.ts b/tests/integration/tools/atlas-local/deleteDeployment.test.ts index 6956da91..f1125d53 100644 --- a/tests/integration/tools/atlas-local/deleteDeployment.test.ts +++ b/tests/integration/tools/atlas-local/deleteDeployment.test.ts @@ -52,15 +52,16 @@ describe("atlas-local-delete-deployment", () => { "should return 'no such container' error when deployment to delete does not exist", async ({ signal }) => { await waitUntilMcpClientIsSet(integration.mcpServer(), signal); + const deploymentName = "non-existent"; const response = await integration.mcpClient().callTool({ name: "atlas-local-delete-deployment", - arguments: { deploymentName: "non-existent" }, + arguments: { deploymentName }, }); const elements = getResponseElements(response.content); expect(elements.length).toBeGreaterThanOrEqual(1); expect(elements[0]?.text).toContain( - "Docker responded with status code 404: No such container: non-existent" + `The Atlas Local deployment "${deploymentName}" was not found. Please check the deployment name or use "atlas-local-list-deployments" to see available deployments.` ); } ); diff --git a/tests/integration/tools/atlas/clusters.test.ts b/tests/integration/tools/atlas/clusters.test.ts index ccb6e774..fc168318 100644 --- a/tests/integration/tools/atlas/clusters.test.ts +++ b/tests/integration/tools/atlas/clusters.test.ts @@ -238,9 +238,17 @@ describeWithAtlas("clusters", (integration) => { expect(elements[0]?.text).toContain( "You need to connect to a MongoDB instance before you can access its data." ); - expect(elements[1]?.text).toContain( - 'Please use one of the following tools: "atlas-connect-cluster", "connect" to connect to a MongoDB instance' - ); + // Check if the response contains all available test tools. + if (process.platform === "darwin" && process.env.GITHUB_ACTIONS === "true") { + // The tool atlas-local-connect-deployment may be disabled in some test environments if Docker is not available. + expect(elements[1]?.text).toContain( + 'Please use one of the following tools: "atlas-connect-cluster", "connect" to connect to a MongoDB instance' + ); + } else { + expect(elements[1]?.text).toContain( + 'Please use one of the following tools: "atlas-connect-cluster", "atlas-local-connect-deployment", "connect" to connect to a MongoDB instance' + ); + } }); }); });