Skip to content

Commit 6521882

Browse files
authored
chore: update atlas tools output to json - MCP-264 (#653)
1 parent 2c80bf4 commit 6521882

File tree

10 files changed

+110
-182
lines changed

10 files changed

+110
-182
lines changed

src/tools/atlas/read/inspectAccessList.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,14 @@ export class InspectAccessListTool extends AtlasToolBase {
3232
};
3333
}
3434

35+
const entries = results.map((entry) => ({
36+
ipAddress: entry.ipAddress,
37+
cidrBlock: entry.cidrBlock,
38+
comment: entry.comment,
39+
}));
40+
3541
return {
36-
content: formatUntrustedData(
37-
`Found ${results.length} access list entries`,
38-
`IP ADDRESS | CIDR | COMMENT
39-
------|------|------
40-
${results
41-
.map((entry) => {
42-
return `${entry.ipAddress} | ${entry.cidrBlock} | ${entry.comment}`;
43-
})
44-
.join("\n")}`
45-
),
42+
content: formatUntrustedData(`Found ${results.length} access list entries`, JSON.stringify(entries)),
4643
};
4744
}
4845
}

src/tools/atlas/read/inspectCluster.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,17 @@ export class InspectClusterTool extends AtlasToolBase {
2525
}
2626

2727
private formatOutput(formattedCluster: Cluster): CallToolResult {
28+
const clusterDetails = {
29+
name: formattedCluster.name || "Unknown",
30+
instanceType: formattedCluster.instanceType,
31+
instanceSize: formattedCluster.instanceSize || "N/A",
32+
state: formattedCluster.state || "UNKNOWN",
33+
mongoDBVersion: formattedCluster.mongoDBVersion || "N/A",
34+
connectionStrings: formattedCluster.connectionStrings || {},
35+
};
36+
2837
return {
29-
content: formatUntrustedData(
30-
"Cluster details:",
31-
`Cluster Name | Cluster Type | Tier | State | MongoDB Version | Connection String
32-
----------------|----------------|----------------|----------------|----------------|----------------
33-
${formattedCluster.name || "Unknown"} | ${formattedCluster.instanceType} | ${formattedCluster.instanceSize || "N/A"} | ${formattedCluster.state || "UNKNOWN"} | ${formattedCluster.mongoDBVersion || "N/A"} | ${formattedCluster.connectionStrings?.standardSrv || formattedCluster.connectionStrings?.standard || "N/A"}`
34-
),
38+
content: formatUntrustedData("Cluster details:", JSON.stringify(clusterDetails)),
3539
};
3640
}
3741
}

src/tools/atlas/read/listAlerts.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,20 @@ export class ListAlertsTool extends AtlasToolBase {
2828
return { content: [{ type: "text", text: "No alerts found in your MongoDB Atlas project." }] };
2929
}
3030

31-
// Format alerts as a table
32-
const output =
33-
`Alert ID | Status | Created | Updated | Type | Comment
34-
----------|---------|----------|----------|------|--------
35-
` +
36-
data.results
37-
.map((alert) => {
38-
const created = alert.created ? new Date(alert.created).toLocaleString() : "N/A";
39-
const updated = alert.updated ? new Date(alert.updated).toLocaleString() : "N/A";
40-
const comment = alert.acknowledgementComment ?? "N/A";
41-
return `${alert.id} | ${alert.status} | ${created} | ${updated} | ${alert.eventTypeName} | ${comment}`;
42-
})
43-
.join("\n");
31+
const alerts = data.results.map((alert) => ({
32+
id: alert.id,
33+
status: alert.status,
34+
created: alert.created ? new Date(alert.created).toISOString() : "N/A",
35+
updated: alert.updated ? new Date(alert.updated).toISOString() : "N/A",
36+
eventTypeName: alert.eventTypeName,
37+
acknowledgementComment: alert.acknowledgementComment ?? "N/A",
38+
}));
4439

4540
return {
46-
content: formatUntrustedData(`Found ${data.results.length} alerts in project ${projectId}`, output),
41+
content: formatUntrustedData(
42+
`Found ${data.results.length} alerts in project ${projectId}`,
43+
JSON.stringify(alerts)
44+
),
4745
};
4846
}
4947
}

