Skip to content

Commit dc2f99b

Browse files
committed
chore: rework list-projects response
1 parent adbf3a0 commit dc2f99b

File tree

3 files changed

+101
-59
lines changed

3 files changed

+101
-59
lines changed

src/tools/atlas/create/createProject.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,13 @@ import { AtlasToolBase } from "../atlasTool.js";
44
import type { Group } from "../../../common/atlas/openapi.js";
55
import { AtlasArgs } from "../../args.js";
66

7-
export const CreateProjectArgs = {
8-
projectName: AtlasArgs.projectName().optional().describe("Name for the new project"),
9-
organizationId: AtlasArgs.organizationId().optional().describe("Organization ID for the new project"),
10-
};
11-
127
export class CreateProjectTool extends AtlasToolBase {
138
public name = "atlas-create-project";
149
protected description = "Create a MongoDB Atlas project";
1510
public operationType: OperationType = "create";
1611
protected argsShape = {
17-
...CreateProjectArgs,
12+
projectName: AtlasArgs.projectName().optional().describe("Name for the new project"),
13+
organizationId: AtlasArgs.organizationId().optional().describe("Organization ID for the new project"),
1814
};
1915

2016
protected async execute({ projectName, organizationId }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {

src/tools/atlas/read/listProjects.ts

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,14 @@ import { formatUntrustedData } from "../../tool.js";
55
import type { ToolArgs } from "../../tool.js";
66
import { AtlasArgs } from "../../args.js";
77

8-
export const ListProjectsArgs = {
9-
orgId: AtlasArgs.organizationId().describe("Atlas organization ID to filter projects").optional(),
10-
};
11-
128
export class ListProjectsTool extends AtlasToolBase {
139
public name = "atlas-list-projects";
1410
protected description = "List MongoDB Atlas projects";
1511
public operationType: OperationType = "read";
1612
protected argsShape = {
17-
...ListProjectsArgs,
13+
orgId: AtlasArgs.organizationId()
14+
.describe("Atlas organization ID to filter projects. If not provided, projects for all orgs are returned.")
15+
.optional(),
1816
};
1917

2018
protected async execute({ orgId }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
@@ -27,9 +25,9 @@ export class ListProjectsTool extends AtlasToolBase {
2725
}
2826

2927
const orgs: Record<string, string> = orgData.results
30-
.map((org) => [org.id || "", org.name])
31-
.filter(([id]) => id)
32-
.reduce((acc, [id, name]) => ({ ...acc, [id as string]: name }), {});
28+
.filter((org) => org.id)
29+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
30+
.reduce((acc, org) => ({ ...acc, [org.id!]: org.name }), {});
3331

3432
const data = orgId
3533
? await this.session.apiClient.listOrganizationProjects({
@@ -47,19 +45,19 @@ export class ListProjectsTool extends AtlasToolBase {
4745
};
4846
}
4947

50-
// Format projects as a table
51-
const rows = data.results
52-
.map((project) => {
53-
const createdAt = project.created ? new Date(project.created).toLocaleString() : "N/A";
54-
const orgName = orgs[project.orgId] ?? "N/A";
55-
return `${project.name} | ${project.id} | ${orgName} | ${project.orgId} | ${createdAt}`;
56-
})
57-
.join("\n");
58-
const formattedProjects = `Project Name | Project ID | Organization Name | Organization ID | Created At
59-
----------------| ----------------| ----------------| ----------------| ----------------
60-
${rows}`;
48+
const serializedProjects = JSON.stringify(
49+
data.results.map((project) => ({
50+
name: project.name,
51+
id: project.id,
52+
orgId: project.orgId,
53+
orgName: orgs[project.orgId] ?? "N/A",
54+
created: project.created ? new Date(project.created).toLocaleString() : "N/A",
55+
})),
56+
null,
57+
2
58+
);
6159
return {
62-
content: formatUntrustedData(`Found ${data.results.length} projects`, formattedProjects),
60+
content: formatUntrustedData(`Found ${data.results.length} projects`, serializedProjects),
6361
};
6462
}
6563
}
Lines changed: 81 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,26 @@
11
import { ObjectId } from "mongodb";
2-
import { parseTable, describeWithAtlas } from "./atlasHelpers.js";
2+
import { describeWithAtlas } from "./atlasHelpers.js";
33
import { expectDefined, getDataFromUntrustedContent, getResponseElements } from "../../helpers.js";
4-
import { afterAll, describe, expect, it } from "vitest";
5-
6-
const randomId = new ObjectId().toString();
4+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
75

86
describeWithAtlas("projects", (integration) => {
9-
const projName = "testProj-" + randomId;
7+
const projectsToCleanup: string[] = [];
108

119
afterAll(async () => {
1210
const session = integration.mcpServer().session;
11+
const projects =
12+
(await session.apiClient.listProjects()).results?.filter((project) =>
13+
projectsToCleanup.includes(project.name)
14+
) || [];
1315

14-
const projects = await session.apiClient.listProjects();
15-
for (const project of projects?.results || []) {
16-
if (project.name === projName) {
17-
await session.apiClient.deleteProject({
18-
params: {
19-
path: {
20-
groupId: project.id || "",
21-
},
16+
for (const project of projects) {
17+
await session.apiClient.deleteProject({
18+
params: {
19+
path: {
20+
groupId: project.id || "",
2221
},
23-
});
24-
break;
25-
}
22+
},
23+
});
2624
}
2725
});
2826

@@ -36,7 +34,11 @@ describeWithAtlas("projects", (integration) => {
3634
expect(createProject.inputSchema.properties).toHaveProperty("projectName");
3735
expect(createProject.inputSchema.properties).toHaveProperty("organizationId");
3836
});
37+
3938
it("should create a project", async () => {
39+
const projName = `test-project-${new ObjectId().toString()}`;
40+
projectsToCleanup.push(projName);
41+
4042
const response = await integration.mcpClient().callTool({
4143
name: "atlas-create-project",
4244
arguments: { projectName: projName },
@@ -47,7 +49,23 @@ describeWithAtlas("projects", (integration) => {
4749
expect(elements[0]?.text).toContain(projName);
4850
});
4951
});
52+
5053
describe("atlas-list-projects", () => {
54+
let projName: string;
55+
let orgId: string;
56+
beforeAll(async () => {
57+
projName = `list-projects-test-${new ObjectId().toString()}`;
58+
projectsToCleanup.push(projName);
59+
60+
const orgs = await integration.mcpServer().session.apiClient.listOrganizations();
61+
orgId = (orgs.results && orgs.results[0]?.id) ?? "";
62+
63+
await integration.mcpClient().callTool({
64+
name: "atlas-create-project",
65+
arguments: { projectName: projName, organizationId: orgId },
66+
});
67+
});
68+
5169
it("should have correct metadata", async () => {
5270
const { tools } = await integration.mcpClient().listTools();
5371
const listProjects = tools.find((tool) => tool.name === "atlas-list-projects");
@@ -57,23 +75,53 @@ describeWithAtlas("projects", (integration) => {
5775
expect(listProjects.inputSchema.properties).toHaveProperty("orgId");
5876
});
5977

60-
it("returns project names", async () => {
61-
const response = await integration.mcpClient().callTool({ name: "atlas-list-projects", arguments: {} });
62-
const elements = getResponseElements(response);
63-
expect(elements).toHaveLength(2);
64-
expect(elements[1]?.text).toContain("<untrusted-user-data-");
65-
expect(elements[1]?.text).toContain(projName);
66-
const data = parseTable(getDataFromUntrustedContent(elements[1]?.text ?? ""));
67-
expect(data.length).toBeGreaterThan(0);
68-
let found = false;
69-
for (const project of data) {
70-
if (project["Project Name"] === projName) {
71-
found = true;
72-
}
73-
}
74-
expect(found).toBe(true);
75-
76-
expect(elements[0]?.text).toBe(`Found ${data.length} projects`);
78+
describe("with orgId filter", () => {
79+
it("returns projects only for that org", async () => {
80+
const response = await integration.mcpClient().callTool({
81+
name: "atlas-list-projects",
82+
arguments: {
83+
orgId,
84+
},
85+
});
86+
87+
const elements = getResponseElements(response);
88+
expect(elements).toHaveLength(2);
89+
expect(elements[1]?.text).toContain("<untrusted-user-data-");
90+
expect(elements[1]?.text).toContain(projName);
91+
const data = JSON.parse(getDataFromUntrustedContent(elements[1]?.text ?? "")) as {
92+
name: string;
93+
orgId: string;
94+
}[];
95+
expect(data.length).toBeGreaterThan(0);
96+
expect(data.every((proj) => proj.orgId === orgId)).toBe(true);
97+
expect(data.find((proj) => proj.name === projName)).toBeDefined();
98+
99+
expect(elements[0]?.text).toBe(`Found ${data.length} projects`);
100+
});
101+
});
102+
103+
describe("without orgId filter", () => {
104+
it("returns projects for all orgs", async () => {
105+
const response = await integration.mcpClient().callTool({
106+
name: "atlas-list-projects",
107+
arguments: {
108+
orgId,
109+
},
110+
});
111+
112+
const elements = getResponseElements(response);
113+
expect(elements).toHaveLength(2);
114+
expect(elements[1]?.text).toContain("<untrusted-user-data-");
115+
expect(elements[1]?.text).toContain(projName);
116+
const data = JSON.parse(getDataFromUntrustedContent(elements[1]?.text ?? "")) as {
117+
name: string;
118+
orgId: string;
119+
}[];
120+
expect(data.length).toBeGreaterThan(0);
121+
expect(data.find((proj) => proj.name === projName && proj.orgId === orgId)).toBeDefined();
122+
123+
expect(elements[0]?.text).toBe(`Found ${data.length} projects`);
124+
});
77125
});
78126
});
79127
});

0 commit comments

Comments
 (0)