Skip to content

Commit e774083

Browse files
feat(atlas-local): Add Atlas Local Create Deployment tool (#546)
1 parent 6938a8f commit e774083

File tree

6 files changed

+279
-26
lines changed

6 files changed

+279
-26
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.2",
124+
"@mongodb-js-preview/atlas-local": "^0.0.0-preview.3",
125125
"kerberos": "^2.2.2"
126126
}
127127
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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, CreateDeploymentOptions, CreationSourceType } from "@mongodb-js-preview/atlas-local";
5+
import z from "zod";
6+
7+
export class CreateDeploymentTool extends AtlasLocalToolBase {
8+
public name = "atlas-local-create-deployment";
9+
protected description = "Create a MongoDB Atlas local deployment";
10+
public operationType: OperationType = "create";
11+
protected argsShape = {
12+
deploymentName: z.string().describe("Name of the deployment to create").optional(),
13+
};
14+
15+
protected async executeWithAtlasLocalClient(
16+
client: Client,
17+
{ deploymentName }: ToolArgs<typeof this.argsShape>
18+
): Promise<CallToolResult> {
19+
const deploymentOptions: CreateDeploymentOptions = {
20+
name: deploymentName,
21+
creationSource: {
22+
type: "MCPServer" as CreationSourceType,
23+
source: "MCPServer",
24+
},
25+
};
26+
// Create the deployment
27+
const deployment = await client.createDeployment(deploymentOptions);
28+
29+
return {
30+
content: [
31+
{
32+
type: "text",
33+
text: `Deployment with container ID "${deployment.containerId}" and name "${deployment.name}" created.`,
34+
},
35+
],
36+
};
37+
}
38+
}

src/tools/atlasLocal/tools.ts

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

