Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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),
};
}
}
33 changes: 21 additions & 12 deletions tests/accuracy/getPerformanceAdvisor.test.ts
Original file line number Diff line number Diff line change
@@ -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 => {
Expand Down Expand Up @@ -44,7 +53,7 @@ const listProjectsAndClustersToolCalls = [
{
toolName: "atlas-list-clusters",
parameters: {
projectId: "mflix",
projectId,
},
optional: true,
},
Expand All @@ -59,7 +68,7 @@ describeAccuracyTests([
{
toolName: "atlas-get-performance-advisor",
parameters: {
projectId: "mflix",
projectId,
clusterName: "mflix-cluster",
operations: ["suggestedIndexes"],
},
Expand All @@ -75,7 +84,7 @@ describeAccuracyTests([
{
toolName: "atlas-get-performance-advisor",
parameters: {
projectId: "mflix",
projectId,
clusterName: "mflix-cluster",
operations: ["dropIndexSuggestions"],
},
Expand All @@ -91,7 +100,7 @@ describeAccuracyTests([
{
toolName: "atlas-get-performance-advisor",
parameters: {
projectId: "mflix",
projectId,
clusterName: "mflix-cluster",
operations: ["slowQueryLogs"],
namespaces: ["mflix.movies", "mflix.shows"],
Expand All @@ -109,7 +118,7 @@ describeAccuracyTests([
{
toolName: "atlas-get-performance-advisor",
parameters: {
projectId: "mflix",
projectId,
clusterName: "mflix-cluster",
operations: ["schemaSuggestions"],
},
Expand All @@ -125,7 +134,7 @@ describeAccuracyTests([
{
toolName: "atlas-get-performance-advisor",
parameters: {
projectId: "mflix",
projectId,
clusterName: "mflix-cluster",
},
},
Expand Down
112 changes: 79 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 = `testProj-${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 = `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");
Expand All @@ -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("<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: {},
});

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