Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions src/tools/atlas/create/createProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof this.argsShape>): Promise<CallToolResult> {
Expand Down
38 changes: 18 additions & 20 deletions src/tools/atlas/read/listProjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof this.argsShape>): Promise<CallToolResult> {
Expand All @@ -27,9 +25,9 @@ export class ListProjectsTool extends AtlasToolBase {
}

const orgs: Record<string, string> = 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({
Expand All @@ -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),
};
}
}
114 changes: 81 additions & 33 deletions tests/integration/tools/atlas/projects.test.ts
Original file line number Diff line number Diff line change
@@ -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;
}
},
});
}
});

Expand All @@ -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 = `test-project-${new ObjectId().toString()}`;
projectsToCleanup.push(projName);

const response = await integration.mcpClient().callTool({
name: "atlas-create-project",
arguments: { projectName: projName },
Expand All @@ -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 = `list-projects-test-${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");
Expand All @@ -57,23 +75,53 @@ 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("<untrusted-user-data-");
expect(elements[1]?.text).toContain(projName);
const data = parseTable(getDataFromUntrustedContent(elements[1]?.text ?? ""));
expect(data.length).toBeGreaterThan(0);
let found = false;
for (const project of data) {
if (project["Project Name"] === projName) {
found = true;
}
}
expect(found).toBe(true);

expect(elements[0]?.text).toBe(`Found ${data.length} projects`);
describe("with orgId filter", () => {
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("<untrusted-user-data-");
expect(elements[1]?.text).toContain(projName);
const data = JSON.parse(getDataFromUntrustedContent(elements[1]?.text ?? "")) as {
name: string;
orgId: string;
}[];
expect(data.length).toBeGreaterThan(0);
expect(data.every((proj) => 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: {
orgId,
},
});

const elements = getResponseElements(response);
expect(elements).toHaveLength(2);
expect(elements[1]?.text).toContain("<untrusted-user-data-");
expect(elements[1]?.text).toContain(projName);
const data = JSON.parse(getDataFromUntrustedContent(elements[1]?.text ?? "")) as {
name: string;
orgId: string;
}[];
expect(data.length).toBeGreaterThan(0);
expect(data.find((proj) => proj.name === projName && proj.orgId === orgId)).toBeDefined();

expect(elements[0]?.text).toBe(`Found ${data.length} projects`);
});
});
});
});
Loading