4-
export const AtlasLocalTools = [ListDeploymentsTool, DeleteDeploymentTool];
5+
export const AtlasLocalTools = [ListDeploymentsTool, DeleteDeploymentTool, CreateDeploymentTool];
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import {
2+
defaultDriverOptions,
3+
defaultTestConfig,
4+
expectDefined,
5+
getResponseElements,
6+
setupIntegrationTest,
7+
waitUntilMcpClientIsSet,
8+
} from "../../helpers.js";
9+
import { afterEach, describe, expect, it } from "vitest";
10+
11+
const isMacOSInGitHubActions = process.platform === "darwin" && process.env.GITHUB_ACTIONS === "true";
12+
13+
// Docker is not available on macOS in GitHub Actions
14+
// That's why we skip the tests on macOS in GitHub Actions
15+
describe("atlas-local-create-deployment", () => {
16+
let deploymentNamesToCleanup: string[] = [];
17+
18+
afterEach(async () => {
19+
// Clean up any deployments created during the test
20+
for (const deploymentName of deploymentNamesToCleanup) {
21+
try {
22+
await integration.mcpClient().callTool({
23+
name: "atlas-local-delete-deployment",
24+
arguments: { deploymentName },
25+
});
26+
} catch (error) {
27+
console.warn(`Failed to delete deployment ${deploymentName}:`, error);
28+
}
29+
}
30+
deploymentNamesToCleanup = [];
31+
});
32+
const integration = setupIntegrationTest(
33+
() => defaultTestConfig,
34+
() => defaultDriverOptions
35+
);
36+
37+
it.skipIf(isMacOSInGitHubActions)("should have the atlas-local-create-deployment tool", async ({ signal }) => {
38+
await waitUntilMcpClientIsSet(integration.mcpServer(), signal);
39+
40+
const { tools } = await integration.mcpClient().listTools();
41+
const createDeployment = tools.find((tool) => tool.name === "atlas-local-create-deployment");
42+
expectDefined(createDeployment);
43+
});
44+
45+
it.skipIf(!isMacOSInGitHubActions)(
46+
"[MacOS in GitHub Actions] should not have the atlas-local-create-deployment tool",
47+
async ({ signal }) => {
48+
// This should throw an error because the client is not set within the timeout of 5 seconds (default)
49+
await expect(waitUntilMcpClientIsSet(integration.mcpServer(), signal)).rejects.toThrow();
50+
51+
const { tools } = await integration.mcpClient().listTools();
52+
const createDeployment = tools.find((tool) => tool.name === "atlas-local-create-deployment");
53+
expect(createDeployment).toBeUndefined();
54+
}
55+
);
56+
57+
it.skipIf(isMacOSInGitHubActions)("should have correct metadata", async ({ signal }) => {
58+
await waitUntilMcpClientIsSet(integration.mcpServer(), signal);
59+
const { tools } = await integration.mcpClient().listTools();
60+
const createDeployment = tools.find((tool) => tool.name === "atlas-local-create-deployment");
61+
expectDefined(createDeployment);
62+
expect(createDeployment.inputSchema.type).toBe("object");
63+
expectDefined(createDeployment.inputSchema.properties);
64+
expect(createDeployment.inputSchema.properties).toHaveProperty("deploymentName");
65+
});
66+
67+
it.skipIf(isMacOSInGitHubActions)("should create a deployment when calling the tool", async ({ signal }) => {
68+
await waitUntilMcpClientIsSet(integration.mcpServer(), signal);
69+
const deploymentName = `test-deployment-${Date.now()}`;
70+
71+
// Check that deployment doesn't exist before creation
72+
const beforeResponse = await integration.mcpClient().callTool({
73+
name: "atlas-local-list-deployments",
74+
arguments: {},
75+
});
76+
const beforeElements = getResponseElements(beforeResponse.content);
77+
expect(beforeElements.length).toBeGreaterThanOrEqual(1);
78+
expect(beforeElements[1]?.text ?? "").not.toContain(deploymentName);
79+
80+
// Create a deployment
81+
deploymentNamesToCleanup.push(deploymentName);
82+
await integration.mcpClient().callTool({
83+
name: "atlas-local-create-deployment",
84+
arguments: { deploymentName },
85+
});
86+
87+
// Check that deployment exists after creation
88+
const afterResponse = await integration.mcpClient().callTool({
89+
name: "atlas-local-list-deployments",
90+
arguments: {},
91+
});
92+
93+
const afterElements = getResponseElements(afterResponse.content);
94+
expect(afterElements.length).toBeGreaterThanOrEqual(1);
95+
expect(afterElements[1]?.text ?? "").toContain(deploymentName);
96+
});
97+
98+
it.skipIf(isMacOSInGitHubActions)(
99+
"should return an error when creating a deployment that already exists",
100+
async ({ signal }) => {
101+
await waitUntilMcpClientIsSet(integration.mcpServer(), signal);
102+
103+
// Create a deployment
104+
const deploymentName = `test-deployment-${Date.now()}`;
105+
deploymentNamesToCleanup.push(deploymentName);
106+
await integration.mcpClient().callTool({
107+
name: "atlas-local-create-deployment",
108+
arguments: { deploymentName },
109+
});
110+
111+
// Try to create the same deployment again
112+
const response = await integration.mcpClient().callTool({
113+
name: "atlas-local-create-deployment",
114+
arguments: { deploymentName },
115+
});
116+
const elements = getResponseElements(response.content);
117+
expect(elements.length).toBeGreaterThanOrEqual(1);
118+
expect(elements[0]?.text).toContain("Container already exists: " + deploymentName);
119+
}
120+
);
121+
122+
it.skipIf(isMacOSInGitHubActions)("should create a deployment with the correct name", async ({ signal }) => {
123+
await waitUntilMcpClientIsSet(integration.mcpServer(), signal);
124+
125+
// Create a deployment
126+
const deploymentName = `test-deployment-${Date.now()}`;
127+
deploymentNamesToCleanup.push(deploymentName);
128+
const createResponse = await integration.mcpClient().callTool({
129+
name: "atlas-local-create-deployment",
130+
arguments: { deploymentName },
131+
});
132+
133+
// Check the response contains the deployment name
134+
const createElements = getResponseElements(createResponse.content);
135+
expect(createElements.length).toBeGreaterThanOrEqual(1);
136+
expect(createElements[0]?.text).toContain(deploymentName);
137+
138+
// List the deployments
139+
const response = await integration.mcpClient().callTool({
140+
name: "atlas-local-list-deployments",
141+
arguments: {},
142+
});
143+
const elements = getResponseElements(response.content);
144+
145+
expect(elements.length).toBeGreaterThanOrEqual(1);
146+
expect(elements[1]?.text ?? "").toContain(deploymentName);
147+
expect(elements[1]?.text ?? "").toContain("Running");
148+
});
149+
150+
it.skipIf(isMacOSInGitHubActions)("should create a deployment when name is not provided", async ({ signal }) => {
151+
await waitUntilMcpClientIsSet(integration.mcpServer(), signal);
152+
153+
// Create a deployment
154+
const createResponse = await integration.mcpClient().callTool({
155+
name: "atlas-local-create-deployment",
156+
arguments: {},
157+
});
158+
159+
// Check the response contains the deployment name
160+
const createElements = getResponseElements(createResponse.content);
161+
expect(createElements.length).toBeGreaterThanOrEqual(1);
162+
163+
// Extract the deployment name from the response
164+
// The name should be in the format local<number>
165+
const deploymentName = createElements[0]?.text.match(/local\d+/)?.[0];
166+
expectDefined(deploymentName);
167+
deploymentNamesToCleanup.push(deploymentName);
168+
169+
// List the deployments
170+
const response = await integration.mcpClient().callTool({
171+
name: "atlas-local-list-deployments",
172+
arguments: {},
173+
});
174+
175+
// Check the deployment has been created
176+
const elements = getResponseElements(response.content);
177+
expect(elements.length).toBeGreaterThanOrEqual(1);
178+
expect(elements[1]?.text ?? "").toContain(deploymentName);
179+
expect(elements[1]?.text ?? "").toContain("Running");
180+
});
181+
});

