From 93c27ece51ee0bcea45f0d63ae575dfff3b23277 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Thu, 18 Sep 2025 14:55:10 +0100 Subject: [PATCH 01/11] chore: add arg validation test coverage --- src/tools/mongodb/mongodbTool.ts | 5 +- tests/integration/helpers.ts | 35 +++++- tests/integration/tools/atlas/alerts.test.ts | 27 ++-- tests/integration/tools/atlas/atlasHelpers.ts | 23 +++- .../integration/tools/atlas/clusters.test.ts | 23 +++- tests/integration/tools/atlas/dbUsers.test.ts | 13 +- tests/integration/tools/atlas/orgs.test.ts | 32 ++--- .../integration/tools/atlas/projects.test.ts | 118 +++++++++--------- 8 files changed, 183 insertions(+), 93 deletions(-) diff --git a/src/tools/mongodb/mongodbTool.ts b/src/tools/mongodb/mongodbTool.ts index ded994ab3..a9c19d483 100644 --- a/src/tools/mongodb/mongodbTool.ts +++ b/src/tools/mongodb/mongodbTool.ts @@ -6,10 +6,11 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { ErrorCodes, MongoDBError } from "../../common/errors.js"; import { LogId } from "../../common/logger.js"; import type { Server } from "../../server.js"; +import { CommonArgs } from "../args.js"; export const DbOperationArgs = { - database: z.string().describe("Database name"), - collection: z.string().describe("Collection name"), + database: CommonArgs.string().describe("Database name"), + collection: CommonArgs.string().describe("Collection name"), }; export abstract class MongoDBToolBase extends ToolBase { diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 0a2ccfe84..3814e376e 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -96,8 +96,10 @@ export function setupIntegrationTest( keychain: new Keychain(), }); - // Mock hasValidAccessToken for tests + // Mock API Client for tests if (!userConfig.apiClientId && !userConfig.apiClientSecret) { + userConfig.apiClientId = "test"; + userConfig.apiClientSecret = "test"; const mockFn = vi.fn().mockResolvedValue(true); session.apiClient.validateAccessToken = mockFn; } @@ -235,6 +237,29 @@ export const databaseCollectionParameters: ParameterInfo[] = [ { name: "collection", type: "string", description: "Collection name", required: true }, ]; +export const projectIdParameters: ParameterInfo[] = [ + { name: "projectId", type: "string", description: "Atlas project ID", required: true }, +]; + +export const createClusterParameters: ParameterInfo[] = [ + { name: "name", type: "string", description: "Name of the cluster", required: true }, + { name: "projectId", type: "string", description: "Atlas project ID to create the cluster in", required: true }, + { name: "region", type: "string", description: "Region of the cluster", required: false }, +]; + +export const createDbUserParameters: ParameterInfo[] = [ + { + name: "projectId", + type: "string", + description: "Atlas project ID to create the database user in", + required: true, + }, + { name: "username", type: "string", description: "Username of the database user", required: true }, + { name: "password", type: "string", description: "Password of the database user", required: false }, + { name: "roles", type: "array", description: "Roles of the database user", required: true }, + { name: "scopes", type: "array", description: "Scopes of the database user", required: false }, +]; + export const databaseCollectionInvalidArgs = [ {}, { database: "test" }, @@ -245,6 +270,14 @@ export const databaseCollectionInvalidArgs = [ { database: "test", collection: [] }, ]; +export const projectIdInvalidArgs = [ + {}, + { projectId: 123 }, + { projectId: [] }, + { projectId: "!✅invalid" }, + { projectId: "invalid-test-project-id" }, +]; + export const databaseInvalidArgs = [{}, { database: 123 }, { database: [] }]; export function validateToolMetadata( diff --git a/tests/integration/tools/atlas/alerts.test.ts b/tests/integration/tools/atlas/alerts.test.ts index 3bf296585..06307a529 100644 --- a/tests/integration/tools/atlas/alerts.test.ts +++ b/tests/integration/tools/atlas/alerts.test.ts @@ -1,15 +1,24 @@ -import { expectDefined, getResponseElements } from "../../helpers.js"; +import { + getResponseElements, + projectIdInvalidArgs, + validateThrowsForInvalidArguments, + validateToolMetadata, +} from "../../helpers.js"; import { parseTable, describeWithAtlas, withProject } from "./atlasHelpers.js"; -import { expect, it } from "vitest"; +import { expect, it, describe } from "vitest"; describeWithAtlas("atlas-list-alerts", (integration) => { - it("should have correct metadata", async () => { - const { tools } = await integration.mcpClient().listTools(); - const listAlerts = tools.find((tool) => tool.name === "atlas-list-alerts"); - expectDefined(listAlerts); - expect(listAlerts.inputSchema.type).toBe("object"); - expectDefined(listAlerts.inputSchema.properties); - expect(listAlerts.inputSchema.properties).toHaveProperty("projectId"); + describe("should have correct metadata and validate invalid arguments", () => { + validateToolMetadata(integration, "atlas-list-alerts", "List MongoDB Atlas alerts", [ + { + name: "projectId", + type: "string", + description: "Atlas project ID to list alerts for", + required: true, + }, + ]); + + validateThrowsForInvalidArguments(integration, "atlas-list-alerts", projectIdInvalidArgs); }); withProject(integration, ({ getProjectId }) => { diff --git a/tests/integration/tools/atlas/atlasHelpers.ts b/tests/integration/tools/atlas/atlasHelpers.ts index 00ac53feb..760c684cd 100644 --- a/tests/integration/tools/atlas/atlasHelpers.ts +++ b/tests/integration/tools/atlas/atlasHelpers.ts @@ -9,11 +9,7 @@ import { afterAll, beforeAll, describe } from "vitest"; export type IntegrationTestFunction = (integration: IntegrationTest) => void; export function describeWithAtlas(name: string, fn: IntegrationTestFunction): void { - const describeFn = - !process.env.MDB_MCP_API_CLIENT_ID?.length || !process.env.MDB_MCP_API_CLIENT_SECRET?.length - ? describe.skip - : describe; - describeFn(name, () => { + describe(name, () => { const integration = setupIntegrationTest( () => ({ ...defaultTestConfig, @@ -34,8 +30,23 @@ interface ProjectTestArgs { type ProjectTestFunction = (args: ProjectTestArgs) => void; +export function withCredentials(integration: IntegrationTest, fn: IntegrationTestFunction): SuiteCollector { + const describeFn = + !process.env.MDB_MCP_API_CLIENT_ID?.length || !process.env.MDB_MCP_API_CLIENT_SECRET?.length + ? describe.skip + : describe; + return describeFn("with credentials", () => { + fn(integration); + }); +} + export function withProject(integration: IntegrationTest, fn: ProjectTestFunction): SuiteCollector { - return describe("with project", () => { + const describeFn = + !process.env.MDB_MCP_API_CLIENT_ID?.length || !process.env.MDB_MCP_API_CLIENT_SECRET?.length + ? describe.skip + : describe; + + return describeFn("with project", () => { let projectId: string = ""; let ipAddress: string = ""; diff --git a/tests/integration/tools/atlas/clusters.test.ts b/tests/integration/tools/atlas/clusters.test.ts index 5c50c570c..fdbb84187 100644 --- a/tests/integration/tools/atlas/clusters.test.ts +++ b/tests/integration/tools/atlas/clusters.test.ts @@ -1,5 +1,13 @@ import type { Session } from "../../../../src/common/session.js"; -import { expectDefined, getDataFromUntrustedContent, getResponseElements } from "../../helpers.js"; +import { + expectDefined, + getResponseElements, + getDataFromUntrustedContent, + createClusterParameters, + projectIdInvalidArgs, + validateThrowsForInvalidArguments, + validateToolMetadata, +} from "../../helpers.js"; import { describeWithAtlas, withProject, randomId, parseTable } from "./atlasHelpers.js"; import type { ClusterDescription20240805 } from "../../../../src/common/atlas/openapi.js"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; @@ -57,6 +65,19 @@ async function waitCluster( } describeWithAtlas("clusters", (integration) => { + describe("should have correct metadata and validate invalid arguments", () => { + validateToolMetadata( + integration, + "atlas-create-free-cluster", + "Create a free MongoDB Atlas cluster", + createClusterParameters + ); + + expect(() => { + validateThrowsForInvalidArguments(integration, "atlas-create-free-cluster", projectIdInvalidArgs); + }).not.toThrow(); + }); + withProject(integration, ({ getProjectId, getIpAddress }) => { const clusterName = "ClusterTest-" + randomId; diff --git a/tests/integration/tools/atlas/dbUsers.test.ts b/tests/integration/tools/atlas/dbUsers.test.ts index fee08b421..42b113851 100644 --- a/tests/integration/tools/atlas/dbUsers.test.ts +++ b/tests/integration/tools/atlas/dbUsers.test.ts @@ -1,10 +1,21 @@ import { describeWithAtlas, withProject, randomId } from "./atlasHelpers.js"; -import { expectDefined, getResponseElements } from "../../helpers.js"; +import { + expectDefined, + getResponseElements, + projectIdInvalidArgs, + validateThrowsForInvalidArguments, + validateToolMetadata, +} from "../../helpers.js"; import { ApiClientError } from "../../../../src/common/atlas/apiClientError.js"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { Keychain } from "../../../../src/common/keychain.js"; describeWithAtlas("db users", (integration) => { + describe("should have correct metadata and validate invalid arguments", () => { + validateToolMetadata(integration, "atlas-create-db-user", "Create a database user", createDbUserParameters); + validateThrowsForInvalidArguments(integration, "atlas-create-db-user", projectIdInvalidArgs); + }); + withProject(integration, ({ getProjectId }) => { let userName: string; beforeEach(() => { diff --git a/tests/integration/tools/atlas/orgs.test.ts b/tests/integration/tools/atlas/orgs.test.ts index 72e0182bf..baa4f96a9 100644 --- a/tests/integration/tools/atlas/orgs.test.ts +++ b/tests/integration/tools/atlas/orgs.test.ts @@ -1,23 +1,25 @@ import { expectDefined, getDataFromUntrustedContent, getResponseElements } from "../../helpers.js"; -import { parseTable, describeWithAtlas } from "./atlasHelpers.js"; +import { parseTable, describeWithAtlas, withCredentials } from "./atlasHelpers.js"; import { describe, expect, it } from "vitest"; describeWithAtlas("orgs", (integration) => { - describe("atlas-list-orgs", () => { - it("should have correct metadata", async () => { - const { tools } = await integration.mcpClient().listTools(); - const listOrgs = tools.find((tool) => tool.name === "atlas-list-orgs"); - expectDefined(listOrgs); - }); + withCredentials(integration, () => { + describe("atlas-list-orgs", () => { + it("should have correct metadata", async () => { + const { tools } = await integration.mcpClient().listTools(); + const listOrgs = tools.find((tool) => tool.name === "atlas-list-orgs"); + expectDefined(listOrgs); + }); - it("returns org names", async () => { - const response = await integration.mcpClient().callTool({ name: "atlas-list-orgs", arguments: {} }); - const elements = getResponseElements(response); - expect(elements[0]?.text).toContain("Found 1 organizations"); - expect(elements[1]?.text).toContain(" { + const response = await integration.mcpClient().callTool({ name: "atlas-list-orgs", arguments: {} }); + const elements = getResponseElements(response); + expect(elements[0]?.text).toContain("Found 1 organizations"); + expect(elements[1]?.text).toContain(" { - const projName = "testProj-" + randomId; + withCredentials(integration, () => { + const projName = "testProj-" + randomId; - afterAll(async () => { - const session = integration.mcpServer().session; + afterAll(async () => { + const session = integration.mcpServer().session; - const projects = await session.apiClient.listProjects(); - for (const project of projects?.results || []) { - if (project.name === projName) { - await session.apiClient.deleteProject({ - params: { - path: { - groupId: project.id || "", + const projects = await session.apiClient.listProjects(); + for (const project of projects?.results || []) { + if (project.name === projName) { + await session.apiClient.deleteProject({ + params: { + path: { + groupId: project.id || "", + }, }, - }, - }); - break; + }); + break; + } } - } - }); - - describe("atlas-create-project", () => { - it("should have correct metadata", async () => { - const { tools } = await integration.mcpClient().listTools(); - const createProject = tools.find((tool) => tool.name === "atlas-create-project"); - expectDefined(createProject); - expect(createProject.inputSchema.type).toBe("object"); - expectDefined(createProject.inputSchema.properties); - expect(createProject.inputSchema.properties).toHaveProperty("projectName"); - expect(createProject.inputSchema.properties).toHaveProperty("organizationId"); }); - it("should create a project", async () => { - const response = await integration.mcpClient().callTool({ - name: "atlas-create-project", - arguments: { projectName: projName }, + + describe("atlas-create-project", () => { + it("should have correct metadata", async () => { + const { tools } = await integration.mcpClient().listTools(); + const createProject = tools.find((tool) => tool.name === "atlas-create-project"); + expectDefined(createProject); + expect(createProject.inputSchema.type).toBe("object"); + expectDefined(createProject.inputSchema.properties); + expect(createProject.inputSchema.properties).toHaveProperty("projectName"); + expect(createProject.inputSchema.properties).toHaveProperty("organizationId"); }); + it("should create a project", async () => { + const response = await integration.mcpClient().callTool({ + name: "atlas-create-project", + arguments: { projectName: projName }, + }); - const elements = getResponseElements(response); - expect(elements).toHaveLength(1); - expect(elements[0]?.text).toContain(projName); - }); - }); - describe("atlas-list-projects", () => { - it("should have correct metadata", async () => { - const { tools } = await integration.mcpClient().listTools(); - const listProjects = tools.find((tool) => tool.name === "atlas-list-projects"); - expectDefined(listProjects); - expect(listProjects.inputSchema.type).toBe("object"); - expectDefined(listProjects.inputSchema.properties); - expect(listProjects.inputSchema.properties).toHaveProperty("orgId"); + const elements = getResponseElements(response); + expect(elements).toHaveLength(1); + expect(elements[0]?.text).toContain(projName); + }); }); + describe("atlas-list-projects", () => { + it("should have correct metadata", async () => { + const { tools } = await integration.mcpClient().listTools(); + const listProjects = tools.find((tool) => tool.name === "atlas-list-projects"); + expectDefined(listProjects); + expect(listProjects.inputSchema.type).toBe("object"); + expectDefined(listProjects.inputSchema.properties); + expect(listProjects.inputSchema.properties).toHaveProperty("orgId"); + }); - it("returns project names", async () => { - const response = await integration.mcpClient().callTool({ name: "atlas-list-projects", arguments: {} }); - const elements = getResponseElements(response); - expect(elements).toHaveLength(2); - expect(elements[1]?.text).toContain(" { + const response = await integration.mcpClient().callTool({ name: "atlas-list-projects", arguments: {} }); + const elements = getResponseElements(response); + expect(elements).toHaveLength(2); + expect(elements[1]?.text).toContain(" Date: Thu, 18 Sep 2025 15:02:48 +0100 Subject: [PATCH 02/11] lint and update --- src/tools/mongodb/mongodbTool.ts | 5 ++--- tests/integration/tools/atlas/dbUsers.test.ts | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tools/mongodb/mongodbTool.ts b/src/tools/mongodb/mongodbTool.ts index a9c19d483..ded994ab3 100644 --- a/src/tools/mongodb/mongodbTool.ts +++ b/src/tools/mongodb/mongodbTool.ts @@ -6,11 +6,10 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { ErrorCodes, MongoDBError } from "../../common/errors.js"; import { LogId } from "../../common/logger.js"; import type { Server } from "../../server.js"; -import { CommonArgs } from "../args.js"; export const DbOperationArgs = { - database: CommonArgs.string().describe("Database name"), - collection: CommonArgs.string().describe("Collection name"), + database: z.string().describe("Database name"), + collection: z.string().describe("Collection name"), }; export abstract class MongoDBToolBase extends ToolBase { diff --git a/tests/integration/tools/atlas/dbUsers.test.ts b/tests/integration/tools/atlas/dbUsers.test.ts index 42b113851..2488ca957 100644 --- a/tests/integration/tools/atlas/dbUsers.test.ts +++ b/tests/integration/tools/atlas/dbUsers.test.ts @@ -5,6 +5,7 @@ import { projectIdInvalidArgs, validateThrowsForInvalidArguments, validateToolMetadata, + createDbUserParameters, } from "../../helpers.js"; import { ApiClientError } from "../../../../src/common/atlas/apiClientError.js"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; From 7848be56fca43e001523004765e994e97d3e7134 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Thu, 18 Sep 2025 17:30:39 +0100 Subject: [PATCH 03/11] wip --- src/tools/args.ts | 11 ++++++++++- src/tools/atlas/connect/connectCluster.ts | 2 +- src/tools/atlas/create/createAccessList.ts | 4 ++-- src/tools/atlas/create/createDBUser.ts | 6 +++--- src/tools/atlas/read/inspectAccessList.ts | 2 +- src/tools/atlas/read/inspectCluster.ts | 9 ++------- tests/integration/helpers.ts | 13 ------------- tests/integration/tools/atlas/dbUsers.test.ts | 3 --- 8 files changed, 19 insertions(+), 31 deletions(-) diff --git a/src/tools/args.ts b/src/tools/args.ts index 165f3da0d..2c31574be 100644 --- a/src/tools/args.ts +++ b/src/tools/args.ts @@ -29,7 +29,7 @@ export const CommonArgs = { }; export const AtlasArgs = { - projectId: (): z.ZodString => CommonArgs.objectId("projectId"), + projectId: (): z.ZodString => CommonArgs.objectId("projectId").describe("Atlas project ID"), organizationId: (): z.ZodString => CommonArgs.objectId("organizationId"), @@ -68,3 +68,12 @@ export const AtlasArgs = { password: (): z.ZodString => z.string().min(1, "Password is required").max(100, "Password must be 100 characters or less"), }; + +export const ProjectAndClusterArgs = { + projectId: AtlasArgs.projectId(), + clusterName: AtlasArgs.clusterName().describe("Atlas cluster name"), +}; + +export const ProjectArgs = { + projectId: AtlasArgs.projectId(), +}; diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index 54f3ae8bd..d1fae89c8 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -20,7 +20,7 @@ function sleep(ms: number): Promise { } export const ConnectClusterArgs = { - projectId: AtlasArgs.projectId().describe("Atlas project ID"), + projectId: AtlasArgs.projectId(), clusterName: AtlasArgs.clusterName().describe("Atlas cluster name"), }; diff --git a/src/tools/atlas/create/createAccessList.ts b/src/tools/atlas/create/createAccessList.ts index fe5a862ff..cbdf96696 100644 --- a/src/tools/atlas/create/createAccessList.ts +++ b/src/tools/atlas/create/createAccessList.ts @@ -3,10 +3,10 @@ import { type OperationType, type ToolArgs } from "../../tool.js"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { AtlasToolBase } from "../atlasTool.js"; import { makeCurrentIpAccessListEntry, DEFAULT_ACCESS_LIST_COMMENT } from "../../../common/atlas/accessListUtils.js"; -import { AtlasArgs, CommonArgs } from "../../args.js"; +import { AtlasArgs, CommonArgs, ProjectArgs } from "../../args.js"; export const CreateAccessListArgs = { - projectId: AtlasArgs.projectId().describe("Atlas project ID"), + ...ProjectArgs, ipAddresses: z.array(AtlasArgs.ipAddress()).describe("IP addresses to allow access from").optional(), cidrBlocks: z.array(AtlasArgs.cidrBlock()).describe("CIDR blocks to allow access from").optional(), currentIpAddress: z.boolean().describe("Add the current IP address").default(false), diff --git a/src/tools/atlas/create/createDBUser.ts b/src/tools/atlas/create/createDBUser.ts index c8e8ea014..df7d4a281 100644 --- a/src/tools/atlas/create/createDBUser.ts +++ b/src/tools/atlas/create/createDBUser.ts @@ -8,7 +8,7 @@ import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUti import { AtlasArgs, CommonArgs } from "../../args.js"; export const CreateDBUserArgs = { - projectId: AtlasArgs.projectId().describe("Atlas project ID"), + projectId: AtlasArgs.projectId(), username: AtlasArgs.username().describe("Username for the new user"), // Models will generate overly simplistic passwords like SecurePassword123 or // AtlasPassword123, which are easily guessable and exploitable. We're instructing @@ -26,7 +26,7 @@ export const CreateDBUserArgs = { collectionName: CommonArgs.string().describe("Collection name").optional(), }) ) - .describe("Roles for the new user"), + .describe("Roles for the new database user"), clusters: z .array(AtlasArgs.clusterName()) .describe("Clusters to assign the user to, leave empty for access to all clusters") @@ -35,7 +35,7 @@ export const CreateDBUserArgs = { export class CreateDBUserTool extends AtlasToolBase { public name = "atlas-create-db-user"; - protected description = "Create an MongoDB Atlas database user"; + protected description = "Create a MongoDB Atlas database user"; public operationType: OperationType = "create"; protected argsShape = { ...CreateDBUserArgs, diff --git a/src/tools/atlas/read/inspectAccessList.ts b/src/tools/atlas/read/inspectAccessList.ts index 6c8eaed30..24a142b2f 100644 --- a/src/tools/atlas/read/inspectAccessList.ts +++ b/src/tools/atlas/read/inspectAccessList.ts @@ -4,7 +4,7 @@ import { AtlasToolBase } from "../atlasTool.js"; import { AtlasArgs } from "../../args.js"; export const InspectAccessListArgs = { - projectId: AtlasArgs.projectId().describe("Atlas project ID"), + projectId: AtlasArgs.projectId(), }; export class InspectAccessListTool extends AtlasToolBase { diff --git a/src/tools/atlas/read/inspectCluster.ts b/src/tools/atlas/read/inspectCluster.ts index 56e1e5a8b..66c8e4a73 100644 --- a/src/tools/atlas/read/inspectCluster.ts +++ b/src/tools/atlas/read/inspectCluster.ts @@ -3,19 +3,14 @@ import { type OperationType, type ToolArgs, formatUntrustedData } from "../../to import { AtlasToolBase } from "../atlasTool.js"; import type { Cluster } from "../../../common/atlas/cluster.js"; import { inspectCluster } from "../../../common/atlas/cluster.js"; -import { AtlasArgs } from "../../args.js"; - -export const InspectClusterArgs = { - projectId: AtlasArgs.projectId().describe("Atlas project ID"), - clusterName: AtlasArgs.clusterName().describe("Atlas cluster name"), -}; +import { ProjectAndClusterArgs } from "../../args.js"; export class InspectClusterTool extends AtlasToolBase { public name = "atlas-inspect-cluster"; protected description = "Inspect MongoDB Atlas cluster"; public operationType: OperationType = "read"; protected argsShape = { - ...InspectClusterArgs, + ...ProjectAndClusterArgs, }; protected async execute({ projectId, clusterName }: ToolArgs): Promise { diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 3814e376e..5cb41ec1b 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -247,19 +247,6 @@ export const createClusterParameters: ParameterInfo[] = [ { name: "region", type: "string", description: "Region of the cluster", required: false }, ]; -export const createDbUserParameters: ParameterInfo[] = [ - { - name: "projectId", - type: "string", - description: "Atlas project ID to create the database user in", - required: true, - }, - { name: "username", type: "string", description: "Username of the database user", required: true }, - { name: "password", type: "string", description: "Password of the database user", required: false }, - { name: "roles", type: "array", description: "Roles of the database user", required: true }, - { name: "scopes", type: "array", description: "Scopes of the database user", required: false }, -]; - export const databaseCollectionInvalidArgs = [ {}, { database: "test" }, diff --git a/tests/integration/tools/atlas/dbUsers.test.ts b/tests/integration/tools/atlas/dbUsers.test.ts index 2488ca957..676f83b6a 100644 --- a/tests/integration/tools/atlas/dbUsers.test.ts +++ b/tests/integration/tools/atlas/dbUsers.test.ts @@ -4,8 +4,6 @@ import { getResponseElements, projectIdInvalidArgs, validateThrowsForInvalidArguments, - validateToolMetadata, - createDbUserParameters, } from "../../helpers.js"; import { ApiClientError } from "../../../../src/common/atlas/apiClientError.js"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; @@ -13,7 +11,6 @@ import { Keychain } from "../../../../src/common/keychain.js"; describeWithAtlas("db users", (integration) => { describe("should have correct metadata and validate invalid arguments", () => { - validateToolMetadata(integration, "atlas-create-db-user", "Create a database user", createDbUserParameters); validateThrowsForInvalidArguments(integration, "atlas-create-db-user", projectIdInvalidArgs); }); From ba7dcbeb78450ae9f902dc22d3cffb39ae437c80 Mon Sep 17 00:00:00 2001 From: Bianca Lisle <40155621+blva@users.noreply.github.com> Date: Thu, 2 Oct 2025 14:04:33 +0200 Subject: [PATCH 04/11] chore: stop waiting for cluster deletion (#607) --- tests/integration/tools/atlas/atlasHelpers.ts | 17 ++++++++++++----- tests/integration/tools/atlas/clusters.test.ts | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/tests/integration/tools/atlas/atlasHelpers.ts b/tests/integration/tools/atlas/atlasHelpers.ts index 760c684cd..98f0e407d 100644 --- a/tests/integration/tools/atlas/atlasHelpers.ts +++ b/tests/integration/tools/atlas/atlasHelpers.ts @@ -71,18 +71,25 @@ export function withProject(integration: IntegrationTest, fn: ProjectTestFunctio } }); - afterAll(async () => { + afterAll(() => { + if (!projectId) { + return; + } + const apiClient = integration.mcpServer().session.apiClient; - if (projectId) { - // projectId may be empty if beforeAll failed. - await apiClient.deleteProject({ + + // send the delete request and ignore errors + apiClient + .deleteProject({ params: { path: { groupId: projectId, }, }, + }) + .catch((error) => { + console.log("Failed to delete project:", error); }); - } }); const args = { diff --git a/tests/integration/tools/atlas/clusters.test.ts b/tests/integration/tools/atlas/clusters.test.ts index fdbb84187..f6d87e2d3 100644 --- a/tests/integration/tools/atlas/clusters.test.ts +++ b/tests/integration/tools/atlas/clusters.test.ts @@ -16,7 +16,12 @@ function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } -async function deleteAndWaitCluster(session: Session, projectId: string, clusterName: string): Promise { +async function deleteCluster( + session: Session, + projectId: string, + clusterName: string, + wait: boolean = false +): Promise { await session.apiClient.deleteCluster({ params: { path: { @@ -25,6 +30,11 @@ async function deleteAndWaitCluster(session: Session, projectId: string, cluster }, }, }); + + if (!wait) { + return; + } + while (true) { try { await session.apiClient.getCluster({ @@ -85,7 +95,7 @@ describeWithAtlas("clusters", (integration) => { const projectId = getProjectId(); if (projectId) { const session: Session = integration.mcpServer().session; - await deleteAndWaitCluster(session, projectId, clusterName); + await deleteCluster(session, projectId, clusterName); } }); From 3bd3d932e0df293549fec9c6d0cecfc863654be8 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 2 Oct 2025 15:40:44 +0200 Subject: [PATCH 05/11] chore(ci): add cleanup script on CI for atlas envs (#608) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/cleanup-atlas-env.yml | 27 +++++ ..._health_fork.yaml => code-health-fork.yml} | 0 .../{code_health.yaml => code-health.yml} | 0 .../{dependabot_pr.yaml => dependabot-pr.yml} | 0 .github/workflows/{docker.yaml => docker.yml} | 0 ...epare_release.yaml => prepare-release.yml} | 0 .../workflows/{publish.yaml => publish.yml} | 0 package.json | 3 +- scripts/cleanupAtlasTestLeftovers.test.ts | 100 ++++++++++++++++++ vitest.config.ts | 9 +- 10 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/cleanup-atlas-env.yml rename .github/workflows/{code_health_fork.yaml => code-health-fork.yml} (100%) rename .github/workflows/{code_health.yaml => code-health.yml} (100%) rename .github/workflows/{dependabot_pr.yaml => dependabot-pr.yml} (100%) rename .github/workflows/{docker.yaml => docker.yml} (100%) rename .github/workflows/{prepare_release.yaml => prepare-release.yml} (100%) rename .github/workflows/{publish.yaml => publish.yml} (100%) create mode 100644 scripts/cleanupAtlasTestLeftovers.test.ts diff --git a/.github/workflows/cleanup-atlas-env.yml b/.github/workflows/cleanup-atlas-env.yml new file mode 100644 index 000000000..8da645832 --- /dev/null +++ b/.github/workflows/cleanup-atlas-env.yml @@ -0,0 +1,27 @@ +--- +name: "Cleanup stale Atlas test environments" +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + +permissions: {} + +jobs: + cleanup-envs: + runs-on: ubuntu-latest + steps: + - uses: GitHubSecurityLab/actions-permissions/monitor@v1 + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version-file: package.json + cache: "npm" + - name: Install dependencies + run: npm ci + - name: Run cleanup script + env: + MDB_MCP_API_CLIENT_ID: ${{ secrets.TEST_ATLAS_CLIENT_ID }} + MDB_MCP_API_CLIENT_SECRET: ${{ secrets.TEST_ATLAS_CLIENT_SECRET }} + MDB_MCP_API_BASE_URL: ${{ vars.TEST_ATLAS_BASE_URL }} + run: npm run atlas:cleanup diff --git a/.github/workflows/code_health_fork.yaml b/.github/workflows/code-health-fork.yml similarity index 100% rename from .github/workflows/code_health_fork.yaml rename to .github/workflows/code-health-fork.yml diff --git a/.github/workflows/code_health.yaml b/.github/workflows/code-health.yml similarity index 100% rename from .github/workflows/code_health.yaml rename to .github/workflows/code-health.yml diff --git a/.github/workflows/dependabot_pr.yaml b/.github/workflows/dependabot-pr.yml similarity index 100% rename from .github/workflows/dependabot_pr.yaml rename to .github/workflows/dependabot-pr.yml diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yml similarity index 100% rename from .github/workflows/docker.yaml rename to .github/workflows/docker.yml diff --git a/.github/workflows/prepare_release.yaml b/.github/workflows/prepare-release.yml similarity index 100% rename from .github/workflows/prepare_release.yaml rename to .github/workflows/prepare-release.yml diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yml similarity index 100% rename from .github/workflows/publish.yaml rename to .github/workflows/publish.yml diff --git a/package.json b/package.json index e689ea9cb..6c327f09b 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,8 @@ "generate": "./scripts/generate.sh", "test": "vitest --project eslint-rules --project unit-and-integration --coverage", "pretest:accuracy": "npm run build", - "test:accuracy": "sh ./scripts/accuracy/runAccuracyTests.sh" + "test:accuracy": "sh ./scripts/accuracy/runAccuracyTests.sh", + "atlas:cleanup": "vitest --project atlas-cleanup" }, "license": "Apache-2.0", "devDependencies": { diff --git a/scripts/cleanupAtlasTestLeftovers.test.ts b/scripts/cleanupAtlasTestLeftovers.test.ts new file mode 100644 index 000000000..24351c8b6 --- /dev/null +++ b/scripts/cleanupAtlasTestLeftovers.test.ts @@ -0,0 +1,100 @@ +import type { Group, AtlasOrganization } from "../src/common/atlas/openapi.js"; +import { ApiClient } from "../src/common/atlas/apiClient.js"; +import { ConsoleLogger } from "../src/common/logger.js"; +import { Keychain } from "../src/lib.js"; +import { describe, it } from "vitest"; + +function isOlderThanADay(date: string): boolean { + const oneDayInMs = 24 * 60 * 60 * 1000; + const projectDate = new Date(date); + const currentDate = new Date(); + return currentDate.getTime() - projectDate.getTime() > oneDayInMs; +} + +async function findTestOrganization(client: ApiClient): Promise { + const orgs = await client.listOrganizations(); + const testOrg = orgs?.results?.find((org) => org.name === "MongoDB MCP Test"); + + if (!testOrg) { + throw new Error('Test organization "MongoDB MCP Test" not found.'); + } + + return testOrg; +} + +async function findAllTestProjects(client: ApiClient, orgId: string): Promise { + const projects = await client.listOrganizationProjects({ + params: { + path: { + orgId, + }, + }, + }); + + const testProjects = projects?.results?.filter((proj) => proj.name.startsWith("testProj-")) || []; + return testProjects.filter((proj) => isOlderThanADay(proj.created)); +} + +async function deleteAllClustersOnStaleProject(client: ApiClient, projectId: string): Promise { + const allClusters = await client + .listClusters({ + params: { + path: { + groupId: projectId || "", + }, + }, + }) + .then((res) => res.results || []); + + await Promise.allSettled( + allClusters.map((cluster) => + client.deleteCluster({ params: { path: { groupId: projectId || "", clusterName: cluster.name || "" } } }) + ) + ); +} + +async function main(): Promise { + const apiClient = new ApiClient( + { + baseUrl: process.env.MDB_MCP_API_BASE_URL || "https://cloud-dev.mongodb.com", + credentials: { + clientId: process.env.MDB_MCP_API_CLIENT_ID || "", + clientSecret: process.env.MDB_MCP_API_CLIENT_SECRET || "", + }, + }, + new ConsoleLogger(Keychain.root) + ); + + const testOrg = await findTestOrganization(apiClient); + const testProjects = await findAllTestProjects(apiClient, testOrg.id || ""); + + if (testProjects.length === 0) { + console.log("No stale test projects found for cleanup."); + } + + for (const project of testProjects) { + console.log(`Cleaning up project: ${project.name} (${project.id})`); + if (!project.id) { + console.warn(`Skipping project with missing ID: ${project.name}`); + continue; + } + + await deleteAllClustersOnStaleProject(apiClient, project.id); + await apiClient.deleteProject({ + params: { + path: { + groupId: project.id, + }, + }, + }); + console.log(`Deleted project: ${project.name} (${project.id})`); + } + + return; +} + +describe("Cleanup Atlas Test Leftovers", () => { + it("should clean up stale test projects", async () => { + await main(); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 903a174af..a8967a476 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -26,7 +26,7 @@ export default defineConfig({ test: { name: "unit-and-integration", include: ["**/*.test.ts"], - exclude: [...vitestDefaultExcludes, "tests/accuracy/**"], + exclude: [...vitestDefaultExcludes, "scripts/**", "tests/accuracy/**"], }, }, { @@ -43,6 +43,13 @@ export default defineConfig({ include: ["eslint-rules/*.test.js"], }, }, + { + extends: true, + test: { + name: "atlas-cleanup", + include: ["scripts/cleanupAtlasTestLeftovers.test.ts"], + }, + }, ], }, }); From b12b4b15cda1462a6a30bca07b58487a86cdd302 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Fri, 3 Oct 2025 19:32:32 +0200 Subject: [PATCH 06/11] wip --- src/tools/args.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/tools/args.ts b/src/tools/args.ts index fcef93d0c..792b939d2 100644 --- a/src/tools/args.ts +++ b/src/tools/args.ts @@ -89,3 +89,11 @@ function toEJSON(value: T): T { export function zEJSON(): z.AnyZodObject { return z.object({}).passthrough().transform(toEJSON) as unknown as z.AnyZodObject; } +export const ProjectAndClusterArgs = { + projectId: AtlasArgs.projectId(), + clusterName: AtlasArgs.clusterName().describe("Atlas cluster name"), +}; + +export const ProjectArgs = { + projectId: AtlasArgs.projectId(), +}; From 2d008596b27b44e41abab061963fa28df49effd9 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Fri, 3 Oct 2025 20:19:38 +0200 Subject: [PATCH 07/11] reuse args --- src/tools/args.ts | 18 ++++++------------ src/tools/atlas/connect/connectCluster.ts | 5 ++--- src/tools/atlas/create/createFreeCluster.ts | 15 +++++++++------ 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/tools/args.ts b/src/tools/args.ts index 792b939d2..91925298e 100644 --- a/src/tools/args.ts +++ b/src/tools/args.ts @@ -18,6 +18,7 @@ export const ALLOWED_CLUSTER_NAME_CHARACTERS_ERROR = const ALLOWED_PROJECT_NAME_CHARACTERS_REGEX = /^[a-zA-Z0-9\s()@&+:._',-]+$/; export const ALLOWED_PROJECT_NAME_CHARACTERS_ERROR = "Project names can't be longer than 64 characters and can only contain letters, numbers, spaces, and the following symbols: ( ) @ & + : . _ - ' ,"; + export const CommonArgs = { string: (): ZodString => z.string().regex(NO_UNICODE_REGEX, NO_UNICODE_ERROR), @@ -70,14 +71,15 @@ export const AtlasArgs = { z.string().min(1, "Password is required").max(100, "Password must be 100 characters or less"), }; -export const ProjectAndClusterArgs = { +export const ProjectArgs = { projectId: AtlasArgs.projectId(), - clusterName: AtlasArgs.clusterName().describe("Atlas cluster name"), }; -export const ProjectArgs = { - projectId: AtlasArgs.projectId(), +export const ProjectAndClusterArgs = { + ...ProjectArgs, + clusterName: AtlasArgs.clusterName().describe("Atlas cluster name"), }; + function toEJSON(value: T): T { if (!value) { return value; @@ -89,11 +91,3 @@ function toEJSON(value: T): T { export function zEJSON(): z.AnyZodObject { return z.object({}).passthrough().transform(toEJSON) as unknown as z.AnyZodObject; } -export const ProjectAndClusterArgs = { - projectId: AtlasArgs.projectId(), - clusterName: AtlasArgs.clusterName().describe("Atlas cluster name"), -}; - -export const ProjectArgs = { - projectId: AtlasArgs.projectId(), -}; diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index d1fae89c8..9f304ebf7 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -7,7 +7,7 @@ import { inspectCluster } from "../../../common/atlas/cluster.js"; import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUtils.js"; import type { AtlasClusterConnectionInfo } from "../../../common/connectionManager.js"; import { getDefaultRoleFromConfig } from "../../../common/atlas/roles.js"; -import { AtlasArgs } from "../../args.js"; +import { ProjectAndClusterArgs } from "../../args.js"; const addedIpAccessListMessage = "Note: Your current IP address has been added to the Atlas project's IP access list to enable secure connection."; @@ -20,8 +20,7 @@ function sleep(ms: number): Promise { } export const ConnectClusterArgs = { - projectId: AtlasArgs.projectId(), - clusterName: AtlasArgs.clusterName().describe("Atlas cluster name"), + ...ProjectAndClusterArgs, }; export class ConnectClusterTool extends AtlasToolBase { diff --git a/src/tools/atlas/create/createFreeCluster.ts b/src/tools/atlas/create/createFreeCluster.ts index 6b1ac98eb..a3c11cc22 100644 --- a/src/tools/atlas/create/createFreeCluster.ts +++ b/src/tools/atlas/create/createFreeCluster.ts @@ -3,22 +3,25 @@ import { type ToolArgs, type OperationType } from "../../tool.js"; import { AtlasToolBase } from "../atlasTool.js"; import type { ClusterDescription20240805 } from "../../../common/atlas/openapi.js"; import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUtils.js"; -import { AtlasArgs } from "../../args.js"; +import { ProjectAndClusterArgs, AtlasArgs } from "../../args.js"; export class CreateFreeClusterTool extends AtlasToolBase { public name = "atlas-create-free-cluster"; protected description = "Create a free MongoDB Atlas cluster"; public operationType: OperationType = "create"; protected argsShape = { - projectId: AtlasArgs.projectId().describe("Atlas project ID to create the cluster in"), - name: AtlasArgs.clusterName().describe("Name of the cluster"), + ...ProjectAndClusterArgs, region: AtlasArgs.region().describe("Region of the cluster").default("US_EAST_1"), }; - protected async execute({ projectId, name, region }: ToolArgs): Promise { + protected async execute({ + projectId, + clusterName, + region, + }: ToolArgs): Promise { const input = { groupId: projectId, - name, + clusterName, clusterType: "REPLICASET", replicationSpecs: [ { @@ -50,7 +53,7 @@ export class CreateFreeClusterTool extends AtlasToolBase { return { content: [ - { type: "text", text: `Cluster "${name}" has been created in region "${region}".` }, + { type: "text", text: `Cluster "${clusterName}" has been created in region "${region}".` }, { type: "text", text: `Double check your access lists to enable your current IP.` }, ], }; From aedcdfd43d6c086ed00498f6b80daf43d65dc803 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 8 Oct 2025 21:08:13 +0100 Subject: [PATCH 08/11] update tests --- tests/integration/helpers.ts | 43 ++++++++++++++++++- .../tools/atlas/accessLists.test.ts | 27 +++++++++++- .../integration/tools/atlas/clusters.test.ts | 4 +- tests/integration/tools/atlas/dbUsers.test.ts | 4 +- .../integration/tools/atlas/projects.test.ts | 40 ++++++++++++++++- 5 files changed, 110 insertions(+), 8 deletions(-) diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 8084aa169..e57dfc1c5 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -249,8 +249,8 @@ export const projectIdParameters: ParameterInfo[] = [ ]; export const createClusterParameters: ParameterInfo[] = [ - { name: "name", type: "string", description: "Name of the cluster", required: true }, - { name: "projectId", type: "string", description: "Atlas project ID to create the cluster in", required: true }, + { name: "projectId", type: "string", description: "Atlas project ID", required: true }, + { name: "clusterName", type: "string", description: "Atlas cluster name", required: true }, { name: "region", type: "string", description: "Region of the cluster", required: false }, ]; @@ -272,6 +272,45 @@ export const projectIdInvalidArgs = [ { projectId: "invalid-test-project-id" }, ]; +export const clusterNameInvalidArgs = [ + { clusterName: 123 }, + { clusterName: [] }, + { clusterName: "!✅invalid" }, + { clusterName: "a".repeat(65) }, // too long +]; + +export const projectAndClusterInvalidArgs = [ + {}, + { projectId: "507f1f77bcf86cd799439011" }, // missing clusterName + { clusterName: "testCluster" }, // missing projectId + { projectId: 123, clusterName: "testCluster" }, + { projectId: "507f1f77bcf86cd799439011", clusterName: 123 }, + { projectId: "invalid", clusterName: "testCluster" }, + { projectId: "507f1f77bcf86cd799439011", clusterName: "!✅invalid" }, +]; + +export const organizationIdInvalidArgs = [ + { organizationId: 123 }, + { organizationId: [] }, + { organizationId: "!✅invalid" }, + { organizationId: "invalid-test-org-id" }, +]; + +export const orgIdInvalidArgs = [ + { orgId: 123 }, + { orgId: [] }, + { orgId: "!✅invalid" }, + { orgId: "invalid-test-org-id" }, +]; + +export const usernameInvalidArgs = [ + {}, + { username: 123 }, + { username: [] }, + { username: "!✅invalid" }, + { username: "a".repeat(101) }, // too long +]; + export const databaseInvalidArgs = [{}, { database: 123 }, { database: [] }]; export function validateToolMetadata( diff --git a/tests/integration/tools/atlas/accessLists.test.ts b/tests/integration/tools/atlas/accessLists.test.ts index a711f38ff..066a9d9e3 100644 --- a/tests/integration/tools/atlas/accessLists.test.ts +++ b/tests/integration/tools/atlas/accessLists.test.ts @@ -1,5 +1,11 @@ import { describeWithAtlas, withProject } from "./atlasHelpers.js"; -import { expectDefined, getResponseElements } from "../../helpers.js"; +import { + expectDefined, + getResponseElements, + projectIdInvalidArgs, + validateThrowsForInvalidArguments, + validateToolMetadata, +} from "../../helpers.js"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { ensureCurrentIpInAccessList } from "../../../../src/common/atlas/accessListUtils.js"; @@ -12,6 +18,25 @@ function generateRandomIp(): string { } describeWithAtlas("ip access lists", (integration) => { + describe("should have correct metadata and validate invalid arguments", () => { + validateToolMetadata( + integration, + "atlas-inspect-access-list", + "Inspect Ip/CIDR ranges with access to your MongoDB Atlas clusters.", + [ + { + name: "projectId", + type: "string", + description: "Atlas project ID", + required: true, + }, + ] + ); + + validateThrowsForInvalidArguments(integration, "atlas-inspect-access-list", projectIdInvalidArgs); + validateThrowsForInvalidArguments(integration, "atlas-create-access-list", projectIdInvalidArgs); + }); + withProject(integration, ({ getProjectId }) => { const ips = [generateRandomIp(), generateRandomIp()]; const cidrBlocks = [generateRandomIp() + "/16", generateRandomIp() + "/24"]; diff --git a/tests/integration/tools/atlas/clusters.test.ts b/tests/integration/tools/atlas/clusters.test.ts index f6d87e2d3..2ff52d558 100644 --- a/tests/integration/tools/atlas/clusters.test.ts +++ b/tests/integration/tools/atlas/clusters.test.ts @@ -4,7 +4,7 @@ import { getResponseElements, getDataFromUntrustedContent, createClusterParameters, - projectIdInvalidArgs, + projectAndClusterInvalidArgs, validateThrowsForInvalidArguments, validateToolMetadata, } from "../../helpers.js"; @@ -84,7 +84,7 @@ describeWithAtlas("clusters", (integration) => { ); expect(() => { - validateThrowsForInvalidArguments(integration, "atlas-create-free-cluster", projectIdInvalidArgs); + validateThrowsForInvalidArguments(integration, "atlas-create-free-cluster", projectAndClusterInvalidArgs); }).not.toThrow(); }); diff --git a/tests/integration/tools/atlas/dbUsers.test.ts b/tests/integration/tools/atlas/dbUsers.test.ts index 676f83b6a..9b1ea66f2 100644 --- a/tests/integration/tools/atlas/dbUsers.test.ts +++ b/tests/integration/tools/atlas/dbUsers.test.ts @@ -2,7 +2,7 @@ import { describeWithAtlas, withProject, randomId } from "./atlasHelpers.js"; import { expectDefined, getResponseElements, - projectIdInvalidArgs, + usernameInvalidArgs, validateThrowsForInvalidArguments, } from "../../helpers.js"; import { ApiClientError } from "../../../../src/common/atlas/apiClientError.js"; @@ -11,7 +11,7 @@ import { Keychain } from "../../../../src/common/keychain.js"; describeWithAtlas("db users", (integration) => { describe("should have correct metadata and validate invalid arguments", () => { - validateThrowsForInvalidArguments(integration, "atlas-create-db-user", projectIdInvalidArgs); + validateThrowsForInvalidArguments(integration, "atlas-create-db-user", usernameInvalidArgs); }); withProject(integration, ({ getProjectId }) => { diff --git a/tests/integration/tools/atlas/projects.test.ts b/tests/integration/tools/atlas/projects.test.ts index 99cb53adb..a220a0f52 100644 --- a/tests/integration/tools/atlas/projects.test.ts +++ b/tests/integration/tools/atlas/projects.test.ts @@ -1,11 +1,49 @@ import { ObjectId } from "mongodb"; import { parseTable, describeWithAtlas, withCredentials } from "./atlasHelpers.js"; -import { expectDefined, getDataFromUntrustedContent, getResponseElements } from "../../helpers.js"; +import { + expectDefined, + getDataFromUntrustedContent, + getResponseElements, + organizationIdInvalidArgs, + orgIdInvalidArgs, + validateThrowsForInvalidArguments, + validateToolMetadata, +} from "../../helpers.js"; import { afterAll, describe, expect, it } from "vitest"; const randomId = new ObjectId().toString(); describeWithAtlas("projects", (integration) => { + describe("should have correct metadata and validate invalid arguments", () => { + validateToolMetadata(integration, "atlas-create-project", "Create a MongoDB Atlas project", [ + { + name: "projectName", + type: "string", + description: "Name for the new project", + required: false, + }, + { + name: "organizationId", + type: "string", + description: "Organization ID for the new project", + required: false, + }, + ]); + + validateThrowsForInvalidArguments(integration, "atlas-create-project", organizationIdInvalidArgs); + + validateToolMetadata(integration, "atlas-list-projects", "List MongoDB Atlas projects", [ + { + name: "orgId", + type: "string", + description: "Atlas organization ID to filter projects", + required: false, + }, + ]); + + validateThrowsForInvalidArguments(integration, "atlas-list-projects", orgIdInvalidArgs); + }); + withCredentials(integration, () => { const projName = "testProj-" + randomId; From 33e76b21986201ae847f2d73afae873ff1c87fd9 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 8 Oct 2025 21:18:11 +0100 Subject: [PATCH 09/11] update request input --- src/tools/atlas/create/createFreeCluster.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/atlas/create/createFreeCluster.ts b/src/tools/atlas/create/createFreeCluster.ts index a3c11cc22..b2d1efbed 100644 --- a/src/tools/atlas/create/createFreeCluster.ts +++ b/src/tools/atlas/create/createFreeCluster.ts @@ -21,7 +21,7 @@ export class CreateFreeClusterTool extends AtlasToolBase { }: ToolArgs): Promise { const input = { groupId: projectId, - clusterName, + name: clusterName, clusterType: "REPLICASET", replicationSpecs: [ { From 6a7780c5a2fea0df012ad9ef966e875e9500a9fa Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 8 Oct 2025 21:22:13 +0100 Subject: [PATCH 10/11] Fix tests --- tests/integration/helpers.ts | 2 -- tests/integration/tools/atlas/atlasHelpers.ts | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 6cd8f972a..889f98b9d 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -105,8 +105,6 @@ export function setupIntegrationTest( // Mock API Client for tests if (!userConfig.apiClientId && !userConfig.apiClientSecret) { - userConfig.apiClientId = "test"; - userConfig.apiClientSecret = "test"; const mockFn = vi.fn().mockResolvedValue(true); session.apiClient.validateAccessToken = mockFn; } diff --git a/tests/integration/tools/atlas/atlasHelpers.ts b/tests/integration/tools/atlas/atlasHelpers.ts index 98f0e407d..247a0fb41 100644 --- a/tests/integration/tools/atlas/atlasHelpers.ts +++ b/tests/integration/tools/atlas/atlasHelpers.ts @@ -13,8 +13,8 @@ export function describeWithAtlas(name: string, fn: IntegrationTestFunction): vo const integration = setupIntegrationTest( () => ({ ...defaultTestConfig, - apiClientId: process.env.MDB_MCP_API_CLIENT_ID, - apiClientSecret: process.env.MDB_MCP_API_CLIENT_SECRET, + apiClientId: process.env.MDB_MCP_API_CLIENT_ID || "test-client", + apiClientSecret: process.env.MDB_MCP_API_CLIENT_SECRET || "test-secret", apiBaseUrl: process.env.MDB_MCP_API_BASE_URL ?? "https://cloud-dev.mongodb.com", }), () => defaultDriverOptions From 10a81a1d207a385481007ccb67c4738bae85fcd9 Mon Sep 17 00:00:00 2001 From: Bianca Lisle Date: Wed, 8 Oct 2025 21:25:19 +0100 Subject: [PATCH 11/11] fix sleep import --- tests/integration/tools/atlas/clusters.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/tools/atlas/clusters.test.ts b/tests/integration/tools/atlas/clusters.test.ts index 06ce10071..0cedd139b 100644 --- a/tests/integration/tools/atlas/clusters.test.ts +++ b/tests/integration/tools/atlas/clusters.test.ts @@ -7,6 +7,7 @@ import { projectAndClusterInvalidArgs, validateThrowsForInvalidArguments, validateToolMetadata, + sleep, } from "../../helpers.js"; import { describeWithAtlas, withProject, randomId, parseTable } from "./atlasHelpers.js"; import type { ClusterDescription20240805 } from "../../../../src/common/atlas/openapi.js";