Skip to content

Commit cb5d335

Browse files
authored
feat(atlas-local): Adds Atlas Local Connect Deployment tool (#612)
1 parent 0d14679 commit cb5d335

File tree

10 files changed

+295
-36
lines changed

10 files changed

+295
-36
lines changed

package-lock.json

Lines changed: 24 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@
121121
"node": "^20.19.0 || ^22.12.0 || >= 23.0.0"
122122
},
123123
"optionalDependencies": {
124-
"@mongodb-js-preview/atlas-local": "^0.0.0-preview.3",
124+
"@mongodb-js-preview/atlas-local": "^0.0.0-preview.6",
125125
"kerberos": "^2.2.2"
126126
}
127127
}

src/common/connectionErrorHandler.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,29 @@ export const connectionErrorHandler: ConnectionErrorHandler = (error, { availabl
1717
.filter((t) => t.operationType === "connect")
1818
.sort((a, b) => a.category.localeCompare(b.category)); // Sort Atlas tools before MongoDB tools
1919

20-
// Find the first Atlas connect tool if available and suggest to the LLM to use it.
21-
// Note: if we ever have multiple Atlas connect tools, we may want to refine this logic to select the most appropriate one.
20+
// 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.
2221
const atlasConnectTool = connectTools?.find((t) => t.category === "atlas");
23-
const llmConnectHint = atlasConnectTool
24-
? `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.`
25-
: "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.";
22+
const atlasLocalConnectTool = connectTools?.find((t) => t.category === "atlas-local");
23+
24+
const llmConnectHint = ((): string => {
25+
const hints: string[] = [];
26+
27+
if (atlasConnectTool) {
28+
hints.push(
29+
`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.`
30+
);
31+
}
32+
33+
if (atlasLocalConnectTool) {
34+
hints.push(
35+
`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.`
36+
);
37+
}
38+
39+
return hints.length > 0
40+
? hints.join("\n")
41+
: "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.";
42+
})();
2643

2744
const connectToolsNames = connectTools?.map((t) => `"${t.name}"`).join(", ");
2845
const additionalPromptForConnectivity: { type: "text"; text: string }[] = [];

src/tools/atlasLocal/atlasLocalTool.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ please log a ticket here: https://github.com/mongodb-js/mongodb-mcp-server/issue
4848
args: ToolArgs<typeof this.argsShape>
4949
): Promise<CallToolResult> | CallToolResult {
5050
// Error Handling for expected Atlas Local errors go here
51+
const errorMessage = error instanceof Error ? error.message : String(error);
52+
if (errorMessage.includes("No such container")) {
53+
const deploymentName =
54+
"deploymentName" in args ? (args.deploymentName as string) : "the specified deployment";
55+
return {
56+
content: [
57+
{
58+
type: "text",
59+
text: `The Atlas Local deployment "${deploymentName}" was not found. Please check the deployment name or use "atlas-local-list-deployments" to see available deployments.`,
60+
},
61+
],
62+
isError: true,
63+
};
64+
}
5165

5266
// For other types of errors, use the default error handling from the base class
5367
return super.handleError(error, args);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2+
import { AtlasLocalToolBase } from "../atlasLocalTool.js";
3+
import type { OperationType, ToolArgs } from "../../tool.js";
4+
import type { Client } from "@mongodb-js-preview/atlas-local";
5+
import { z } from "zod";
6+
7+
export class ConnectDeploymentTool extends AtlasLocalToolBase {
8+
public name = "atlas-local-connect-deployment";
9+
protected description = "Connect to a MongoDB Atlas Local deployment";
10+
public operationType: OperationType = "connect";
11+
protected argsShape = {
12+
deploymentName: z.string().describe("Name of the deployment to connect to"),
13+
};
14+
15+
protected async executeWithAtlasLocalClient(
16+
client: Client,
17+
{ deploymentName }: ToolArgs<typeof this.argsShape>
18+
): Promise<CallToolResult> {
19+
// Get the connection string for the deployment
20+
const connectionString = await client.getConnectionString(deploymentName);
21+
22+
// Connect to the deployment
23+
await this.session.connectToMongoDB({ connectionString });
24+
25+
return {
26+
content: [
27+
{
28+
type: "text",
29+
text: `Successfully connected to Atlas Local deployment "${deploymentName}".`,
30+
},
31+
],
32+
};
33+
}
34+
}

src/tools/atlasLocal/tools.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { DeleteDeploymentTool } from "./delete/deleteDeployment.js";
22
import { ListDeploymentsTool } from "./read/listDeployments.js";
33
import { CreateDeploymentTool } from "./create/createDeployment.js";
4+
import { ConnectDeploymentTool } from "./connect/connectDeployment.js";
45

5-
export const AtlasLocalTools = [ListDeploymentsTool, DeleteDeploymentTool, CreateDeploymentTool];
6+
export const AtlasLocalTools = [ListDeploymentsTool, DeleteDeploymentTool, CreateDeploymentTool, ConnectDeploymentTool];
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { describeAccuracyTests } from "./sdk/describeAccuracyTests.js";
2+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3+
4+
describeAccuracyTests([
5+
{
6+
prompt: "Connect to the local MongoDB cluster called 'my-database'",
7+
expectedToolCalls: [
8+
{
9+
toolName: "atlas-local-connect-deployment",
10+
parameters: {
11+
deploymentIdOrName: "my-database",
12+
},
13+
},
14+
],
15+
},
16+
{
17+
prompt: "Connect to the local MongoDB atlas database called 'my-instance'",
18+
expectedToolCalls: [
19+
{
20+
toolName: "atlas-local-connect-deployment",
21+
parameters: {
22+
deploymentIdOrName: "my-instance",
23+
},
24+
},
25+
],
26+
},
27+
{
28+
prompt: "If and only if, the local MongoDB deployment 'local-mflix' exists, then connect to it",
29+
mockedTools: {
30+
"atlas-local-list-deployments": (): CallToolResult => ({
31+
content: [
32+
{ type: "text", text: "Found 1 deployment:" },
33+
{
34+
type: "text",
35+
text: "Deployment Name | State | MongoDB Version\n----------------|----------------|----------------\nlocal-mflix | Running | 6.0",
36+
},
37+
],
38+
}),
39+
},
40+
expectedToolCalls: [
41+
{
42+
toolName: "atlas-local-list-deployments",
43+
parameters: {},
44+
},
45+
{
46+
toolName: "atlas-local-connect-deployment",
47+
parameters: {
48+
deploymentIdOrName: "local-mflix",
49+
},
50+
},
51+
],
52+
},
53+
{
54+
prompt: "Connect to a new local MongoDB cluster named 'local-mflix'",
55+
expectedToolCalls: [
56+
{
57+
toolName: "atlas-local-create-deployment",
58+
parameters: {
59+
deploymentName: "local-mflix",
60+
},
61+
},
62+
{
63+
toolName: "atlas-local-connect-deployment",
64+
parameters: {
65+
deploymentIdOrName: "local-mflix",
66+
},
67+
},
68+
],
69+
},
70+
]);
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { beforeEach } from "vitest";
2+
import {
3+
defaultDriverOptions,
4+
defaultTestConfig,
5+
expectDefined,
6+
getResponseElements,
7+
setupIntegrationTest,
8+
validateToolMetadata,
9+
waitUntilMcpClientIsSet,
10+
} from "../../helpers.js";
11+
import { afterEach, describe, expect, it } from "vitest";
12+
13+
const isMacOSInGitHubActions = process.platform === "darwin" && process.env.GITHUB_ACTIONS === "true";
14+
const integration = setupIntegrationTest(
15+
() => defaultTestConfig,
16+
() => defaultDriverOptions
17+
);
18+
19+
// Docker is not available on macOS in GitHub Actions
20+
// That's why we skip the tests on macOS in GitHub Actions
21+
describe.skipIf(isMacOSInGitHubActions)("atlas-local-connect-deployment", () => {
22+
beforeEach(async ({ signal }) => {
23+
await waitUntilMcpClientIsSet(integration.mcpServer(), signal);
24+
});
25+
26+
validateToolMetadata(integration, "atlas-local-connect-deployment", "Connect to a MongoDB Atlas Local deployment", [
27+
{
28+
name: "deploymentName",
29+
type: "string",
30+
description: "Name of the deployment to connect to",
31+
required: true,
32+
},
33+
]);
34+
35+
it("should have the atlas-local-connect-deployment tool", async () => {
36+
const { tools } = await integration.mcpClient().listTools();
37+
const connectDeployment = tools.find((tool) => tool.name === "atlas-local-connect-deployment");
38+
expectDefined(connectDeployment);
39+
});
40+
41+
it("should return 'no such container' error when connecting to non-existent deployment", async () => {
42+
const deploymentName = "non-existent";
43+
const response = await integration.mcpClient().callTool({
44+
name: "atlas-local-connect-deployment",
45+
arguments: { deploymentName },
46+
});
47+
const elements = getResponseElements(response.content);
48+
expect(elements.length).toBeGreaterThanOrEqual(1);
49+
expect(elements[0]?.text).toContain(
50+
`The Atlas Local deployment "${deploymentName}" was not found. Please check the deployment name or use "atlas-local-list-deployments" to see available deployments.`
51+
);
52+
});
53+
});
54+
55+
describe.skipIf(isMacOSInGitHubActions)("atlas-local-connect-deployment with deployments", () => {
56+
let deploymentName: string = "";
57+
let deploymentNamesToCleanup: string[] = [];
58+
59+
beforeEach(async ({ signal }) => {
60+
await waitUntilMcpClientIsSet(integration.mcpServer(), signal);
61+
62+
// Create deployments
63+
deploymentName = `test-deployment-1-${Date.now()}`;
64+
deploymentNamesToCleanup.push(deploymentName);
65+
await integration.mcpClient().callTool({
66+
name: "atlas-local-create-deployment",
67+
arguments: { deploymentName },
68+
});
69+
70+
const anotherDeploymentName = `test-deployment-2-${Date.now()}`;
71+
deploymentNamesToCleanup.push(anotherDeploymentName);
72+
await integration.mcpClient().callTool({
73+
name: "atlas-local-create-deployment",
74+
arguments: { deploymentName: anotherDeploymentName },
75+
});
76+
});
77+
78+
afterEach(async () => {
79+
// Delete all created deployments
80+
for (const deploymentNameToCleanup of deploymentNamesToCleanup) {
81+
try {
82+
await integration.mcpClient().callTool({
83+
name: "atlas-local-delete-deployment",
84+
arguments: { deploymentName: deploymentNameToCleanup },
85+
});
86+
} catch (error) {
87+
console.warn(`Failed to delete deployment ${deploymentNameToCleanup}:`, error);
88+
}
89+
}
90+
deploymentNamesToCleanup = [];
91+
});
92+
93+
it("should connect to correct deployment when calling the tool", async () => {
94+
// Connect to the deployment
95+
const response = await integration.mcpClient().callTool({
96+
name: "atlas-local-connect-deployment",
97+
arguments: { deploymentName },
98+
});
99+
const elements = getResponseElements(response.content);
100+
expect(elements.length).toBeGreaterThanOrEqual(1);
101+
expect(elements[0]?.text).toContain(`Successfully connected to Atlas Local deployment "${deploymentName}".`);
102+
});
103+
});
104+
105+
describe.skipIf(!isMacOSInGitHubActions)("atlas-local-connect-deployment [MacOS in GitHub Actions]", () => {
106+
it("should not have the atlas-local-connect-deployment tool", async ({ signal }) => {
107+
// This should throw an error because the client is not set within the timeout of 5 seconds (default)
108+
await expect(waitUntilMcpClientIsSet(integration.mcpServer(), signal)).rejects.toThrow();
109+
110+
const { tools } = await integration.mcpClient().listTools();
111+
const connectDeployment = tools.find((tool) => tool.name === "atlas-local-connect-deployment");
112+
expect(connectDeployment).toBeUndefined();
113+
});
114+
});

0 commit comments

Comments
 (0)