diff --git a/package-lock.json b/package-lock.json index 8ae6845ac..d5fdf990f 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.2", + "@mongodb-js-preview/atlas-local": "^0.0.0-preview.3", "kerberos": "^2.2.2" } }, @@ -2050,26 +2050,26 @@ } }, "node_modules/@mongodb-js-preview/atlas-local": { - "version": "0.0.0-preview.2", - "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local/-/atlas-local-0.0.0-preview.2.tgz", - "integrity": "sha512-gDU+xL3p//aSfGqjr3Zth8rlfjXXiu8D9+K7Q8s4Z83+0V0TciT7x4hVQYYtVdsny4MxHokTr/6PnG83z0yHIw==", + "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==", "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.2", - "@mongodb-js-preview/atlas-local-darwin-x64": "0.0.0-preview.2", - "@mongodb-js-preview/atlas-local-linux-arm64-gnu": "0.0.0-preview.2", - "@mongodb-js-preview/atlas-local-linux-x64-gnu": "0.0.0-preview.2", - "@mongodb-js-preview/atlas-local-win32-x64-msvc": "0.0.0-preview.2" + "@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" } }, "node_modules/@mongodb-js-preview/atlas-local-darwin-arm64": { - "version": "0.0.0-preview.2", - "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-darwin-arm64/-/atlas-local-darwin-arm64-0.0.0-preview.2.tgz", - "integrity": "sha512-aqiMeXCjawUYIP63y4buP+oRgg5jJ4g9HTOU7nMVZPO4aidLAMbIMgZVtolwplqofRlzxB9V6g4TiXS+Ksr8LA==", + "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==", "cpu": [ "arm64" ], @@ -2083,9 +2083,9 @@ } }, "node_modules/@mongodb-js-preview/atlas-local-darwin-x64": { - "version": "0.0.0-preview.2", - "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-darwin-x64/-/atlas-local-darwin-x64-0.0.0-preview.2.tgz", - "integrity": "sha512-0wKAf+XddBHYqDJ9ofnXUKZhOKv2ruqv1Ev1M+mksIiX+b321yz3K2HCDRjYMaLva75QYxbBy2csBoxSUBwbmA==", + "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==", "cpu": [ "x64" ], @@ -2099,9 +2099,9 @@ } }, "node_modules/@mongodb-js-preview/atlas-local-linux-arm64-gnu": { - "version": "0.0.0-preview.2", - "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-linux-arm64-gnu/-/atlas-local-linux-arm64-gnu-0.0.0-preview.2.tgz", - "integrity": "sha512-6qaA64ffmKbnjfDFo9s+jESSWOJO2v85HYlEdZlCj//7gWKFIN+5sDqCYHmSVlLUFGJAWSsQcJVhai2ojhQyjQ==", + "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==", "cpu": [ "arm64" ], @@ -2115,9 +2115,9 @@ } }, "node_modules/@mongodb-js-preview/atlas-local-linux-x64-gnu": { - "version": "0.0.0-preview.2", - "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-linux-x64-gnu/-/atlas-local-linux-x64-gnu-0.0.0-preview.2.tgz", - "integrity": "sha512-Y/AjlvP6rJqxByygycS0jtmNphsEjNcVFI2+uEFlY/QqU8I74RIYkFArJSuNJjv5vBh8/i+bw10gDwYlWVEuYA==", + "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==", "cpu": [ "x64" ], @@ -2131,9 +2131,9 @@ } }, "node_modules/@mongodb-js-preview/atlas-local-win32-x64-msvc": { - "version": "0.0.0-preview.2", - "resolved": "https://registry.npmjs.org/@mongodb-js-preview/atlas-local-win32-x64-msvc/-/atlas-local-win32-x64-msvc-0.0.0-preview.2.tgz", - "integrity": "sha512-5xo3k+o/4m6P3CkMI8IPebVxdxVVSWwc+amMISuLq7DPFKmhU5m9gDiXfeg/tS67W1+HIHlUFeuCbcOpnIqRZQ==", + "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==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index 352fb3dc9..58004422c 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.2", + "@mongodb-js-preview/atlas-local": "^0.0.0-preview.3", "kerberos": "^2.2.2" } } diff --git a/src/tools/atlasLocal/create/createDeployment.ts b/src/tools/atlasLocal/create/createDeployment.ts new file mode 100644 index 000000000..17cf26dab --- /dev/null +++ b/src/tools/atlasLocal/create/createDeployment.ts @@ -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 + ): Promise { + 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.`, + }, + ], + }; + } +} diff --git a/src/tools/atlasLocal/tools.ts b/src/tools/atlasLocal/tools.ts index 5284be1d2..655ae1dc7 100644 --- a/src/tools/atlasLocal/tools.ts +++ b/src/tools/atlasLocal/tools.ts @@ -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]; diff --git a/tests/integration/tools/atlas-local/createDeployment.test.ts b/tests/integration/tools/atlas-local/createDeployment.test.ts new file mode 100644 index 000000000..90dff002a --- /dev/null +++ b/tests/integration/tools/atlas-local/createDeployment.test.ts @@ -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 () => { + // 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); + }); + + 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); + } + ); + + 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); + + // 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 + 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"); + }); +}); diff --git a/tests/integration/tools/atlas-local/deleteDeployment.test.ts b/tests/integration/tools/atlas-local/deleteDeployment.test.ts index 87a309182..6956da91f 100644 --- a/tests/integration/tools/atlas-local/deleteDeployment.test.ts +++ b/tests/integration/tools/atlas-local/deleteDeployment.test.ts @@ -64,4 +64,37 @@ describe("atlas-local-delete-deployment", () => { ); } ); + + it.skipIf(isMacOSInGitHubActions)("should delete a deployment when calling the tool", async ({ signal }) => { + await waitUntilMcpClientIsSet(integration.mcpServer(), signal); + // Create a deployment + const deploymentName = `test-deployment-${Date.now()}`; + await integration.mcpClient().callTool({ + name: "atlas-local-create-deployment", + arguments: { deploymentName }, + }); + + // Check that deployment exists before deletion + 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 ?? "").toContain(deploymentName); + + // Delete the deployment + await integration.mcpClient().callTool({ + name: "atlas-local-delete-deployment", + arguments: { deploymentName }, + }); + + // Count the number of deployments after deleting the deployment + const afterResponse = await integration.mcpClient().callTool({ + name: "atlas-local-list-deployments", + arguments: {}, + }); + const afterElements = getResponseElements(afterResponse.content); + expect(afterElements[1]?.text ?? "").not.toContain(deploymentName); + }); });