Skip to content

Commit ebe3d9f

Browse files
authored
chore: rework list-projects response (#665)
1 parent 7e28a7e commit ebe3d9f

File tree

4 files changed

+120
-71
lines changed

4 files changed

+120
-71
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
}

tests/accuracy/getPerformanceAdvisor.test.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
1+
import { formatUntrustedData } from "../../src/tools/tool.js";
12
import { describeAccuracyTests } from "./sdk/describeAccuracyTests.js";
23
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
34

5+
const projectId = "68f600519f16226591d054c0";
6+
47
// Shared mock tool implementations
58
const mockedTools = {
69
"atlas-list-projects": (): CallToolResult => {
710
return {
8-
content: [
9-
{
10-
type: "text",
11-
text: "Found 1 project\n\n# | Name | ID\n---|------|----\n1 | mflix | mflix",
12-
},
13-
],
11+
content: formatUntrustedData(
12+
"Found 1 projects",
13+
JSON.stringify([
14+
{
15+
name: "mflix",
16+
id: projectId,
17+
orgId: "68f600589f16226591d054c1",
18+
orgName: "MyOrg",
19+
created: "N/A",
20+
},
21+
])
22+
),
1423
};
1524
},
1625
"atlas-list-clusters": (): CallToolResult => {
@@ -44,7 +53,7 @@ const listProjectsAndClustersToolCalls = [
4453
{
4554
toolName: "atlas-list-clusters",
4655
parameters: {
47-
projectId: "mflix",
56+
projectId,
4857
},
4958
optional: true,
5059
},
@@ -59,7 +68,7 @@ describeAccuracyTests([
5968
{
6069
toolName: "atlas-get-performance-advisor",
6170
parameters: {
62-
projectId: "mflix",
71+
projectId,
6372
clusterName: "mflix-cluster",
6473
operations: ["suggestedIndexes"],
6574
},
@@ -75,7 +84,7 @@ describeAccuracyTests([
7584
{
7685
toolName: "atlas-get-performance-advisor",
7786
parameters: {
78-
projectId: "mflix",
87+
projectId,
7988
clusterName: "mflix-cluster",
8089
operations: ["dropIndexSuggestions"],
8190
},
@@ -91,7 +100,7 @@ describeAccuracyTests([
91100
{
92101
toolName: "atlas-get-performance-advisor",
93102
parameters: {
94-
projectId: "mflix",
103+
projectId,
95104
clusterName: "mflix-cluster",
96105
operations: ["slowQueryLogs"],
97106
namespaces: ["mflix.movies", "mflix.shows"],
@@ -109,7 +118,7 @@ describeAccuracyTests([
109118
{
110119
toolName: "atlas-get-performance-advisor",
111120
parameters: {
112-
projectId: "mflix",
121+
projectId,
113122
clusterName: "mflix-cluster",
114123
operations: ["schemaSuggestions"],
115124
},
@@ -125,7 +134,7 @@ describeAccuracyTests([
125134
{
126135
toolName: "atlas-get-performance-advisor",
127136
parameters: {
128-
projectId: "mflix",
137+
projectId,
129138
clusterName: "mflix-cluster",
130139
},
131140
},
Lines changed: 79 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 = `testProj-${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 = `testProj-${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,51 @@ 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+
});
109+
110+
const elements = getResponseElements(response);
111+
expect(elements).toHaveLength(2);
112+
expect(elements[1]?.text).toContain("<untrusted-user-data-");
113+
expect(elements[1]?.text).toContain(projName);
114+
const data = JSON.parse(getDataFromUntrustedContent(elements[1]?.text ?? "")) as {
115+
name: string;
116+
orgId: string;
117+
}[];
118+
expect(data.length).toBeGreaterThan(0);
119+
expect(data.find((proj) => proj.name === projName && proj.orgId === orgId)).toBeDefined();
120+
121+
expect(elements[0]?.text).toBe(`Found ${data.length} projects`);
122+
});
77123
});
78124
});
79125
});

0 commit comments

Comments
 (0)