src/tools/atlas/read/listClusters.ts

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -59,28 +59,22 @@ export class ListClustersTool extends AtlasToolBase {
5959
}
6060
const formattedClusters = clusters.results
6161
.map((result) => {
62-
return (result.clusters || []).map((cluster) => {
63-
return { ...result, ...cluster, clusters: undefined };
64-
});
62+
return (result.clusters || []).map((cluster) => ({
63+
projectName: result.groupName,
64+
projectId: result.groupId,
65+
clusterName: cluster.name,
66+
}));
6567
})
6668
.flat();
6769
if (!formattedClusters.length) {
6870
throw new Error("No clusters found.");
6971
}
70-
const rows = formattedClusters
71-
.map((cluster) => {
72-
return `${cluster.groupName} (${cluster.groupId}) | ${cluster.name}`;
73-
})
74-
.join("\n");
72+
7573
return {
76-
content: [
77-
{
78-
type: "text",
79-
text: `Project | Cluster Name
80-
----------------|----------------
81-
${rows}`,
82-
},
83-
],
74+
content: formatUntrustedData(
75+
`Found ${formattedClusters.length} clusters across all projects`,
76+
JSON.stringify(formattedClusters)
77+
),
8478
};
8579
}
8680

@@ -98,16 +92,11 @@ ${rows}`,
9892
const formattedClusters = clusters?.results?.map((cluster) => formatCluster(cluster)) || [];
9993
const formattedFlexClusters = flexClusters?.results?.map((cluster) => formatFlexCluster(cluster)) || [];
10094
const allClusters = [...formattedClusters, ...formattedFlexClusters];
95+
10196
return {
10297
content: formatUntrustedData(
10398
`Found ${allClusters.length} clusters in project "${project.name}" (${project.id}):`,
104-
`Cluster Name | Cluster Type | Tier | State | MongoDB Version | Connection String
105-
----------------|----------------|----------------|----------------|----------------|----------------
106-
${allClusters
107-
.map((formattedCluster) => {
108-
return `${formattedCluster.name || "Unknown"} | ${formattedCluster.instanceType} | ${formattedCluster.instanceSize || "N/A"} | ${formattedCluster.state || "UNKNOWN"} | ${formattedCluster.mongoDBVersion || "N/A"} | ${formattedCluster.connectionStrings?.standardSrv || formattedCluster.connectionStrings?.standard || "N/A"}`;
109-
})
110-
.join("\n")}`
99+
JSON.stringify(allClusters)
111100
),
112101
};
113102
}

src/tools/atlas/read/listDBUsers.ts

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
22
import { AtlasToolBase } from "../atlasTool.js";
33
import type { ToolArgs, OperationType } from "../../tool.js";
44
import { formatUntrustedData } from "../../tool.js";
5-
import type { DatabaseUserRole, UserScope } from "../../../common/atlas/openapi.js";
65
import { AtlasArgs } from "../../args.js";
76

87
export const ListDBUsersArgs = {
@@ -32,36 +31,26 @@ export class ListDBUsersTool extends AtlasToolBase {
3231
};
3332
}
3433

35-
const output =
36-
`Username | Roles | Scopes
37-
----------------|----------------|----------------
38-
` +
39-
data.results
40-
.map((user) => {
41-
return `${user.username} | ${formatRoles(user.roles)} | ${formatScopes(user.scopes)}`;
42-
})
43-
.join("\n");
34+
const users = data.results.map((user) => ({
35+
username: user.username,
36+
roles:
37+
user.roles?.map((role) => ({
38+
roleName: role.roleName,
39+
databaseName: role.databaseName,
40+
collectionName: role.collectionName,
41+
})) ?? [],
42+
scopes:
43+
user.scopes?.map((scope) => ({
44+
type: scope.type,
45+
name: scope.name,
46+
})) ?? [],
47+
}));
48+
4449
return {
45-
content: formatUntrustedData(`Found ${data.results.length} database users in project ${projectId}`, output),
50+
content: formatUntrustedData(
51+
`Found ${data.results.length} database users in project ${projectId}`,
52+
JSON.stringify(users)
53+
),
4654
};
4755
}
4856
}
49-
50-
function formatRoles(roles?: DatabaseUserRole[]): string {
51-
if (!roles?.length) {
52-
return "N/A";
53-
}
54-
return roles
55-
.map(
56-
(role) =>
57-
`${role.roleName}${role.databaseName ? `@${role.databaseName}${role.collectionName ? `:${role.collectionName}` : ""}` : ""}`
58-
)
59-
.join(", ");
60-
}
61-
62-
function formatScopes(scopes?: UserScope[]): string {
63-
if (!scopes?.length) {
64-
return "All";
65-
}
66-
return scopes.map((scope) => `${scope.type}:${scope.name}`).join(", ");
67-
}

src/tools/atlas/read/listOrgs.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,15 @@ export class ListOrganizationsTool extends AtlasToolBase {
1818
};
1919
}
2020

21-
// Format organizations as a table
22-
const output =
23-
`Organization Name | Organization ID
24-
----------------| ----------------
25-
` +
26-
data.results
27-
.map((org) => {
28-
return `${org.name} | ${org.id}`;
29-
})
30-
.join("\n");
21+
const orgs = data.results.map((org) => ({
22+
name: org.name,
23+
id: org.id,
24+
}));
25+
3126
return {
3227
content: formatUntrustedData(
3328
`Found ${data.results.length} organizations in your MongoDB Atlas account.`,
34-
output
29+
JSON.stringify(orgs)
3530
),
3631
};
3732
}

tests/integration/tools/atlas/alerts.test.ts

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { expectDefined, getResponseElements } from "../../helpers.js";
2-
import { parseTable, describeWithAtlas, withProject } from "./atlasHelpers.js";
1+
import { expectDefined, getResponseContent } from "../../helpers.js";
2+
import { describeWithAtlas, withProject } from "./atlasHelpers.js";
33
import { expect, it } from "vitest";
44

55
describeWithAtlas("atlas-list-alerts", (integration) => {
@@ -13,26 +13,20 @@ describeWithAtlas("atlas-list-alerts", (integration) => {
1313
});
1414

1515
withProject(integration, ({ getProjectId }) => {
16-
it("returns alerts in table format", async () => {
16+
it("returns alerts in JSON format", async () => {
1717
const response = await integration.mcpClient().callTool({
1818
name: "atlas-list-alerts",
1919
arguments: { projectId: getProjectId() },
2020
});
2121

22-
const elements = getResponseElements(response.content);
23-
expect(elements).toHaveLength(1);
24-
25-
const data = parseTable(elements[0]?.text ?? "");
26-
27-
// Since we can't guarantee alerts will exist, we just verify the table structure
28-
if (data.length > 0) {
29-
const alert = data[0];
30-
expect(alert).toHaveProperty("Alert ID");
31-
expect(alert).toHaveProperty("Status");
32-
expect(alert).toHaveProperty("Created");
33-
expect(alert).toHaveProperty("Updated");
34-
expect(alert).toHaveProperty("Type");
35-
expect(alert).toHaveProperty("Comment");
22+
const content = getResponseContent(response.content);
23+
// check that there are alerts or no alerts
24+
if (content.includes("Found alerts in project")) {
25+
expect(content).toContain("<untrusted-user-data-");
26+
// expect projectId in the content
27+
expect(content).toContain(getProjectId());
28+
} else {
29+
expect(content).toContain("No alerts found");
3630
}
3731
});
3832
});

tests/integration/tools/atlas/atlasHelpers.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -101,26 +101,6 @@ export function withProject(integration: IntegrationTest, fn: ProjectTestFunctio
101101
});
102102
}
103103

104-
export function parseTable(text: string): Record<string, string>[] {
105-
const data = text
106-
.split("\n")
107-
.filter((line) => line.trim() !== "")
108-
.map((line) => line.split("|").map((cell) => cell.trim()));
109-
110-
const headers = data[0];
111-
return data
112-
.filter((_, index) => index >= 2)
113-
.map((cells) => {
114-
const row: Record<string, string> = {};
115-
cells.forEach((cell, index) => {
116-
if (headers) {
117-
row[headers[index] ?? ""] = cell;
118-
}
119-
});
120-
return row;
121-
});
122-
}
123-
124104
export const randomId = new ObjectId().toString();
125105

126106
async function createProject(apiClient: ApiClient): Promise<Group & Required<Pick<Group, "id">>> {

0 commit comments

Comments
 (0)