diff --git a/src/tools/atlas/read/inspectAccessList.ts b/src/tools/atlas/read/inspectAccessList.ts index 6c8eaed3..78cb8de3 100644 --- a/src/tools/atlas/read/inspectAccessList.ts +++ b/src/tools/atlas/read/inspectAccessList.ts @@ -32,17 +32,14 @@ export class InspectAccessListTool extends AtlasToolBase { }; } + const entries = results.map((entry) => ({ + ipAddress: entry.ipAddress, + cidrBlock: entry.cidrBlock, + comment: entry.comment, + })); + return { - content: formatUntrustedData( - `Found ${results.length} access list entries`, - `IP ADDRESS | CIDR | COMMENT -------|------|------ -${results - .map((entry) => { - return `${entry.ipAddress} | ${entry.cidrBlock} | ${entry.comment}`; - }) - .join("\n")}` - ), + content: formatUntrustedData(`Found ${results.length} access list entries`, JSON.stringify(entries)), }; } } diff --git a/src/tools/atlas/read/inspectCluster.ts b/src/tools/atlas/read/inspectCluster.ts index d4defcc9..fd880610 100644 --- a/src/tools/atlas/read/inspectCluster.ts +++ b/src/tools/atlas/read/inspectCluster.ts @@ -25,13 +25,17 @@ export class InspectClusterTool extends AtlasToolBase { } private formatOutput(formattedCluster: Cluster): CallToolResult { + const clusterDetails = { + name: formattedCluster.name || "Unknown", + instanceType: formattedCluster.instanceType, + instanceSize: formattedCluster.instanceSize || "N/A", + state: formattedCluster.state || "UNKNOWN", + mongoDBVersion: formattedCluster.mongoDBVersion || "N/A", + connectionStrings: formattedCluster.connectionStrings || {}, + }; + return { - content: formatUntrustedData( - "Cluster details:", - `Cluster Name | Cluster Type | Tier | State | MongoDB Version | Connection String -----------------|----------------|----------------|----------------|----------------|---------------- -${formattedCluster.name || "Unknown"} | ${formattedCluster.instanceType} | ${formattedCluster.instanceSize || "N/A"} | ${formattedCluster.state || "UNKNOWN"} | ${formattedCluster.mongoDBVersion || "N/A"} | ${formattedCluster.connectionStrings?.standardSrv || formattedCluster.connectionStrings?.standard || "N/A"}` - ), + content: formatUntrustedData("Cluster details:", JSON.stringify(clusterDetails)), }; } } diff --git a/src/tools/atlas/read/listAlerts.ts b/src/tools/atlas/read/listAlerts.ts index d55a917f..36d3a67d 100644 --- a/src/tools/atlas/read/listAlerts.ts +++ b/src/tools/atlas/read/listAlerts.ts @@ -28,22 +28,20 @@ export class ListAlertsTool extends AtlasToolBase { return { content: [{ type: "text", text: "No alerts found in your MongoDB Atlas project." }] }; } - // Format alerts as a table - const output = - `Alert ID | Status | Created | Updated | Type | Comment -----------|---------|----------|----------|------|-------- -` + - data.results - .map((alert) => { - const created = alert.created ? new Date(alert.created).toLocaleString() : "N/A"; - const updated = alert.updated ? new Date(alert.updated).toLocaleString() : "N/A"; - const comment = alert.acknowledgementComment ?? "N/A"; - return `${alert.id} | ${alert.status} | ${created} | ${updated} | ${alert.eventTypeName} | ${comment}`; - }) - .join("\n"); + const alerts = data.results.map((alert) => ({ + id: alert.id, + status: alert.status, + created: alert.created ? new Date(alert.created).toISOString() : null, + updated: alert.updated ? new Date(alert.updated).toISOString() : null, + eventTypeName: alert.eventTypeName, + acknowledgementComment: alert.acknowledgementComment ?? "N/A", + })); return { - content: formatUntrustedData(`Found ${data.results.length} alerts in project ${projectId}`, output), + content: formatUntrustedData( + `Found ${data.results.length} alerts in project ${projectId}`, + JSON.stringify(alerts) + ), }; } } diff --git a/src/tools/atlas/read/listClusters.ts b/src/tools/atlas/read/listClusters.ts index 1dfe626e..d626f6d9 100644 --- a/src/tools/atlas/read/listClusters.ts +++ b/src/tools/atlas/read/listClusters.ts @@ -59,28 +59,22 @@ export class ListClustersTool extends AtlasToolBase { } const formattedClusters = clusters.results .map((result) => { - return (result.clusters || []).map((cluster) => { - return { ...result, ...cluster, clusters: undefined }; - }); + return (result.clusters || []).map((cluster) => ({ + projectName: result.groupName, + projectId: result.groupId, + clusterName: cluster.name, + })); }) .flat(); if (!formattedClusters.length) { throw new Error("No clusters found."); } - const rows = formattedClusters - .map((cluster) => { - return `${cluster.groupName} (${cluster.groupId}) | ${cluster.name}`; - }) - .join("\n"); + return { - content: [ - { - type: "text", - text: `Project | Cluster Name -----------------|---------------- -${rows}`, - }, - ], + content: formatUntrustedData( + `Found ${formattedClusters.length} clusters across all projects`, + JSON.stringify(formattedClusters) + ), }; } @@ -98,16 +92,11 @@ ${rows}`, const formattedClusters = clusters?.results?.map((cluster) => formatCluster(cluster)) || []; const formattedFlexClusters = flexClusters?.results?.map((cluster) => formatFlexCluster(cluster)) || []; const allClusters = [...formattedClusters, ...formattedFlexClusters]; + return { content: formatUntrustedData( `Found ${allClusters.length} clusters in project "${project.name}" (${project.id}):`, - `Cluster Name | Cluster Type | Tier | State | MongoDB Version | Connection String -----------------|----------------|----------------|----------------|----------------|---------------- -${allClusters - .map((formattedCluster) => { - 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"}`; - }) - .join("\n")}` + JSON.stringify(allClusters) ), }; } diff --git a/src/tools/atlas/read/listDBUsers.ts b/src/tools/atlas/read/listDBUsers.ts index 5ab23250..7103f266 100644 --- a/src/tools/atlas/read/listDBUsers.ts +++ b/src/tools/atlas/read/listDBUsers.ts @@ -2,7 +2,6 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { AtlasToolBase } from "../atlasTool.js"; import type { ToolArgs, OperationType } from "../../tool.js"; import { formatUntrustedData } from "../../tool.js"; -import type { DatabaseUserRole, UserScope } from "../../../common/atlas/openapi.js"; import { AtlasArgs } from "../../args.js"; export const ListDBUsersArgs = { @@ -32,36 +31,26 @@ export class ListDBUsersTool extends AtlasToolBase { }; } - const output = - `Username | Roles | Scopes -----------------|----------------|---------------- -` + - data.results - .map((user) => { - return `${user.username} | ${formatRoles(user.roles)} | ${formatScopes(user.scopes)}`; - }) - .join("\n"); + const users = data.results.map((user) => ({ + username: user.username, + roles: + user.roles?.map((role) => ({ + roleName: role.roleName, + databaseName: role.databaseName, + collectionName: role.collectionName, + })) ?? [], + scopes: + user.scopes?.map((scope) => ({ + type: scope.type, + name: scope.name, + })) ?? [], + })); + return { - content: formatUntrustedData(`Found ${data.results.length} database users in project ${projectId}`, output), + content: formatUntrustedData( + `Found ${data.results.length} database users in project ${projectId}`, + JSON.stringify(users) + ), }; } } - -function formatRoles(roles?: DatabaseUserRole[]): string { - if (!roles?.length) { - return "N/A"; - } - return roles - .map( - (role) => - `${role.roleName}${role.databaseName ? `@${role.databaseName}${role.collectionName ? `:${role.collectionName}` : ""}` : ""}` - ) - .join(", "); -} - -function formatScopes(scopes?: UserScope[]): string { - if (!scopes?.length) { - return "All"; - } - return scopes.map((scope) => `${scope.type}:${scope.name}`).join(", "); -} diff --git a/src/tools/atlas/read/listOrgs.ts b/src/tools/atlas/read/listOrgs.ts index b3679193..0145524f 100644 --- a/src/tools/atlas/read/listOrgs.ts +++ b/src/tools/atlas/read/listOrgs.ts @@ -18,20 +18,15 @@ export class ListOrganizationsTool extends AtlasToolBase { }; } - // Format organizations as a table - const output = - `Organization Name | Organization ID -----------------| ---------------- -` + - data.results - .map((org) => { - return `${org.name} | ${org.id}`; - }) - .join("\n"); + const orgs = data.results.map((org) => ({ + name: org.name, + id: org.id, + })); + return { content: formatUntrustedData( `Found ${data.results.length} organizations in your MongoDB Atlas account.`, - output + JSON.stringify(orgs) ), }; } diff --git a/tests/integration/tools/atlas/alerts.test.ts b/tests/integration/tools/atlas/alerts.test.ts index 3bf29658..fa0eae51 100644 --- a/tests/integration/tools/atlas/alerts.test.ts +++ b/tests/integration/tools/atlas/alerts.test.ts @@ -1,5 +1,5 @@ -import { expectDefined, getResponseElements } from "../../helpers.js"; -import { parseTable, describeWithAtlas, withProject } from "./atlasHelpers.js"; +import { expectDefined, getResponseContent } from "../../helpers.js"; +import { describeWithAtlas, withProject } from "./atlasHelpers.js"; import { expect, it } from "vitest"; describeWithAtlas("atlas-list-alerts", (integration) => { @@ -13,26 +13,20 @@ describeWithAtlas("atlas-list-alerts", (integration) => { }); withProject(integration, ({ getProjectId }) => { - it("returns alerts in table format", async () => { + it("returns alerts in JSON format", async () => { const response = await integration.mcpClient().callTool({ name: "atlas-list-alerts", arguments: { projectId: getProjectId() }, }); - const elements = getResponseElements(response.content); - expect(elements).toHaveLength(1); - - const data = parseTable(elements[0]?.text ?? ""); - - // Since we can't guarantee alerts will exist, we just verify the table structure - if (data.length > 0) { - const alert = data[0]; - expect(alert).toHaveProperty("Alert ID"); - expect(alert).toHaveProperty("Status"); - expect(alert).toHaveProperty("Created"); - expect(alert).toHaveProperty("Updated"); - expect(alert).toHaveProperty("Type"); - expect(alert).toHaveProperty("Comment"); + const content = getResponseContent(response.content); + // check that there are alerts or no alerts + if (content.includes("Found alerts in project")) { + expect(content).toContain("[] { - const data = text - .split("\n") - .filter((line) => line.trim() !== "") - .map((line) => line.split("|").map((cell) => cell.trim())); - - const headers = data[0]; - return data - .filter((_, index) => index >= 2) - .map((cells) => { - const row: Record = {}; - cells.forEach((cell, index) => { - if (headers) { - row[headers[index] ?? ""] = cell; - } - }); - return row; - }); -} - export const randomId = new ObjectId().toString(); async function createProject(apiClient: ApiClient): Promise>> { diff --git a/tests/integration/tools/atlas/clusters.test.ts b/tests/integration/tools/atlas/clusters.test.ts index 30f15bb9..e9b0425a 100644 --- a/tests/integration/tools/atlas/clusters.test.ts +++ b/tests/integration/tools/atlas/clusters.test.ts @@ -1,14 +1,6 @@ import type { Session } from "../../../../src/common/session.js"; -import { expectDefined, getDataFromUntrustedContent, getResponseElements } from "../../helpers.js"; -import { - describeWithAtlas, - withProject, - randomId, - parseTable, - deleteCluster, - waitCluster, - sleep, -} from "./atlasHelpers.js"; +import { expectDefined, getResponseContent } from "../../helpers.js"; +import { describeWithAtlas, withProject, randomId, deleteCluster, waitCluster, sleep } from "./atlasHelpers.js"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; describeWithAtlas("clusters", (integration) => { @@ -48,9 +40,11 @@ describeWithAtlas("clusters", (integration) => { region: "US_EAST_1", }, }); - const elements = getResponseElements(response.content); - expect(elements).toHaveLength(2); - expect(elements[0]?.text).toContain("has been created"); + const content = getResponseContent(response.content); + expect(content).toContain("Cluster"); + expect(content).toContain(clusterName); + expect(content).toContain("has been created"); + expect(content).toContain("US_EAST_1"); // Check that the current IP is present in the access list const accessList = await session.apiClient.listProjectIpAccessLists({ @@ -80,11 +74,10 @@ describeWithAtlas("clusters", (integration) => { name: "atlas-inspect-cluster", arguments: { projectId, clusterName: clusterName }, }); - const elements = getResponseElements(response.content); - expect(elements).toHaveLength(2); - expect(elements[0]?.text).toContain("Cluster details:"); - expect(elements[1]?.text).toContain(" { .mcpClient() .callTool({ name: "atlas-list-clusters", arguments: { projectId } }); - const elements = getResponseElements(response); - expect(elements).toHaveLength(2); - - expect(elements[1]?.text).toContain(" { expectDefined(connectCluster.inputSchema.properties); expect(connectCluster.inputSchema.properties).toHaveProperty("projectId"); expect(connectCluster.inputSchema.properties).toHaveProperty("clusterName"); - expect(connectCluster.inputSchema.properties).toHaveProperty("connectionType"); }); it("connects to cluster", async () => { @@ -164,23 +152,20 @@ describeWithAtlas("clusters", (integration) => { arguments: { projectId, clusterName, connectionType }, }); - const elements = getResponseElements(response.content); - expect(elements.length).toBeGreaterThanOrEqual(1); - if (elements[0]?.text.includes(`Connected to cluster "${clusterName}"`)) { + const content = getResponseContent(response.content); + expect(content).toContain("Connected to cluster"); + expect(content).toContain(clusterName); + if (content.includes(`Connected to cluster "${clusterName}"`)) { connected = true; // assert that some of the element s have the message - expect( - elements.some((element) => - element.text.includes( - "Note: A temporary user has been created to enable secure connection to the cluster. For more information, see https://dochub.mongodb.org/core/mongodb-mcp-server-tools-considerations" - ) - ) - ).toBe(true); + expect(content).toContain( + "Note: A temporary user has been created to enable secure connection to the cluster. For more information, see https://dochub.mongodb.org/core/mongodb-mcp-server-tools-considerations" + ); break; } else { - expect(elements[0]?.text).toContain(`Attempting to connect to cluster "${clusterName}"...`); + expect(content).toContain(`Attempting to connect to cluster "${clusterName}"...`); } await sleep(500); } @@ -193,19 +178,18 @@ describeWithAtlas("clusters", (integration) => { name: "find", arguments: { database: "some-db", collection: "some-collection" }, }); - const elements = getResponseElements(response.content); - expect(elements).toHaveLength(2); - expect(elements[0]?.text).toContain( + const content = getResponseContent(response.content); + expect(content).toContain( "You need to connect to a MongoDB instance before you can access its data." ); // Check if the response contains all available test tools. if (process.platform === "darwin" && process.env.GITHUB_ACTIONS === "true") { // The tool atlas-local-connect-deployment may be disabled in some test environments if Docker is not available. - expect(elements[1]?.text).toContain( + expect(content).toContain( 'Please use one of the following tools: "atlas-connect-cluster", "connect" to connect to a MongoDB instance' ); } else { - expect(elements[1]?.text).toContain( + expect(content).toContain( 'Please use one of the following tools: "atlas-connect-cluster", "atlas-local-connect-deployment", "connect" to connect to a MongoDB instance' ); } diff --git a/tests/integration/tools/atlas/orgs.test.ts b/tests/integration/tools/atlas/orgs.test.ts index baa4f96a..e745c0a7 100644 --- a/tests/integration/tools/atlas/orgs.test.ts +++ b/tests/integration/tools/atlas/orgs.test.ts @@ -1,5 +1,5 @@ -import { expectDefined, getDataFromUntrustedContent, getResponseElements } from "../../helpers.js"; -import { parseTable, describeWithAtlas, withCredentials } from "./atlasHelpers.js"; +import { expectDefined, getResponseContent } from "../../helpers.js"; +import { describeWithAtlas, withCredentials } from "./atlasHelpers.js"; import { describe, expect, it } from "vitest"; describeWithAtlas("orgs", (integration) => { @@ -13,12 +13,10 @@ describeWithAtlas("orgs", (integration) => { it("returns org names", async () => { const response = await integration.mcpClient().callTool({ name: "atlas-list-orgs", arguments: {} }); - const elements = getResponseElements(response); - expect(elements[0]?.text).toContain("Found 1 organizations"); - expect(elements[1]?.text).toContain("