tests/integration/tools/atlas-local/deleteDeployment.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,37 @@ describe("atlas-local-delete-deployment", () => {
6464
);
6565
}
6666
);
67+
68+
it.skipIf(isMacOSInGitHubActions)("should delete a deployment when calling the tool", async ({ signal }) => {
69+
await waitUntilMcpClientIsSet(integration.mcpServer(), signal);
70+
// Create a deployment
71+
const deploymentName = `test-deployment-${Date.now()}`;
72+
await integration.mcpClient().callTool({
73+
name: "atlas-local-create-deployment",
74+
arguments: { deploymentName },
75+
});
76+
77+
// Check that deployment exists before deletion
78+
const beforeResponse = await integration.mcpClient().callTool({
79+
name: "atlas-local-list-deployments",
80+
arguments: {},
81+
});
82+
const beforeElements = getResponseElements(beforeResponse.content);
83+
expect(beforeElements.length).toBeGreaterThanOrEqual(1);
84+
expect(beforeElements[1]?.text ?? "").toContain(deploymentName);
85+
86+
// Delete the deployment
87+
await integration.mcpClient().callTool({
88+
name: "atlas-local-delete-deployment",
89+
arguments: { deploymentName },
90+
});
91+
92+
// Count the number of deployments after deleting the deployment
93+
const afterResponse = await integration.mcpClient().callTool({
94+
name: "atlas-local-list-deployments",
95+
arguments: {},
96+
});
97+
const afterElements = getResponseElements(afterResponse.content);
98+
expect(afterElements[1]?.text ?? "").not.toContain(deploymentName);
99+
});
67100
});

0 commit comments

Comments
 (0)