-
Notifications
You must be signed in to change notification settings - Fork 173
feat(atlas-local): Add Atlas Local Create Deployment tool #546
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2ad8fce
4c61ad3
6ee6636
1a3bffa
2e02b05
5656f1e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; | ||
| import { AtlasLocalToolBase } from "../atlasLocalTool.js"; | ||
| import type { OperationType, ToolArgs } from "../../tool.js"; | ||
| import type { Client, CreateDeploymentOptions, CreationSourceType } from "@mongodb-js-preview/atlas-local"; | ||
| import z from "zod"; | ||
|
|
||
| export class CreateDeploymentTool extends AtlasLocalToolBase { | ||
| public name = "atlas-local-create-deployment"; | ||
| protected description = "Create a MongoDB Atlas local deployment"; | ||
| public operationType: OperationType = "create"; | ||
| protected argsShape = { | ||
| deploymentName: z.string().describe("Name of the deployment to create").optional(), | ||
| }; | ||
|
|
||
| protected async executeWithAtlasLocalClient( | ||
| client: Client, | ||
| { deploymentName }: ToolArgs<typeof this.argsShape> | ||
| ): Promise<CallToolResult> { | ||
| const deploymentOptions: CreateDeploymentOptions = { | ||
| name: deploymentName, | ||
| creationSource: { | ||
| type: "MCPServer" as CreationSourceType, | ||
| source: "MCPServer", | ||
| }, | ||
| }; | ||
| // Create the deployment | ||
| const deployment = await client.createDeployment(deploymentOptions); | ||
|
|
||
| return { | ||
| content: [ | ||
| { | ||
| type: "text", | ||
| text: `Deployment with container ID "${deployment.containerId}" and name "${deployment.name}" created.`, | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import { DeleteDeploymentTool } from "./delete/deleteDeployment.js"; | ||
| import { ListDeploymentsTool } from "./read/listDeployments.js"; | ||
| import { CreateDeploymentTool } from "./create/createDeployment.js"; | ||
|
|
||
| export const AtlasLocalTools = [ListDeploymentsTool, DeleteDeploymentTool]; | ||
| export const AtlasLocalTools = [ListDeploymentsTool, DeleteDeploymentTool, CreateDeploymentTool]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| import { | ||
| defaultDriverOptions, | ||
| defaultTestConfig, | ||
| expectDefined, | ||
| getResponseElements, | ||
| setupIntegrationTest, | ||
| waitUntilMcpClientIsSet, | ||
| } from "../../helpers.js"; | ||
| import { afterEach, 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-create-deployment", () => { | ||
| let deploymentNamesToCleanup: string[] = []; | ||
|
|
||
| afterEach(async () => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need to run this on
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see a way to do the skipIf(isMacOSInGitHubActions) or similar for an afterEach. This leaves adding something like this in the afterEach function: But since the deploymentToCleanup array will always be empty it will be the same execution time
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense to not skip it then! |
||
| // Clean up any deployments created during the test | ||
| for (const deploymentName of deploymentNamesToCleanup) { | ||
| try { | ||
| await integration.mcpClient().callTool({ | ||
| name: "atlas-local-delete-deployment", | ||
| arguments: { deploymentName }, | ||
| }); | ||
| } catch (error) { | ||
| console.warn(`Failed to delete deployment ${deploymentName}:`, error); | ||
| } | ||
| } | ||
| deploymentNamesToCleanup = []; | ||
| }); | ||
| const integration = setupIntegrationTest( | ||
| () => defaultTestConfig, | ||
| () => defaultDriverOptions | ||
| ); | ||
|
|
||
| it.skipIf(isMacOSInGitHubActions)("should have the atlas-local-create-deployment tool", async ({ signal }) => { | ||
| await waitUntilMcpClientIsSet(integration.mcpServer(), signal); | ||
|
|
||
| const { tools } = await integration.mcpClient().listTools(); | ||
| const createDeployment = tools.find((tool) => tool.name === "atlas-local-create-deployment"); | ||
| expectDefined(createDeployment); | ||
| }); | ||
|
|
||
| it.skipIf(!isMacOSInGitHubActions)( | ||
| "[MacOS in GitHub Actions] should not have the atlas-local-create-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 createDeployment = tools.find((tool) => tool.name === "atlas-local-create-deployment"); | ||
| expect(createDeployment).toBeUndefined(); | ||
| } | ||
| ); | ||
|
|
||
| it.skipIf(isMacOSInGitHubActions)("should have correct metadata", async ({ signal }) => { | ||
| await waitUntilMcpClientIsSet(integration.mcpServer(), signal); | ||
| const { tools } = await integration.mcpClient().listTools(); | ||
| const createDeployment = tools.find((tool) => tool.name === "atlas-local-create-deployment"); | ||
| expectDefined(createDeployment); | ||
| expect(createDeployment.inputSchema.type).toBe("object"); | ||
| expectDefined(createDeployment.inputSchema.properties); | ||
| expect(createDeployment.inputSchema.properties).toHaveProperty("deploymentName"); | ||
| }); | ||
|
|
||
| it.skipIf(isMacOSInGitHubActions)("should create a deployment when calling the tool", async ({ signal }) => { | ||
| await waitUntilMcpClientIsSet(integration.mcpServer(), signal); | ||
| const deploymentName = `test-deployment-${Date.now()}`; | ||
|
|
||
| // Check that deployment doesn't exist before creation | ||
| const beforeResponse = await integration.mcpClient().callTool({ | ||
| name: "atlas-local-list-deployments", | ||
| arguments: {}, | ||
| }); | ||
| const beforeElements = getResponseElements(beforeResponse.content); | ||
| expect(beforeElements.length).toBeGreaterThanOrEqual(1); | ||
| expect(beforeElements[1]?.text ?? "").not.toContain(deploymentName); | ||
|
|
||
| // Create a deployment | ||
| deploymentNamesToCleanup.push(deploymentName); | ||
| await integration.mcpClient().callTool({ | ||
| name: "atlas-local-create-deployment", | ||
| arguments: { deploymentName }, | ||
| }); | ||
|
|
||
| // Check that deployment exists after creation | ||
| const afterResponse = await integration.mcpClient().callTool({ | ||
| name: "atlas-local-list-deployments", | ||
| arguments: {}, | ||
| }); | ||
|
|
||
| const afterElements = getResponseElements(afterResponse.content); | ||
| expect(afterElements.length).toBeGreaterThanOrEqual(1); | ||
| expect(afterElements[1]?.text ?? "").toContain(deploymentName); | ||
|
Check failure on line 95 in tests/integration/tools/atlas-local/createDeployment.test.ts
|
||
| }); | ||
|
|
||
| it.skipIf(isMacOSInGitHubActions)( | ||
| "should return an error when creating a deployment that already exists", | ||
| async ({ signal }) => { | ||
| await waitUntilMcpClientIsSet(integration.mcpServer(), signal); | ||
|
|
||
| // Create a deployment | ||
| const deploymentName = `test-deployment-${Date.now()}`; | ||
| deploymentNamesToCleanup.push(deploymentName); | ||
| await integration.mcpClient().callTool({ | ||
| name: "atlas-local-create-deployment", | ||
| arguments: { deploymentName }, | ||
| }); | ||
|
|
||
| // Try to create the same deployment again | ||
| const response = await integration.mcpClient().callTool({ | ||
| name: "atlas-local-create-deployment", | ||
| arguments: { deploymentName }, | ||
| }); | ||
| const elements = getResponseElements(response.content); | ||
| expect(elements.length).toBeGreaterThanOrEqual(1); | ||
| expect(elements[0]?.text).toContain("Container already exists: " + deploymentName); | ||
|
Check failure on line 118 in tests/integration/tools/atlas-local/createDeployment.test.ts
|
||
| } | ||
| ); | ||
|
|
||
| it.skipIf(isMacOSInGitHubActions)("should create a deployment with the correct name", async ({ signal }) => { | ||
| await waitUntilMcpClientIsSet(integration.mcpServer(), signal); | ||
|
|
||
| // Create a deployment | ||
| const deploymentName = `test-deployment-${Date.now()}`; | ||
| deploymentNamesToCleanup.push(deploymentName); | ||
| const createResponse = await integration.mcpClient().callTool({ | ||
| name: "atlas-local-create-deployment", | ||
| arguments: { deploymentName }, | ||
| }); | ||
|
|
||
| // Check the response contains the deployment name | ||
| const createElements = getResponseElements(createResponse.content); | ||
| expect(createElements.length).toBeGreaterThanOrEqual(1); | ||
| expect(createElements[0]?.text).toContain(deploymentName); | ||
|
Check failure on line 136 in tests/integration/tools/atlas-local/createDeployment.test.ts
|
||
|
|
||
| // List the deployments | ||
| const response = await integration.mcpClient().callTool({ | ||
| name: "atlas-local-list-deployments", | ||
| arguments: {}, | ||
| }); | ||
| const elements = getResponseElements(response.content); | ||
|
|
||
| expect(elements.length).toBeGreaterThanOrEqual(1); | ||
| expect(elements[1]?.text ?? "").toContain(deploymentName); | ||
| expect(elements[1]?.text ?? "").toContain("Running"); | ||
| }); | ||
|
|
||
| it.skipIf(isMacOSInGitHubActions)("should create a deployment when name is not provided", async ({ signal }) => { | ||
| await waitUntilMcpClientIsSet(integration.mcpServer(), signal); | ||
|
|
||
| // Create a deployment | ||
| const createResponse = await integration.mcpClient().callTool({ | ||
| name: "atlas-local-create-deployment", | ||
| arguments: {}, | ||
| }); | ||
|
|
||
| // Check the response contains the deployment name | ||
| const createElements = getResponseElements(createResponse.content); | ||
| expect(createElements.length).toBeGreaterThanOrEqual(1); | ||
|
|
||
| // Extract the deployment name from the response | ||
| // The name should be in the format local<number> | ||
| const deploymentName = createElements[0]?.text.match(/local\d+/)?.[0]; | ||
| expectDefined(deploymentName); | ||
| deploymentNamesToCleanup.push(deploymentName); | ||
|
|
||
| // List the deployments | ||
| const response = await integration.mcpClient().callTool({ | ||
| name: "atlas-local-list-deployments", | ||
| arguments: {}, | ||
| }); | ||
|
|
||
| // Check the deployment has been created | ||
| const elements = getResponseElements(response.content); | ||
| expect(elements.length).toBeGreaterThanOrEqual(1); | ||
| expect(elements[1]?.text ?? "").toContain(deploymentName); | ||
| expect(elements[1]?.text ?? "").toContain("Running"); | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.