diff --git a/src/tools/atlas/create/createProject.ts b/src/tools/atlas/create/createProject.ts index b981fd8e8..919e851ca 100644 --- a/src/tools/atlas/create/createProject.ts +++ b/src/tools/atlas/create/createProject.ts @@ -4,17 +4,13 @@ import { AtlasToolBase } from "../atlasTool.js"; import type { Group } from "../../../common/atlas/openapi.js"; import { AtlasArgs } from "../../args.js"; -export const CreateProjectArgs = { - projectName: AtlasArgs.projectName().optional().describe("Name for the new project"), - organizationId: AtlasArgs.organizationId().optional().describe("Organization ID for the new project"), -}; - export class CreateProjectTool extends AtlasToolBase { public name = "atlas-create-project"; protected description = "Create a MongoDB Atlas project"; public operationType: OperationType = "create"; protected argsShape = { - ...CreateProjectArgs, + projectName: AtlasArgs.projectName().optional().describe("Name for the new project"), + organizationId: AtlasArgs.organizationId().optional().describe("Organization ID for the new project"), }; protected async execute({ projectName, organizationId }: ToolArgs): Promise { diff --git a/src/tools/atlas/read/listProjects.ts b/src/tools/atlas/read/listProjects.ts index 3b7d24939..2c2bd2dc4 100644 --- a/src/tools/atlas/read/listProjects.ts +++ b/src/tools/atlas/read/listProjects.ts @@ -5,16 +5,14 @@ import { formatUntrustedData } from "../../tool.js"; import type { ToolArgs } from "../../tool.js"; import { AtlasArgs } from "../../args.js"; -export const ListProjectsArgs = { - orgId: AtlasArgs.organizationId().describe("Atlas organization ID to filter projects").optional(), -}; - export class ListProjectsTool extends AtlasToolBase { public name = "atlas-list-projects"; protected description = "List MongoDB Atlas projects"; public operationType: OperationType = "read"; protected argsShape = { - ...ListProjectsArgs, + orgId: AtlasArgs.organizationId() + .describe("Atlas organization ID to filter projects. If not provided, projects for all orgs are returned.") + .optional(), }; protected async execute({ orgId }: ToolArgs): Promise { @@ -27,9 +25,9 @@ export class ListProjectsTool extends AtlasToolBase { } const orgs: Record = orgData.results - .map((org) => [org.id || "", org.name]) - .filter(([id]) => id) - .reduce((acc, [id, name]) => ({ ...acc, [id as string]: name }), {}); + .filter((org) => org.id) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + .reduce((acc, org) => ({ ...acc, [org.id!]: org.name }), {}); const data = orgId ? await this.session.apiClient.listOrganizationProjects({ @@ -47,19 +45,19 @@ export class ListProjectsTool extends AtlasToolBase { }; } - // Format projects as a table - const rows = data.results - .map((project) => { - const createdAt = project.created ? new Date(project.created).toLocaleString() : "N/A"; - const orgName = orgs[project.orgId] ?? "N/A"; - return `${project.name} | ${project.id} | ${orgName} | ${project.orgId} | ${createdAt}`; - }) - .join("\n"); - const formattedProjects = `Project Name | Project ID | Organization Name | Organization ID | Created At -----------------| ----------------| ----------------| ----------------| ---------------- -${rows}`; + const serializedProjects = JSON.stringify( + data.results.map((project) => ({ + name: project.name, + id: project.id, + orgId: project.orgId, + orgName: orgs[project.orgId] ?? "N/A", + created: project.created ? new Date(project.created).toLocaleString() : "N/A", + })), + null, + 2 + ); return { - content: formatUntrustedData(`Found ${data.results.length} projects`, formattedProjects), + content: formatUntrustedData(`Found ${data.results.length} projects`, serializedProjects), }; } } diff --git a/tests/accuracy/getPerformanceAdvisor.test.ts b/tests/accuracy/getPerformanceAdvisor.test.ts index 02b61b33f..f54b3ab2e 100644 --- a/tests/accuracy/getPerformanceAdvisor.test.ts +++ b/tests/accuracy/getPerformanceAdvisor.test.ts @@ -1,16 +1,25 @@ +import { formatUntrustedData } from "../../src/tools/tool.js"; import { describeAccuracyTests } from "./sdk/describeAccuracyTests.js"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +const projectId = "68f600519f16226591d054c0"; + // Shared mock tool implementations const mockedTools = { "atlas-list-projects": (): CallToolResult => { return { - content: [ - { - type: "text", - text: "Found 1 project\n\n# | Name | ID\n---|------|----\n1 | mflix | mflix", - }, - ], + content: formatUntrustedData( + "Found 1 projects", + JSON.stringify([ + { + name: "mflix", + id: projectId, + orgId: "68f600589f16226591d054c1", + orgName: "MyOrg", + created: "N/A", + }, + ]) + ), }; }, "atlas-list-clusters": (): CallToolResult => { @@ -44,7 +53,7 @@ const listProjectsAndClustersToolCalls = [ { toolName: "atlas-list-clusters", parameters: { - projectId: "mflix", + projectId, }, optional: true, }, @@ -59,7 +68,7 @@ describeAccuracyTests([ { toolName: "atlas-get-performance-advisor", parameters: { - projectId: "mflix", + projectId, clusterName: "mflix-cluster", operations: ["suggestedIndexes"], }, @@ -75,7 +84,7 @@ describeAccuracyTests([ { toolName: "atlas-get-performance-advisor", parameters: { - projectId: "mflix", + projectId, clusterName: "mflix-cluster", operations: ["dropIndexSuggestions"], }, @@ -91,7 +100,7 @@ describeAccuracyTests([ { toolName: "atlas-get-performance-advisor", parameters: { - projectId: "mflix", + projectId, clusterName: "mflix-cluster", operations: ["slowQueryLogs"], namespaces: ["mflix.movies", "mflix.shows"], @@ -109,7 +118,7 @@ describeAccuracyTests([ { toolName: "atlas-get-performance-advisor", parameters: { - projectId: "mflix", + projectId, clusterName: "mflix-cluster", operations: ["schemaSuggestions"], }, @@ -125,7 +134,7 @@ describeAccuracyTests([ { toolName: "atlas-get-performance-advisor", parameters: { - projectId: "mflix", + projectId, clusterName: "mflix-cluster", }, }, diff --git a/tests/integration/tools/atlas/projects.test.ts b/tests/integration/tools/atlas/projects.test.ts index de637a23a..cfa78efe0 100644 --- a/tests/integration/tools/atlas/projects.test.ts +++ b/tests/integration/tools/atlas/projects.test.ts @@ -1,28 +1,26 @@ import { ObjectId } from "mongodb"; -import { parseTable, describeWithAtlas } from "./atlasHelpers.js"; +import { describeWithAtlas } from "./atlasHelpers.js"; import { expectDefined, getDataFromUntrustedContent, getResponseElements } from "../../helpers.js"; -import { afterAll, describe, expect, it } from "vitest"; - -const randomId = new ObjectId().toString(); +import { afterAll, beforeAll, describe, expect, it } from "vitest"; describeWithAtlas("projects", (integration) => { - const projName = "testProj-" + randomId; + const projectsToCleanup: string[] = []; afterAll(async () => { const session = integration.mcpServer().session; + const projects = + (await session.apiClient.listProjects()).results?.filter((project) => + projectsToCleanup.includes(project.name) + ) || []; - 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 || "", - }, + for (const project of projects) { + await session.apiClient.deleteProject({ + params: { + path: { + groupId: project.id || "", }, - }); - break; - } + }, + }); } }); @@ -36,7 +34,11 @@ describeWithAtlas("projects", (integration) => { expect(createProject.inputSchema.properties).toHaveProperty("projectName"); expect(createProject.inputSchema.properties).toHaveProperty("organizationId"); }); + it("should create a project", async () => { + const projName = `testProj-${new ObjectId().toString()}`; + projectsToCleanup.push(projName); + const response = await integration.mcpClient().callTool({ name: "atlas-create-project", arguments: { projectName: projName }, @@ -47,7 +49,23 @@ describeWithAtlas("projects", (integration) => { expect(elements[0]?.text).toContain(projName); }); }); + describe("atlas-list-projects", () => { + let projName: string; + let orgId: string; + beforeAll(async () => { + projName = `testProj-${new ObjectId().toString()}`; + projectsToCleanup.push(projName); + + const orgs = await integration.mcpServer().session.apiClient.listOrganizations(); + orgId = (orgs.results && orgs.results[0]?.id) ?? ""; + + await integration.mcpClient().callTool({ + name: "atlas-create-project", + arguments: { projectName: projName, organizationId: orgId }, + }); + }); + it("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); const listProjects = tools.find((tool) => tool.name === "atlas-list-projects"); @@ -57,23 +75,51 @@ describeWithAtlas("projects", (integration) => { 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(" { + it("returns projects only for that org", async () => { + const response = await integration.mcpClient().callTool({ + name: "atlas-list-projects", + arguments: { + orgId, + }, + }); + + const elements = getResponseElements(response); + expect(elements).toHaveLength(2); + expect(elements[1]?.text).toContain(" proj.orgId === orgId)).toBe(true); + expect(data.find((proj) => proj.name === projName)).toBeDefined(); + + expect(elements[0]?.text).toBe(`Found ${data.length} projects`); + }); + }); + + describe("without orgId filter", () => { + it("returns projects for all orgs", 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(" proj.name === projName && proj.orgId === orgId)).toBeDefined(); + + expect(elements[0]?.text).toBe(`Found ${data.length} projects`); + }); }); }); });