From e3da69b0d7b6841b270b33b0040abefd3449901e Mon Sep 17 00:00:00 2001 From: Melanija Cvetic Date: Tue, 21 Oct 2025 11:04:25 +0100 Subject: [PATCH 1/7] MCP-259: support atlas connect via private and private endpoint connection strings --- src/common/atlas/cluster.ts | 37 ++++++++++++++++--- src/tools/args.ts | 3 ++ src/tools/atlas/connect/connectCluster.ts | 45 +++++++++++++++++++---- src/tools/atlas/read/inspectCluster.ts | 2 +- src/tools/atlas/read/listClusters.ts | 2 +- 5 files changed, 73 insertions(+), 16 deletions(-) diff --git a/src/common/atlas/cluster.ts b/src/common/atlas/cluster.ts index 1ea30286b..aa61fa82c 100644 --- a/src/common/atlas/cluster.ts +++ b/src/common/atlas/cluster.ts @@ -1,4 +1,8 @@ -import type { ClusterDescription20240805, FlexClusterDescription20241113 } from "./openapi.js"; +import type { + ClusterConnectionStrings, + ClusterDescription20240805, + FlexClusterDescription20241113, +} from "./openapi.js"; import type { ApiClient } from "./apiClient.js"; import { LogId } from "../logger.js"; import { ConnectionString } from "mongodb-connection-string-url"; @@ -18,19 +22,18 @@ export interface Cluster { instanceSize?: string; state?: "IDLE" | "CREATING" | "UPDATING" | "DELETING" | "REPAIRING"; mongoDBVersion?: string; - connectionString?: string; + connectionStrings?: ClusterConnectionStrings; processIds?: Array; } export function formatFlexCluster(cluster: FlexClusterDescription20241113): Cluster { - const connectionString = cluster.connectionStrings?.standardSrv || cluster.connectionStrings?.standard; return { name: cluster.name, instanceType: "FLEX", instanceSize: undefined, state: cluster.stateName, mongoDBVersion: cluster.mongoDBVersion, - connectionString, + connectionStrings: cluster.connectionStrings, processIds: extractProcessIds(cluster.connectionStrings?.standard ?? ""), }; } @@ -65,7 +68,6 @@ export function formatCluster(cluster: ClusterDescription20240805): Cluster { const instanceSize = regionConfigs[0]?.instanceSize ?? "UNKNOWN"; const clusterInstanceType = instanceSize === "M0" ? "FREE" : "DEDICATED"; - const connectionString = cluster.connectionStrings?.standardSrv || cluster.connectionStrings?.standard; return { name: cluster.name, @@ -73,7 +75,7 @@ export function formatCluster(cluster: ClusterDescription20240805): Cluster { instanceSize: clusterInstanceType === "DEDICATED" ? instanceSize : undefined, state: cluster.stateName, mongoDBVersion: cluster.mongoDBVersion, - connectionString, + connectionStrings: cluster.connectionStrings, processIds: extractProcessIds(cluster.connectionStrings?.standard ?? ""), }; } @@ -112,6 +114,29 @@ export async function inspectCluster(apiClient: ApiClient, projectId: string, cl } } +// getConnectionString returns a connection string given a connectionType. +// For "privateEndpoint", it returns the first private endpoint connection string available. +export function getConnectionString( + connectionStrings: ClusterConnectionStrings, + connectionType: "standard" | "private" | "privateEndpoint" +): string | undefined { + if (connectionStrings === undefined) { + return undefined; + } + + switch (connectionType) { + case "standard": + return connectionStrings.standardSrv || connectionStrings.standard; + case "private": + return connectionStrings.privateSrv || connectionStrings.private; + case "privateEndpoint": + return ( + connectionStrings.privateEndpoint?.[0]?.srvConnectionString || + connectionStrings.privateEndpoint?.[0]?.connectionString + ); + } +} + export async function getProcessIdsFromCluster( apiClient: ApiClient, projectId: string, diff --git a/src/tools/args.ts b/src/tools/args.ts index 653f72da2..11b5b8b80 100644 --- a/src/tools/args.ts +++ b/src/tools/args.ts @@ -41,6 +41,9 @@ export const AtlasArgs = { .max(64, "Cluster name must be 64 characters or less") .regex(ALLOWED_CLUSTER_NAME_CHARACTERS_REGEX, ALLOWED_CLUSTER_NAME_CHARACTERS_ERROR), + connectionType: (): z.ZodDefault> => + z.enum(["standard", "private", "privateEndpoint"]).default("standard"), + projectName: (): z.ZodString => z .string() diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index 54f3ae8bd..12c18ae8a 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -3,7 +3,7 @@ import { type OperationType, type ToolArgs } from "../../tool.js"; import { AtlasToolBase } from "../atlasTool.js"; import { generateSecurePassword } from "../../../helpers/generatePassword.js"; import { LogId } from "../../../common/logger.js"; -import { inspectCluster } from "../../../common/atlas/cluster.js"; +import { getConnectionString, inspectCluster } from "../../../common/atlas/cluster.js"; import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUtils.js"; import type { AtlasClusterConnectionInfo } from "../../../common/connectionManager.js"; import { getDefaultRoleFromConfig } from "../../../common/atlas/roles.js"; @@ -22,6 +22,9 @@ function sleep(ms: number): Promise { export const ConnectClusterArgs = { projectId: AtlasArgs.projectId().describe("Atlas project ID"), clusterName: AtlasArgs.clusterName().describe("Atlas cluster name"), + connectionType: AtlasArgs.connectionType() + .optional() + .describe("Desired connection type (standard, private, or privateEndpoint) to an Atlas cluster"), }; export class ConnectClusterTool extends AtlasToolBase { @@ -69,12 +72,17 @@ export class ConnectClusterTool extends AtlasToolBase { private async prepareClusterConnection( projectId: string, - clusterName: string + clusterName: string, + connectionType: "standard" | "private" | "privateEndpoint" | undefined = "standard" ): Promise<{ connectionString: string; atlas: AtlasClusterConnectionInfo }> { const cluster = await inspectCluster(this.session.apiClient, projectId, clusterName); - if (!cluster.connectionString) { - throw new Error("Connection string not available"); + if (cluster.connectionStrings === undefined) { + throw new Error("Connection strings not available"); + } + const connectionString = getConnectionString(cluster.connectionStrings, connectionType); + if (connectionString === undefined) { + throw new Error(`Connection string for type "${connectionType}" not available`); } const username = `mcpUser${Math.floor(Math.random() * 100000)}`; @@ -113,13 +121,26 @@ export class ConnectClusterTool extends AtlasToolBase { expiryDate, }; - const cn = new URL(cluster.connectionString); + this.session.logger.debug({ + id: LogId.atlasConnectFailure, + context: "atlas-connect-cluster", + message: `Connection string received: ${connectionString}`, + }); + const cn = new URL(connectionString); cn.username = username; cn.password = password; - cn.searchParams.set("authSource", "admin"); + if (connectionType !== "privateEndpoint") { + cn.searchParams.set("authSource", "admin"); + } this.session.keychain.register(username, "user"); this.session.keychain.register(password, "password"); + const thing = cn.toString(); + this.session.logger.debug({ + id: LogId.atlasConnectFailure, + context: "atlas-connect-cluster", + message: `>>>>>> Connection string used: ${thing}`, + }); return { connectionString: cn.toString(), atlas: connectedAtlasCluster }; } @@ -200,7 +221,11 @@ export class ConnectClusterTool extends AtlasToolBase { }); } - protected async execute({ projectId, clusterName }: ToolArgs): Promise { + protected async execute({ + projectId, + clusterName, + connectionType, + }: ToolArgs): Promise { const ipAccessListUpdated = await ensureCurrentIpInAccessList(this.session.apiClient, projectId); let createdUser = false; @@ -239,7 +264,11 @@ export class ConnectClusterTool extends AtlasToolBase { case "disconnected": default: { await this.session.disconnect(); - const { connectionString, atlas } = await this.prepareClusterConnection(projectId, clusterName); + const { connectionString, atlas } = await this.prepareClusterConnection( + projectId, + clusterName, + connectionType + ); createdUser = true; // try to connect for about 5 minutes asynchronously diff --git a/src/tools/atlas/read/inspectCluster.ts b/src/tools/atlas/read/inspectCluster.ts index 56e1e5a8b..d4defcc92 100644 --- a/src/tools/atlas/read/inspectCluster.ts +++ b/src/tools/atlas/read/inspectCluster.ts @@ -30,7 +30,7 @@ export class InspectClusterTool extends AtlasToolBase { "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.connectionString || "N/A"}` +${formattedCluster.name || "Unknown"} | ${formattedCluster.instanceType} | ${formattedCluster.instanceSize || "N/A"} | ${formattedCluster.state || "UNKNOWN"} | ${formattedCluster.mongoDBVersion || "N/A"} | ${formattedCluster.connectionStrings?.standardSrv || formattedCluster.connectionStrings?.standard || "N/A"}` ), }; } diff --git a/src/tools/atlas/read/listClusters.ts b/src/tools/atlas/read/listClusters.ts index 60344f7d3..1dfe626ea 100644 --- a/src/tools/atlas/read/listClusters.ts +++ b/src/tools/atlas/read/listClusters.ts @@ -105,7 +105,7 @@ ${rows}`, ----------------|----------------|----------------|----------------|----------------|---------------- ${allClusters .map((formattedCluster) => { - return `${formattedCluster.name || "Unknown"} | ${formattedCluster.instanceType} | ${formattedCluster.instanceSize || "N/A"} | ${formattedCluster.state || "UNKNOWN"} | ${formattedCluster.mongoDBVersion || "N/A"} | ${formattedCluster.connectionString || "N/A"}`; + 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")}` ), From 80b353f1835d44c63c210028864a5735b71ac5e7 Mon Sep 17 00:00:00 2001 From: Melanija Cvetic Date: Thu, 23 Oct 2025 09:31:28 +0100 Subject: [PATCH 2/7] removes comments --- src/tools/atlas/connect/connectCluster.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index 12c18ae8a..a1d5ee3ba 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -121,26 +121,13 @@ export class ConnectClusterTool extends AtlasToolBase { expiryDate, }; - this.session.logger.debug({ - id: LogId.atlasConnectFailure, - context: "atlas-connect-cluster", - message: `Connection string received: ${connectionString}`, - }); const cn = new URL(connectionString); cn.username = username; cn.password = password; - if (connectionType !== "privateEndpoint") { - cn.searchParams.set("authSource", "admin"); - } + cn.searchParams.set("authSource", "admin"); this.session.keychain.register(username, "user"); this.session.keychain.register(password, "password"); - const thing = cn.toString(); - this.session.logger.debug({ - id: LogId.atlasConnectFailure, - context: "atlas-connect-cluster", - message: `>>>>>> Connection string used: ${thing}`, - }); return { connectionString: cn.toString(), atlas: connectedAtlasCluster }; } From f7b4ec429491b2c2ab5e7ce250a0fcc365828aed Mon Sep 17 00:00:00 2001 From: Melanija Cvetic Date: Thu, 23 Oct 2025 09:35:46 +0100 Subject: [PATCH 3/7] improves error msgs --- src/tools/atlas/connect/connectCluster.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index a1d5ee3ba..daf5b0f5a 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -24,7 +24,7 @@ export const ConnectClusterArgs = { clusterName: AtlasArgs.clusterName().describe("Atlas cluster name"), connectionType: AtlasArgs.connectionType() .optional() - .describe("Desired connection type (standard, private, or privateEndpoint) to an Atlas cluster"), + .describe("Type of connection (standard, private, or privateEndpoint) to an Atlas cluster"), }; export class ConnectClusterTool extends AtlasToolBase { @@ -82,7 +82,7 @@ export class ConnectClusterTool extends AtlasToolBase { } const connectionString = getConnectionString(cluster.connectionStrings, connectionType); if (connectionString === undefined) { - throw new Error(`Connection string for type "${connectionType}" not available`); + throw new Error(`Connection string for connection type "${connectionType}" not available`); } const username = `mcpUser${Math.floor(Math.random() * 100000)}`; From 933e18ad5f813a983cee1fbd0b728305b42c48a8 Mon Sep 17 00:00:00 2001 From: Melanija Cvetic Date: Thu, 23 Oct 2025 09:46:57 +0100 Subject: [PATCH 4/7] adjusts integ test --- tests/integration/tools/atlas/clusters.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/tools/atlas/clusters.test.ts b/tests/integration/tools/atlas/clusters.test.ts index f340dc08f..30f15bb96 100644 --- a/tests/integration/tools/atlas/clusters.test.ts +++ b/tests/integration/tools/atlas/clusters.test.ts @@ -150,16 +150,18 @@ describeWithAtlas("clusters", (integration) => { 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 () => { const projectId = getProjectId(); + const connectionType = "standard"; let connected = false; for (let i = 0; i < 10; i++) { const response = await integration.mcpClient().callTool({ name: "atlas-connect-cluster", - arguments: { projectId, clusterName }, + arguments: { projectId, clusterName, connectionType }, }); const elements = getResponseElements(response.content); From a18cbdb5b41f9a3f542b79bb4dc55c39d0f3ffae Mon Sep 17 00:00:00 2001 From: Melanija Cvetic Date: Thu, 23 Oct 2025 10:00:27 +0100 Subject: [PATCH 5/7] Addresses copilot review --- src/common/atlas/cluster.ts | 10 ++++------ src/tools/atlas/connect/connectCluster.ts | 6 +++--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/common/atlas/cluster.ts b/src/common/atlas/cluster.ts index aa61fa82c..a153e7fea 100644 --- a/src/common/atlas/cluster.ts +++ b/src/common/atlas/cluster.ts @@ -114,16 +114,14 @@ export async function inspectCluster(apiClient: ApiClient, projectId: string, cl } } -// getConnectionString returns a connection string given a connectionType. -// For "privateEndpoint", it returns the first private endpoint connection string available. +/** + * Returns a connection string for the specified connectionType. + * For "privateEndpoint", it returns the first private endpoint connection string available. + */ export function getConnectionString( connectionStrings: ClusterConnectionStrings, connectionType: "standard" | "private" | "privateEndpoint" ): string | undefined { - if (connectionStrings === undefined) { - return undefined; - } - switch (connectionType) { case "standard": return connectionStrings.standardSrv || connectionStrings.standard; diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index daf5b0f5a..ff79e8d14 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -22,9 +22,9 @@ function sleep(ms: number): Promise { export const ConnectClusterArgs = { projectId: AtlasArgs.projectId().describe("Atlas project ID"), clusterName: AtlasArgs.clusterName().describe("Atlas cluster name"), - connectionType: AtlasArgs.connectionType() - .optional() - .describe("Type of connection (standard, private, or privateEndpoint) to an Atlas cluster"), + connectionType: AtlasArgs.connectionType().describe( + "Type of connection (standard, private, or privateEndpoint) to an Atlas cluster" + ), }; export class ConnectClusterTool extends AtlasToolBase { From f37c2e4343311d9eebc71e93f34125a071ace15d Mon Sep 17 00:00:00 2001 From: Melanija Cvetic Date: Thu, 23 Oct 2025 10:38:50 +0100 Subject: [PATCH 6/7] addresses pr comments --- src/tools/atlas/connect/connectCluster.ts | 4 +++- src/tools/atlas/read/inspectCluster.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index ff79e8d14..3ba519fc8 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -82,7 +82,9 @@ export class ConnectClusterTool extends AtlasToolBase { } const connectionString = getConnectionString(cluster.connectionStrings, connectionType); if (connectionString === undefined) { - throw new Error(`Connection string for connection type "${connectionType}" not available`); + throw new Error( + `Connection string for connection type "${connectionType}" is not available. Please ensure this connection type is set up in Atlas. See https://www.mongodb.com/docs/atlas/connect-to-database-deployment/#connect-to-an-atlas-cluster.` + ); } const username = `mcpUser${Math.floor(Math.random() * 100000)}`; diff --git a/src/tools/atlas/read/inspectCluster.ts b/src/tools/atlas/read/inspectCluster.ts index d4defcc92..0eb70cee1 100644 --- a/src/tools/atlas/read/inspectCluster.ts +++ b/src/tools/atlas/read/inspectCluster.ts @@ -28,7 +28,7 @@ export class InspectClusterTool extends AtlasToolBase { return { content: formatUntrustedData( "Cluster details:", - `Cluster Name | Cluster Type | Tier | State | MongoDB Version | Connection String + `Cluster Name | Cluster Type | Tier | State | MongoDB Version | Standard 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"}` ), From a9d6526c1342b239e666c8293dfbe73114829534 Mon Sep 17 00:00:00 2001 From: Melanija Cvetic Date: Thu, 23 Oct 2025 10:45:42 +0100 Subject: [PATCH 7/7] reverting inspect cluster table change --- src/tools/atlas/read/inspectCluster.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/atlas/read/inspectCluster.ts b/src/tools/atlas/read/inspectCluster.ts index 0eb70cee1..d4defcc92 100644 --- a/src/tools/atlas/read/inspectCluster.ts +++ b/src/tools/atlas/read/inspectCluster.ts @@ -28,7 +28,7 @@ export class InspectClusterTool extends AtlasToolBase { return { content: formatUntrustedData( "Cluster details:", - `Cluster Name | Cluster Type | Tier | State | MongoDB Version | Standard Connection String + `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"}` ),