From 3977aa775a768ab3c39ea91433aa86c33e2d17c3 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Mon, 4 Aug 2025 14:51:53 +0200 Subject: [PATCH 01/26] chore: refactor to include the new ConnectionManager Now the connection itself and the user flow is managed by the new connection manager. There are tests missing for this class, that will be added in a following commit. --- src/common/connectionManager.ts | 179 ++++++++++++++++++ src/common/session.ts | 83 ++++---- src/resources/common/debug.ts | 6 +- src/server.ts | 10 +- src/tools/atlas/connect/connectCluster.ts | 6 +- src/tools/mongodb/connect/connect.ts | 8 +- src/tools/mongodb/mongodbTool.ts | 9 +- tests/integration/helpers.ts | 4 + .../tools/mongodb/connect/connect.test.ts | 7 +- tests/integration/transports/stdio.test.ts | 13 +- tests/unit/common/session.test.ts | 7 +- 11 files changed, 269 insertions(+), 63 deletions(-) create mode 100644 src/common/connectionManager.ts diff --git a/src/common/connectionManager.ts b/src/common/connectionManager.ts new file mode 100644 index 000000000..7a66d4fd9 --- /dev/null +++ b/src/common/connectionManager.ts @@ -0,0 +1,179 @@ +import { ConnectOptions } from "./config.js"; +import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver"; +import EventEmitter from "events"; +import { setAppNameParamIfMissing } from "../helpers/connectionOptions.js"; +import { packageInfo } from "./packageInfo.js"; +import ConnectionString from "mongodb-connection-string-url"; +import { MongoClientOptions } from "mongodb"; +import { ErrorCodes, MongoDBError } from "./errors.js"; + +export interface AtlasClusterConnectionInfo { + username: string; + projectId: string; + clusterName: string; + expiryDate: Date; +} + +export interface ConnectionSettings extends ConnectOptions { + connectionString: string; + atlas?: AtlasClusterConnectionInfo; +} + +type ConnectionTag = "connected" | "connecting" | "disconnected" | "errored"; +type OIDCConnectionAuthType = "oidc-auth-flow" | "oidc-device-flow"; +type ConnectionStringAuthType = "scram" | "ldap" | "kerberos" | OIDCConnectionAuthType | "x.509"; + +export interface ConnectionState { + tag: ConnectionTag; + connectionStringAuthType?: ConnectionStringAuthType; +} + +export interface ConnectionStateConnected extends ConnectionState { + tag: "connected"; + serviceProvider: NodeDriverServiceProvider; + connectedAtlasCluster?: AtlasClusterConnectionInfo; +} + +export interface ConnectionStateConnecting extends ConnectionState { + tag: "connecting"; + serviceProvider: NodeDriverServiceProvider; + oidcConnectionType: OIDCConnectionAuthType; + oidcLoginUrl?: string; + oidcUserCode?: string; +} + +export interface ConnectionStateDisconnected extends ConnectionState { + tag: "disconnected"; +} + +export interface ConnectionStateErrored extends ConnectionState { + tag: "errored"; + errorReason: string; +} + +export type AnyConnectionState = + | ConnectionState + | ConnectionStateConnected + | ConnectionStateConnecting + | ConnectionStateDisconnected + | ConnectionStateErrored; + +export interface ConnectionManagerEvents { + "connection-requested": [AnyConnectionState]; + "connection-succeeded": [ConnectionStateConnected]; + "connection-timed-out": [ConnectionStateErrored]; + "connection-closed": [ConnectionStateDisconnected]; + "connection-errored": [ConnectionStateErrored]; +} + +export class ConnectionManager extends EventEmitter { + private state: AnyConnectionState; + + constructor() { + super(); + this.state = { tag: "disconnected" }; + } + + async connect(settings: ConnectionSettings): Promise { + if (this.state.tag == "connected" || this.state.tag == "connecting") { + await this.disconnect(); + } + + let serviceProvider: NodeDriverServiceProvider; + try { + settings = { ...settings }; + settings.connectionString = setAppNameParamIfMissing({ + connectionString: settings.connectionString, + defaultAppName: `${packageInfo.mcpServerName} ${packageInfo.version}`, + }); + + serviceProvider = await NodeDriverServiceProvider.connect(settings.connectionString, { + productDocsLink: "https://github.com/mongodb-js/mongodb-mcp-server/", + productName: "MongoDB MCP", + readConcern: { + level: settings.readConcern, + }, + readPreference: settings.readPreference, + writeConcern: { + w: settings.writeConcern, + }, + timeoutMS: settings.timeoutMS, + proxy: { useEnvironmentVariableProxies: true }, + applyProxyToOIDC: true, + }); + } catch (error: unknown) { + const errorReason = error instanceof Error ? error.message : `${error as string}`; + this.changeState("connection-errored", { tag: "errored", errorReason }); + throw new MongoDBError(ErrorCodes.MisconfiguredConnectionString, errorReason); + } + + try { + await serviceProvider?.runCommand?.("admin", { hello: 1 }); + + return this.changeState("connection-succeeded", { + tag: "connected", + connectedAtlasCluster: settings.atlas, + serviceProvider, + connectionStringAuthType: this.inferConnectionTypeFromSettings(settings), + }); + } catch (error: unknown) { + const errorReason = error instanceof Error ? error.message : `${error as string}`; + this.changeState("connection-errored", { tag: "errored", errorReason }); + throw new MongoDBError(ErrorCodes.NotConnectedToMongoDB, errorReason); + } + } + + async disconnect(): Promise { + if (this.state.tag == "disconnected") { + return this.state as ConnectionStateDisconnected; + } + + if (this.state.tag == "errored") { + return this.state as ConnectionStateErrored; + } + + if (this.state.tag == "connected" || this.state.tag == "connecting") { + const state = this.state as ConnectionStateConnecting | ConnectionStateConnected; + try { + await state.serviceProvider?.close(true); + } finally { + this.changeState("connection-closed", { tag: "disconnected" }); + } + } + + return { tag: "disconnected" }; + } + + get currentConnectionState(): AnyConnectionState { + return this.state; + } + + changeState(event: keyof ConnectionManagerEvents, newState: State): State { + this.state = newState; + this.emit(event, newState); + return newState; + } + + private inferConnectionTypeFromSettings(settings: ConnectionSettings): ConnectionStringAuthType { + const connString = new ConnectionString(settings.connectionString); + const searchParams = connString.typedSearchParams(); + + switch (searchParams.get("authMechanism")) { + case "MONGODB-OIDC": { + return "oidc-auth-flow"; // TODO: depending on if we don't have a --browser later it can be oidc-device-flow + } + case "MONGODB-X509": + return "x.509"; + case "GSSAPI": + return "kerberos"; + case "PLAIN": + if (searchParams.get("authSource") == "$external") { + return "ldap"; + } + break; + default: + return "scram"; + } + return "scram"; + } +} diff --git a/src/common/session.ts b/src/common/session.ts index 2a75af337..b8f066756 100644 --- a/src/common/session.ts +++ b/src/common/session.ts @@ -1,16 +1,21 @@ -import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver"; import { ApiClient, ApiClientCredentials } from "./atlas/apiClient.js"; import { Implementation } from "@modelcontextprotocol/sdk/types.js"; import logger, { LogId } from "./logger.js"; import EventEmitter from "events"; -import { ConnectOptions } from "./config.js"; -import { setAppNameParamIfMissing } from "../helpers/connectionOptions.js"; -import { packageInfo } from "./packageInfo.js"; +import { + AnyConnectionState, + ConnectionManager, + ConnectionSettings, + ConnectionStateConnected, +} from "./connectionManager.js"; +import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver"; +import { ErrorCodes, MongoDBError } from "./errors.js"; export interface SessionOptions { apiBaseUrl: string; apiClientId?: string; apiClientSecret?: string; + connectionManager?: ConnectionManager; } export type SessionEvents = { @@ -22,7 +27,7 @@ export type SessionEvents = { export class Session extends EventEmitter { sessionId?: string; - serviceProvider?: NodeDriverServiceProvider; + connectionManager: ConnectionManager; apiClient: ApiClient; agentRunner?: { name: string; @@ -35,7 +40,7 @@ export class Session extends EventEmitter { expiryDate: Date; }; - constructor({ apiBaseUrl, apiClientId, apiClientSecret }: SessionOptions) { + constructor({ apiBaseUrl, apiClientId, apiClientSecret, connectionManager }: SessionOptions) { super(); const credentials: ApiClientCredentials | undefined = @@ -46,10 +51,13 @@ export class Session extends EventEmitter { } : undefined; - this.apiClient = new ApiClient({ - baseUrl: apiBaseUrl, - credentials, - }); + this.apiClient = new ApiClient({ baseUrl: apiBaseUrl, credentials }); + + this.connectionManager = connectionManager ?? new ConnectionManager(); + this.connectionManager.on("connection-succeeded", () => this.emit("connect")); + this.connectionManager.on("connection-timed-out", (error) => this.emit("connection-error", error.errorReason)); + this.connectionManager.on("connection-closed", () => this.emit("disconnect")); + this.connectionManager.on("connection-errored", (error) => this.emit("connection-error", error.errorReason)); } setAgentRunner(agentRunner: Implementation | undefined) { @@ -62,15 +70,13 @@ export class Session extends EventEmitter { } async disconnect(): Promise { - if (this.serviceProvider) { - try { - await this.serviceProvider.close(true); - } catch (err: unknown) { - const error = err instanceof Error ? err : new Error(String(err)); - logger.error(LogId.mongodbDisconnectFailure, "Error closing service provider:", error.message); - } - this.serviceProvider = undefined; + try { + await this.connectionManager.disconnect(); + } catch (err: unknown) { + const error = err instanceof Error ? err : new Error(String(err)); + logger.error(LogId.mongodbDisconnectFailure, "Error closing service provider:", error.message); } + if (this.connectedAtlasCluster?.username && this.connectedAtlasCluster?.projectId) { void this.apiClient .deleteDatabaseUser({ @@ -92,44 +98,33 @@ export class Session extends EventEmitter { }); this.connectedAtlasCluster = undefined; } - this.emit("disconnect"); } async close(): Promise { await this.disconnect(); await this.apiClient.close(); - this.emit("close"); } - async connectToMongoDB(connectionString: string, connectOptions: ConnectOptions): Promise { - connectionString = setAppNameParamIfMissing({ - connectionString, - defaultAppName: `${packageInfo.mcpServerName} ${packageInfo.version}`, - }); - + async connectToMongoDB(settings: ConnectionSettings): Promise { try { - this.serviceProvider = await NodeDriverServiceProvider.connect(connectionString, { - productDocsLink: "https://github.com/mongodb-js/mongodb-mcp-server/", - productName: "MongoDB MCP", - readConcern: { - level: connectOptions.readConcern, - }, - readPreference: connectOptions.readPreference, - writeConcern: { - w: connectOptions.writeConcern, - }, - timeoutMS: connectOptions.timeoutMS, - proxy: { useEnvironmentVariableProxies: true }, - applyProxyToOIDC: true, - }); - - await this.serviceProvider?.runCommand?.("admin", { hello: 1 }); + return await this.connectionManager.connect({ ...settings }); } catch (error: unknown) { - const message = error instanceof Error ? error.message : `${error as string}`; + const message = error instanceof Error ? error.message : (error as string); this.emit("connection-error", message); throw error; } + } + + isConnectedToMongoDB(): boolean { + return this.connectionManager.currentConnectionState.tag == "connected"; + } + + get serviceProvider(): NodeDriverServiceProvider { + if (this.isConnectedToMongoDB()) { + const state = this.connectionManager.currentConnectionState as ConnectionStateConnected; + return state.serviceProvider; + } - this.emit("connect"); + throw new MongoDBError(ErrorCodes.NotConnectedToMongoDB, "Not connected to MongoDB"); } } diff --git a/src/resources/common/debug.ts b/src/resources/common/debug.ts index c8de2dd05..60fcda070 100644 --- a/src/resources/common/debug.ts +++ b/src/resources/common/debug.ts @@ -10,10 +10,10 @@ type ConnectionStateDebuggingInformation = { export class DebugResource extends ReactiveResource( { - name: "debug-mongodb-connectivity", - uri: "debug://mongodb-connectivity", + name: "debug-mongodb", + uri: "debug://mongodb", config: { - description: "Debugging information for connectivity issues.", + description: "Debugging information for MongoDB connectivity issues.", }, }, { diff --git a/src/server.ts b/src/server.ts index 1eccbdcdc..9f1a959aa 100644 --- a/src/server.ts +++ b/src/server.ts @@ -38,12 +38,15 @@ export class Server { } async connect(transport: Transport): Promise { + // Resources are now reactive, so we register them ASAP so they can listen to events like + // connection events. + this.registerResources(); await this.validateConfig(); this.mcpServer.server.registerCapabilities({ logging: {} }); + // TODO: Eventually we might want to make tools reactive too instead of relying on custom logic. this.registerTools(); - this.registerResources(); // This is a workaround for an issue we've seen with some models, where they'll see that everything in the `arguments` // object is optional, and then not pass it at all. However, the MCP server expects the `arguments` object to be if @@ -194,7 +197,10 @@ export class Server { if (this.userConfig.connectionString) { try { - await this.session.connectToMongoDB(this.userConfig.connectionString, this.userConfig.connectOptions); + await this.session.connectToMongoDB({ + connectionString: this.userConfig.connectionString, + ...this.userConfig.connectOptions, + }); } catch (error) { console.error( "Failed to connect to MongoDB instance using the connection string from the config: ", diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index e83c30402..62e9f7391 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -27,7 +27,7 @@ export class ConnectClusterTool extends AtlasToolBase { clusterName: string ): Promise<"connected" | "disconnected" | "connecting" | "connected-to-other-cluster" | "unknown"> { if (!this.session.connectedAtlasCluster) { - if (this.session.serviceProvider) { + if (this.session.isConnectedToMongoDB()) { return "connected-to-other-cluster"; } return "disconnected"; @@ -40,7 +40,7 @@ export class ConnectClusterTool extends AtlasToolBase { return "connected-to-other-cluster"; } - if (!this.session.serviceProvider) { + if (!this.session.isConnectedToMongoDB()) { return "connecting"; } @@ -145,7 +145,7 @@ export class ConnectClusterTool extends AtlasToolBase { try { lastError = undefined; - await this.session.connectToMongoDB(connectionString, this.config.connectOptions); + await this.session.connectToMongoDB({ connectionString, ...this.config.connectOptions }); break; } catch (err: unknown) { const error = err instanceof Error ? err : new Error(String(err)); diff --git a/src/tools/mongodb/connect/connect.ts b/src/tools/mongodb/connect/connect.ts index c21006892..fd86c55c9 100644 --- a/src/tools/mongodb/connect/connect.ts +++ b/src/tools/mongodb/connect/connect.ts @@ -7,6 +7,7 @@ import { UserConfig } from "../../../common/config.js"; import { Telemetry } from "../../../telemetry/telemetry.js"; import { Session } from "../../../common/session.js"; import { Server } from "../../../server.js"; +import logger from "../../../common/logger.js"; const disconnectedSchema = z .object({ @@ -46,6 +47,10 @@ export class ConnectTool extends MongoDBToolBase { constructor(session: Session, config: UserConfig, telemetry: Telemetry) { super(session, config, telemetry); + session.on("connect", () => { + this.updateMetadata(); + }); + session.on("disconnect", () => { this.updateMetadata(); }); @@ -67,6 +72,7 @@ export class ConnectTool extends MongoDBToolBase { await this.connectToMongoDB(connectionString); this.updateMetadata(); + return { content: [{ type: "text", text: "Successfully connected to MongoDB." }], }; @@ -82,7 +88,7 @@ export class ConnectTool extends MongoDBToolBase { } private updateMetadata(): void { - if (this.config.connectionString || this.session.serviceProvider) { + if (this.session.isConnectedToMongoDB()) { this.update?.({ name: connectedName, description: connectedDescription, diff --git a/src/tools/mongodb/mongodbTool.ts b/src/tools/mongodb/mongodbTool.ts index 83fc85aba..6fa09682c 100644 --- a/src/tools/mongodb/mongodbTool.ts +++ b/src/tools/mongodb/mongodbTool.ts @@ -5,6 +5,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { ErrorCodes, MongoDBError } from "../../common/errors.js"; import logger, { LogId } from "../../common/logger.js"; import { Server } from "../../server.js"; +import { AnyConnectionState } from "../../common/connectionManager.js"; export const DbOperationArgs = { database: z.string().describe("Database name"), @@ -16,7 +17,7 @@ export abstract class MongoDBToolBase extends ToolBase { public category: ToolCategory = "mongodb"; protected async ensureConnected(): Promise { - if (!this.session.serviceProvider) { + if (!this.session.isConnectedToMongoDB()) { if (this.session.connectedAtlasCluster) { throw new MongoDBError( ErrorCodes.NotConnectedToMongoDB, @@ -38,7 +39,7 @@ export abstract class MongoDBToolBase extends ToolBase { } } - if (!this.session.serviceProvider) { + if (!this.session.isConnectedToMongoDB()) { throw new MongoDBError(ErrorCodes.NotConnectedToMongoDB, "Not connected to MongoDB"); } @@ -116,8 +117,8 @@ export abstract class MongoDBToolBase extends ToolBase { return super.handleError(error, args); } - protected connectToMongoDB(connectionString: string): Promise { - return this.session.connectToMongoDB(connectionString, this.config.connectOptions); + protected connectToMongoDB(connectionString: string): Promise { + return this.session.connectToMongoDB({ connectionString, ...this.config.connectOptions }); } protected resolveTelemetryMetadata( diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 3a3b0525d..f5a6ab7f1 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -8,6 +8,7 @@ import { Session } from "../../src/common/session.js"; import { Telemetry } from "../../src/telemetry/telemetry.js"; import { config } from "../../src/common/config.js"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { ConnectionManager } from "../../src/common/connectionManager.js"; interface ParameterInfo { name: string; @@ -53,10 +54,13 @@ export function setupIntegrationTest(getUserConfig: () => UserConfig): Integrati } ); + const connectionManager = new ConnectionManager(); + const session = new Session({ apiBaseUrl: userConfig.apiBaseUrl, apiClientId: userConfig.apiClientId, apiClientSecret: userConfig.apiClientSecret, + connectionManager, }); // Mock hasValidAccessToken for tests diff --git a/tests/integration/tools/mongodb/connect/connect.test.ts b/tests/integration/tools/mongodb/connect/connect.test.ts index d8be8e5ad..b162fc13c 100644 --- a/tests/integration/tools/mongodb/connect/connect.test.ts +++ b/tests/integration/tools/mongodb/connect/connect.test.ts @@ -14,6 +14,9 @@ describeWithMongoDB( (integration) => { beforeEach(() => { integration.mcpServer().userConfig.connectionString = integration.connectionString(); + integration.mcpServer().session.connectionManager.changeState("connection-succeeded", { + tag: "connected", + }); }); validateToolMetadata( @@ -75,7 +78,7 @@ describeWithMongoDB( const content = getResponseContent(response.content); - expect(content).toContain("Error running switch-connection"); + expect(content).toContain("The configured connection string is not valid."); }); }); }, @@ -125,7 +128,7 @@ describeWithMongoDB( arguments: { connectionString: "mongodb://localhost:12345" }, }); const content = getResponseContent(response.content); - expect(content).toContain("Error running connect"); + expect(content).toContain("The configured connection string is not valid."); // Should not suggest using the config connection string (because we don't have one) expect(content).not.toContain("Your config lists a different connection string"); diff --git a/tests/integration/transports/stdio.test.ts b/tests/integration/transports/stdio.test.ts index 2bc03b5b7..b3b5683c2 100644 --- a/tests/integration/transports/stdio.test.ts +++ b/tests/integration/transports/stdio.test.ts @@ -1,8 +1,16 @@ -import { describe, expect, it, beforeAll, afterAll } from "vitest"; +import { describe, expect, beforeEach, it, beforeAll, afterAll } from "vitest"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { describeWithMongoDB } from "../tools/mongodb/mongodbHelpers.js"; + +describeWithMongoDB("StdioRunner", (integration) => { + beforeEach(() => { + integration.mcpServer().userConfig.connectionString = integration.connectionString(); + integration.mcpServer().session.connectionManager.changeState("connection-succeeded", { + tag: "connected", + }); + }); -describe("StdioRunner", () => { describe("client connects successfully", () => { let client: Client; let transport: StdioClientTransport; @@ -12,6 +20,7 @@ describe("StdioRunner", () => { args: ["dist/index.js"], env: { MDB_MCP_TRANSPORT: "stdio", + MDB_MCP_CONNECTION_STRING: integration.connectionString(), }, }); client = new Client({ diff --git a/tests/unit/common/session.test.ts b/tests/unit/common/session.test.ts index 73236c5f2..f96952fef 100644 --- a/tests/unit/common/session.test.ts +++ b/tests/unit/common/session.test.ts @@ -43,7 +43,10 @@ describe("Session", () => { for (const testCase of testCases) { it(`should update connection string for ${testCase.name}`, async () => { - await session.connectToMongoDB(testCase.connectionString, config.connectOptions); + await session.connectToMongoDB({ + connectionString: testCase.connectionString, + ...config.connectOptions, + }); expect(session.serviceProvider).toBeDefined(); const connectMock = MockNodeDriverServiceProvider.connect; @@ -58,7 +61,7 @@ describe("Session", () => { } it("should configure the proxy to use environment variables", async () => { - await session.connectToMongoDB("mongodb://localhost", config.connectOptions); + await session.connectToMongoDB({ connectionString: "mongodb://localhost", ...config.connectOptions }); expect(session.serviceProvider).toBeDefined(); const connectMock = MockNodeDriverServiceProvider.connect; From 7a1e21702be3a6f34dfc92d99027c8ddebc1f727 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Tue, 5 Aug 2025 13:21:41 +0200 Subject: [PATCH 02/26] chore: add tests to connection manager these test should validate that connecting to a cluster is fine and also the connection mechanism inference is working as expected until we support both oidc flows. --- src/common/connectionManager.ts | 6 +- .../common/connectionManager.test.ts | 151 ++++++++++++++++++ 2 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 tests/integration/common/connectionManager.test.ts diff --git a/src/common/connectionManager.ts b/src/common/connectionManager.ts index 7a66d4fd9..731afe98f 100644 --- a/src/common/connectionManager.ts +++ b/src/common/connectionManager.ts @@ -21,7 +21,7 @@ export interface ConnectionSettings extends ConnectOptions { type ConnectionTag = "connected" | "connecting" | "disconnected" | "errored"; type OIDCConnectionAuthType = "oidc-auth-flow" | "oidc-device-flow"; -type ConnectionStringAuthType = "scram" | "ldap" | "kerberos" | OIDCConnectionAuthType | "x.509"; +export type ConnectionStringAuthType = "scram" | "ldap" | "kerberos" | OIDCConnectionAuthType | "x.509"; export interface ConnectionState { tag: ConnectionTag; @@ -114,7 +114,7 @@ export class ConnectionManager extends EventEmitter { tag: "connected", connectedAtlasCluster: settings.atlas, serviceProvider, - connectionStringAuthType: this.inferConnectionTypeFromSettings(settings), + connectionStringAuthType: ConnectionManager.inferConnectionTypeFromSettings(settings), }); } catch (error: unknown) { const errorReason = error instanceof Error ? error.message : `${error as string}`; @@ -154,7 +154,7 @@ export class ConnectionManager extends EventEmitter { return newState; } - private inferConnectionTypeFromSettings(settings: ConnectionSettings): ConnectionStringAuthType { + static inferConnectionTypeFromSettings(settings: ConnectionSettings): ConnectionStringAuthType { const connString = new ConnectionString(settings.connectionString); const searchParams = connString.typedSearchParams(); diff --git a/tests/integration/common/connectionManager.test.ts b/tests/integration/common/connectionManager.test.ts new file mode 100644 index 000000000..73fa4d57f --- /dev/null +++ b/tests/integration/common/connectionManager.test.ts @@ -0,0 +1,151 @@ +import { + ConnectionManager, + ConnectionManagerEvents, + ConnectionStateConnected, + ConnectionStringAuthType, +} from "../../../src/common/connectionManager.js"; +import { describeWithMongoDB } from "../tools/mongodb/mongodbHelpers.js"; +import { describe, beforeEach, expect, it, vi, afterEach } from "vitest"; +import { config } from "../../../src/common/config.js"; + +describeWithMongoDB("Connection Manager", (integration) => { + function connectionManager() { + return integration.mcpServer().session.connectionManager; + } + + afterEach(async () => { + // disconnect on purpose doesn't change the state if it was failed to avoid losing + // information in production. + await connectionManager().disconnect(); + // for testing, force disconnecting AND setting the connection to closed to reset the + // state of the connection manager + connectionManager().changeState("connection-closed", { tag: "disconnected" }); + }); + + describe("when successfully connected", () => { + type ConnectionManagerSpies = { + "connection-requested": (event: ConnectionManagerEvents["connection-requested"][0]) => void; + "connection-succeeded": (event: ConnectionManagerEvents["connection-succeeded"][0]) => void; + "connection-timed-out": (event: ConnectionManagerEvents["connection-timed-out"][0]) => void; + "connection-closed": (event: ConnectionManagerEvents["connection-closed"][0]) => void; + "connection-errored": (event: ConnectionManagerEvents["connection-errored"][0]) => void; + }; + + let connectionManagerSpies: ConnectionManagerSpies; + + beforeEach(async () => { + connectionManagerSpies = { + "connection-requested": vi.fn(), + "connection-succeeded": vi.fn(), + "connection-timed-out": vi.fn(), + "connection-closed": vi.fn(), + "connection-errored": vi.fn(), + }; + + for (const [event, spy] of Object.entries(connectionManagerSpies)) { + connectionManager().on(event as keyof ConnectionManagerEvents, spy); + } + + await connectionManager().connect({ + connectionString: integration.connectionString(), + ...integration.mcpServer().userConfig.connectOptions, + }); + }); + + it("should be marked explicitly as connected", () => { + expect(connectionManager().currentConnectionState.tag).toEqual("connected"); + }); + + it("can query mongodb successfully", async () => { + const connectionState = connectionManager().currentConnectionState as ConnectionStateConnected; + const collections = await connectionState.serviceProvider.listCollections("admin"); + expect(collections).not.toBe([]); + }); + + it("should notify that the connection was successful", () => { + expect(connectionManagerSpies["connection-succeeded"]).toHaveBeenCalledOnce(); + }); + + describe("when disconnects", () => { + beforeEach(async () => { + await connectionManager().disconnect(); + }); + + it("should notify that it was disconnected before connecting", () => { + expect(connectionManagerSpies["connection-closed"]).toHaveBeenCalled(); + }); + }); + + describe("when reconnects", () => { + beforeEach(async () => { + await connectionManager().connect({ + connectionString: integration.connectionString(), + ...integration.mcpServer().userConfig.connectOptions, + }); + }); + + it("should notify that it was disconnected before connecting", () => { + expect(connectionManagerSpies["connection-closed"]).toHaveBeenCalled(); + }); + + it("should notify that it was connected again", () => { + expect(connectionManagerSpies["connection-succeeded"]).toHaveBeenCalled(); + }); + }); + + describe("when fails to connect to a new cluster", () => { + beforeEach(async () => { + try { + await connectionManager().connect({ + connectionString: "mongodb://localhost:xxxxx", + ...integration.mcpServer().userConfig.connectOptions, + }); + } catch (_error: unknown) { + void _error; + } + }); + + it("should notify that it was disconnected before connecting", () => { + expect(connectionManagerSpies["connection-closed"]).toHaveBeenCalled(); + }); + + it("should notify that it failed connecting", () => { + expect(connectionManagerSpies["connection-errored"]).toHaveBeenCalled(); + }); + }); + }); + + describe("when disconnected", () => { + it("should be marked explictly as disconnected", () => { + expect(connectionManager().currentConnectionState.tag).toEqual("disconnected"); + }); + }); +}); + +describe("Connection Manager connection type inference", () => { + const testCases = [ + { connectionString: "mongodb://localhost:27017", connectionType: "scram" }, + { connectionString: "mongodb://localhost:27017?authMechanism=MONGODB-X509", connectionType: "x.509" }, + { connectionString: "mongodb://localhost:27017?authMechanism=GSSAPI", connectionType: "kerberos" }, + { + connectionString: "mongodb://localhost:27017?authMechanism=PLAIN&authSource=$external", + connectionType: "ldap", + }, + { connectionString: "mongodb://localhost:27017?authMechanism=PLAIN", connectionType: "scram" }, + { connectionString: "mongodb://localhost:27017?authMechanism=MONGODB-OIDC", connectionType: "oidc-auth-flow" }, + ] as { + connectionString: string; + connectionType: ConnectionStringAuthType; + }[]; + + for (const { connectionString, connectionType } of testCases) { + it(`infers ${connectionType} from ${connectionString}`, () => { + const actualConnectionType = ConnectionManager.inferConnectionTypeFromSettings({ + connectionString, + ...config.connectOptions, + }); + + expect(actualConnectionType).toBe(connectionType); + }); + } +}); From ece4d6cfe128c2ebbef9c6bcf8ac90421ad3d3d5 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Tue, 5 Aug 2025 13:32:43 +0200 Subject: [PATCH 03/26] chore: fix typo --- tests/integration/common/connectionManager.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/common/connectionManager.test.ts b/tests/integration/common/connectionManager.test.ts index 73fa4d57f..440c1b12c 100644 --- a/tests/integration/common/connectionManager.test.ts +++ b/tests/integration/common/connectionManager.test.ts @@ -116,7 +116,7 @@ describeWithMongoDB("Connection Manager", (integration) => { }); describe("when disconnected", () => { - it("should be marked explictly as disconnected", () => { + it("should be marked explicitly as disconnected", () => { expect(connectionManager().currentConnectionState.tag).toEqual("disconnected"); }); }); From 06a7ea8209ae5bddc8dc9f3ec79bfa294876673b Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Tue, 5 Aug 2025 13:35:04 +0200 Subject: [PATCH 04/26] chore: js prefers strict equality --- src/common/session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/session.ts b/src/common/session.ts index b8f066756..78bb4a2fa 100644 --- a/src/common/session.ts +++ b/src/common/session.ts @@ -116,7 +116,7 @@ export class Session extends EventEmitter { } isConnectedToMongoDB(): boolean { - return this.connectionManager.currentConnectionState.tag == "connected"; + return this.connectionManager.currentConnectionState.tag === "connected"; } get serviceProvider(): NodeDriverServiceProvider { From 806382fbe0b8a632af3dadb2afab817154ebbdce Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Tue, 5 Aug 2025 13:36:31 +0200 Subject: [PATCH 05/26] chore: fix linter errors --- src/common/connectionManager.ts | 3 +++ src/tools/mongodb/connect/connect.ts | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common/connectionManager.ts b/src/common/connectionManager.ts index 731afe98f..9cb4f06f3 100644 --- a/src/common/connectionManager.ts +++ b/src/common/connectionManager.ts @@ -171,6 +171,9 @@ export class ConnectionManager extends EventEmitter { return "ldap"; } break; + // default should catch also null, but eslint complains + // about it. + case null: default: return "scram"; } diff --git a/src/tools/mongodb/connect/connect.ts b/src/tools/mongodb/connect/connect.ts index fd86c55c9..b768490bd 100644 --- a/src/tools/mongodb/connect/connect.ts +++ b/src/tools/mongodb/connect/connect.ts @@ -7,7 +7,6 @@ import { UserConfig } from "../../../common/config.js"; import { Telemetry } from "../../../telemetry/telemetry.js"; import { Session } from "../../../common/session.js"; import { Server } from "../../../server.js"; -import logger from "../../../common/logger.js"; const disconnectedSchema = z .object({ From 4c357f9c51b86e7ec63a0e7be227509d51a2ae2f Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Tue, 5 Aug 2025 14:20:48 +0200 Subject: [PATCH 06/26] chore: connection requested is not necessary at the end --- src/common/connectionManager.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/common/connectionManager.ts b/src/common/connectionManager.ts index 9cb4f06f3..8ba5b19e6 100644 --- a/src/common/connectionManager.ts +++ b/src/common/connectionManager.ts @@ -59,7 +59,6 @@ export type AnyConnectionState = | ConnectionStateErrored; export interface ConnectionManagerEvents { - "connection-requested": [AnyConnectionState]; "connection-succeeded": [ConnectionStateConnected]; "connection-timed-out": [ConnectionStateErrored]; "connection-closed": [ConnectionStateDisconnected]; From 3f52815fcfa1a1226834d2640ff970a137aa00cb Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Tue, 5 Aug 2025 14:25:18 +0200 Subject: [PATCH 07/26] chore: add test to connection-requested --- src/common/connectionManager.ts | 3 +++ tests/integration/common/connectionManager.test.ts | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/src/common/connectionManager.ts b/src/common/connectionManager.ts index 8ba5b19e6..3cb7a2aa0 100644 --- a/src/common/connectionManager.ts +++ b/src/common/connectionManager.ts @@ -59,6 +59,7 @@ export type AnyConnectionState = | ConnectionStateErrored; export interface ConnectionManagerEvents { + "connection-requested": [AnyConnectionState]; "connection-succeeded": [ConnectionStateConnected]; "connection-timed-out": [ConnectionStateErrored]; "connection-closed": [ConnectionStateDisconnected]; @@ -74,6 +75,8 @@ export class ConnectionManager extends EventEmitter { } async connect(settings: ConnectionSettings): Promise { + this.emit("connection-requested", this.state); + if (this.state.tag == "connected" || this.state.tag == "connecting") { await this.disconnect(); } diff --git a/tests/integration/common/connectionManager.test.ts b/tests/integration/common/connectionManager.test.ts index 440c1b12c..e1a3e6201 100644 --- a/tests/integration/common/connectionManager.test.ts +++ b/tests/integration/common/connectionManager.test.ts @@ -62,6 +62,10 @@ describeWithMongoDB("Connection Manager", (integration) => { expect(collections).not.toBe([]); }); + it("should notify that the connection was requested", () => { + expect(connectionManagerSpies["connection-requested"]).toHaveBeenCalledOnce(); + }); + it("should notify that the connection was successful", () => { expect(connectionManagerSpies["connection-succeeded"]).toHaveBeenCalledOnce(); }); From 28d8b69ad28bdb0a99f2a5cdae111d6f65394d23 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 10:15:54 +0200 Subject: [PATCH 08/26] chore: add tests for the actual connection status Also remove the code that forces the reconnection in the stdio transport test as it's not needed anymore. --- tests/integration/common/connectionManager.test.ts | 12 ++++++++++++ tests/integration/transports/stdio.test.ts | 7 ------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/integration/common/connectionManager.test.ts b/tests/integration/common/connectionManager.test.ts index e1a3e6201..fed5ac485 100644 --- a/tests/integration/common/connectionManager.test.ts +++ b/tests/integration/common/connectionManager.test.ts @@ -78,6 +78,10 @@ describeWithMongoDB("Connection Manager", (integration) => { it("should notify that it was disconnected before connecting", () => { expect(connectionManagerSpies["connection-closed"]).toHaveBeenCalled(); }); + + it("should be marked explicitly as disconnected", () => { + expect(connectionManager().currentConnectionState.tag).toEqual("disconnected"); + }); }); describe("when reconnects", () => { @@ -95,6 +99,10 @@ describeWithMongoDB("Connection Manager", (integration) => { it("should notify that it was connected again", () => { expect(connectionManagerSpies["connection-succeeded"]).toHaveBeenCalled(); }); + + it("should be marked explicitly as connected", () => { + expect(connectionManager().currentConnectionState.tag).toEqual("connected"); + }); }); describe("when fails to connect to a new cluster", () => { @@ -116,6 +124,10 @@ describeWithMongoDB("Connection Manager", (integration) => { it("should notify that it failed connecting", () => { expect(connectionManagerSpies["connection-errored"]).toHaveBeenCalled(); }); + + it("should be marked explicitly as connected", () => { + expect(connectionManager().currentConnectionState.tag).toEqual("errored"); + }); }); }); diff --git a/tests/integration/transports/stdio.test.ts b/tests/integration/transports/stdio.test.ts index b3b5683c2..dfdb89fd8 100644 --- a/tests/integration/transports/stdio.test.ts +++ b/tests/integration/transports/stdio.test.ts @@ -4,13 +4,6 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { describeWithMongoDB } from "../tools/mongodb/mongodbHelpers.js"; describeWithMongoDB("StdioRunner", (integration) => { - beforeEach(() => { - integration.mcpServer().userConfig.connectionString = integration.connectionString(); - integration.mcpServer().session.connectionManager.changeState("connection-succeeded", { - tag: "connected", - }); - }); - describe("client connects successfully", () => { let client: Client; let transport: StdioClientTransport; From f7ec158a953025cbd843b937cfffb07217d12949 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 12:11:54 +0200 Subject: [PATCH 09/26] chore: Fix typing issues and few PR suggestions --- src/common/connectionManager.ts | 23 +++++++++---------- .../tools/mongodb/connect/connect.test.ts | 8 +++---- tests/integration/transports/stdio.test.ts | 2 +- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/common/connectionManager.ts b/src/common/connectionManager.ts index 3cb7a2aa0..081d096ef 100644 --- a/src/common/connectionManager.ts +++ b/src/common/connectionManager.ts @@ -52,7 +52,6 @@ export interface ConnectionStateErrored extends ConnectionState { } export type AnyConnectionState = - | ConnectionState | ConnectionStateConnected | ConnectionStateConnecting | ConnectionStateDisconnected @@ -77,7 +76,7 @@ export class ConnectionManager extends EventEmitter { async connect(settings: ConnectionSettings): Promise { this.emit("connection-requested", this.state); - if (this.state.tag == "connected" || this.state.tag == "connecting") { + if (this.state.tag === "connected" || this.state.tag === "connecting") { await this.disconnect(); } @@ -126,18 +125,13 @@ export class ConnectionManager extends EventEmitter { } async disconnect(): Promise { - if (this.state.tag == "disconnected") { - return this.state as ConnectionStateDisconnected; - } - - if (this.state.tag == "errored") { - return this.state as ConnectionStateErrored; + if (this.state.tag === "disconnected" || this.state.tag === "errored") { + return this.state; } if (this.state.tag == "connected" || this.state.tag == "connecting") { - const state = this.state as ConnectionStateConnecting | ConnectionStateConnected; try { - await state.serviceProvider?.close(true); + await this.state.serviceProvider?.close(true); } finally { this.changeState("connection-closed", { tag: "disconnected" }); } @@ -150,9 +144,14 @@ export class ConnectionManager extends EventEmitter { return this.state; } - changeState(event: keyof ConnectionManagerEvents, newState: State): State { + changeState( + event: Event, + newState: State + ): State { this.state = newState; - this.emit(event, newState); + // TypeScript doesn't seem to be happy with the spread operator and generics + // eslint-disable-next-line + this.emit(event, ...([newState] as any)); return newState; } diff --git a/tests/integration/tools/mongodb/connect/connect.test.ts b/tests/integration/tools/mongodb/connect/connect.test.ts index b162fc13c..8e9d20f3e 100644 --- a/tests/integration/tools/mongodb/connect/connect.test.ts +++ b/tests/integration/tools/mongodb/connect/connect.test.ts @@ -12,10 +12,10 @@ import { beforeEach, describe, expect, it } from "vitest"; describeWithMongoDB( "SwitchConnection tool", (integration) => { - beforeEach(() => { - integration.mcpServer().userConfig.connectionString = integration.connectionString(); - integration.mcpServer().session.connectionManager.changeState("connection-succeeded", { - tag: "connected", + beforeEach(async () => { + await integration.mcpServer().session.connectToMongoDB({ + connectionString: integration.connectionString(), + ...config.connectOptions, }); }); diff --git a/tests/integration/transports/stdio.test.ts b/tests/integration/transports/stdio.test.ts index dfdb89fd8..6b08e4e6a 100644 --- a/tests/integration/transports/stdio.test.ts +++ b/tests/integration/transports/stdio.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, beforeEach, it, beforeAll, afterAll } from "vitest"; +import { describe, expect, it, beforeAll, afterAll } from "vitest"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { describeWithMongoDB } from "../tools/mongodb/mongodbHelpers.js"; From f49e2b08f2cb1bf4e43aaadd5e6a166da23dfaa5 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 12:17:42 +0200 Subject: [PATCH 10/26] chore: style changes, use a getter for isConnectedToMongoDB --- src/common/session.ts | 8 ++++---- src/tools/mongodb/connect/connect.ts | 2 +- src/tools/mongodb/mongodbTool.ts | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/common/session.ts b/src/common/session.ts index 78bb4a2fa..2c86a7322 100644 --- a/src/common/session.ts +++ b/src/common/session.ts @@ -105,9 +105,9 @@ export class Session extends EventEmitter { await this.apiClient.close(); } - async connectToMongoDB(settings: ConnectionSettings): Promise { + async connectToMongoDB(settings: ConnectionSettings): Promise { try { - return await this.connectionManager.connect({ ...settings }); + await this.connectionManager.connect({ ...settings }); } catch (error: unknown) { const message = error instanceof Error ? error.message : (error as string); this.emit("connection-error", message); @@ -115,12 +115,12 @@ export class Session extends EventEmitter { } } - isConnectedToMongoDB(): boolean { + get isConnectedToMongoDB(): boolean { return this.connectionManager.currentConnectionState.tag === "connected"; } get serviceProvider(): NodeDriverServiceProvider { - if (this.isConnectedToMongoDB()) { + if (this.isConnectedToMongoDB) { const state = this.connectionManager.currentConnectionState as ConnectionStateConnected; return state.serviceProvider; } diff --git a/src/tools/mongodb/connect/connect.ts b/src/tools/mongodb/connect/connect.ts index b768490bd..1a1f8cd8b 100644 --- a/src/tools/mongodb/connect/connect.ts +++ b/src/tools/mongodb/connect/connect.ts @@ -87,7 +87,7 @@ export class ConnectTool extends MongoDBToolBase { } private updateMetadata(): void { - if (this.session.isConnectedToMongoDB()) { + if (this.session.isConnectedToMongoDB) { this.update?.({ name: connectedName, description: connectedDescription, diff --git a/src/tools/mongodb/mongodbTool.ts b/src/tools/mongodb/mongodbTool.ts index 6fa09682c..9497d6d55 100644 --- a/src/tools/mongodb/mongodbTool.ts +++ b/src/tools/mongodb/mongodbTool.ts @@ -17,7 +17,7 @@ export abstract class MongoDBToolBase extends ToolBase { public category: ToolCategory = "mongodb"; protected async ensureConnected(): Promise { - if (!this.session.isConnectedToMongoDB()) { + if (!this.session.isConnectedToMongoDB) { if (this.session.connectedAtlasCluster) { throw new MongoDBError( ErrorCodes.NotConnectedToMongoDB, @@ -39,7 +39,7 @@ export abstract class MongoDBToolBase extends ToolBase { } } - if (!this.session.isConnectedToMongoDB()) { + if (!this.session.isConnectedToMongoDB) { throw new MongoDBError(ErrorCodes.NotConnectedToMongoDB, "Not connected to MongoDB"); } @@ -117,7 +117,7 @@ export abstract class MongoDBToolBase extends ToolBase { return super.handleError(error, args); } - protected connectToMongoDB(connectionString: string): Promise { + protected connectToMongoDB(connectionString: string): Promise { return this.session.connectToMongoDB({ connectionString, ...this.config.connectOptions }); } From e03b7a6e7b81c96abeac88187e0eecdf8763d480 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 12:47:26 +0200 Subject: [PATCH 11/26] chore: move AtlasConnectionInfo to the connection manager But keep deleting users outside, as it's out of scope of the connection manager as it's not related to MongoDB itself but to Atlas. --- src/common/session.ts | 28 +++++++++------ src/tools/atlas/connect/connectCluster.ts | 44 ++++++++++++++--------- src/tools/mongodb/mongodbTool.ts | 1 - 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/common/session.ts b/src/common/session.ts index 2c86a7322..2f1d6cbec 100644 --- a/src/common/session.ts +++ b/src/common/session.ts @@ -3,7 +3,7 @@ import { Implementation } from "@modelcontextprotocol/sdk/types.js"; import logger, { LogId } from "./logger.js"; import EventEmitter from "events"; import { - AnyConnectionState, + AtlasClusterConnectionInfo, ConnectionManager, ConnectionSettings, ConnectionStateConnected, @@ -33,12 +33,6 @@ export class Session extends EventEmitter { name: string; version: string; }; - connectedAtlasCluster?: { - username: string; - projectId: string; - clusterName: string; - expiryDate: Date; - }; constructor({ apiBaseUrl, apiClientId, apiClientSecret, connectionManager }: SessionOptions) { super(); @@ -70,6 +64,10 @@ export class Session extends EventEmitter { } async disconnect(): Promise { + const currentConnection = this.connectionManager.currentConnectionState; + const atlasCluster = + currentConnection.tag === "connected" ? currentConnection.connectedAtlasCluster : undefined; + try { await this.connectionManager.disconnect(); } catch (err: unknown) { @@ -77,13 +75,13 @@ export class Session extends EventEmitter { logger.error(LogId.mongodbDisconnectFailure, "Error closing service provider:", error.message); } - if (this.connectedAtlasCluster?.username && this.connectedAtlasCluster?.projectId) { + if (atlasCluster?.username && atlasCluster?.projectId) { void this.apiClient .deleteDatabaseUser({ params: { path: { - groupId: this.connectedAtlasCluster.projectId, - username: this.connectedAtlasCluster.username, + groupId: atlasCluster.projectId, + username: atlasCluster.username, databaseName: "admin", }, }, @@ -96,7 +94,6 @@ export class Session extends EventEmitter { `Error deleting previous database user: ${error.message}` ); }); - this.connectedAtlasCluster = undefined; } } @@ -127,4 +124,13 @@ export class Session extends EventEmitter { throw new MongoDBError(ErrorCodes.NotConnectedToMongoDB, "Not connected to MongoDB"); } + + get connectedAtlasCluster(): AtlasClusterConnectionInfo | undefined { + const connectionState = this.connectionManager.currentConnectionState; + if (connectionState.tag === "connected") { + return connectionState.connectedAtlasCluster; + } + + return undefined; + } } diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index 62e9f7391..da038312c 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -6,6 +6,7 @@ import { generateSecurePassword } from "../../../helpers/generatePassword.js"; import logger, { LogId } from "../../../common/logger.js"; import { inspectCluster } from "../../../common/atlas/cluster.js"; import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUtils.js"; +import { AtlasClusterConnectionInfo } from "../../../common/connectionManager.js"; const EXPIRY_MS = 1000 * 60 * 60 * 12; // 12 hours @@ -27,7 +28,7 @@ export class ConnectClusterTool extends AtlasToolBase { clusterName: string ): Promise<"connected" | "disconnected" | "connecting" | "connected-to-other-cluster" | "unknown"> { if (!this.session.connectedAtlasCluster) { - if (this.session.isConnectedToMongoDB()) { + if (this.session.isConnectedToMongoDB) { return "connected-to-other-cluster"; } return "disconnected"; @@ -40,7 +41,7 @@ export class ConnectClusterTool extends AtlasToolBase { return "connected-to-other-cluster"; } - if (!this.session.isConnectedToMongoDB()) { + if (!this.session.isConnectedToMongoDB) { return "connecting"; } @@ -61,7 +62,10 @@ export class ConnectClusterTool extends AtlasToolBase { } } - private async prepareClusterConnection(projectId: string, clusterName: string): Promise { + private async prepareClusterConnection( + projectId: string, + clusterName: string + ): Promise<[string, AtlasClusterConnectionInfo]> { const cluster = await inspectCluster(this.session.apiClient, projectId, clusterName); if (!cluster.connectionString) { @@ -109,7 +113,7 @@ export class ConnectClusterTool extends AtlasToolBase { }, }); - this.session.connectedAtlasCluster = { + const connectedAtlasCluster = { username, projectId, clusterName, @@ -120,10 +124,15 @@ export class ConnectClusterTool extends AtlasToolBase { cn.username = username; cn.password = password; cn.searchParams.set("authSource", "admin"); - return cn.toString(); + return [cn.toString(), connectedAtlasCluster]; } - private async connectToCluster(projectId: string, clusterName: string, connectionString: string): Promise { + private async connectToCluster( + projectId: string, + clusterName: string, + connectionString: string, + atlas: AtlasClusterConnectionInfo + ): Promise { let lastError: Error | undefined = undefined; logger.debug( @@ -145,7 +154,7 @@ export class ConnectClusterTool extends AtlasToolBase { try { lastError = undefined; - await this.session.connectToMongoDB({ connectionString, ...this.config.connectOptions }); + await this.session.connectToMongoDB({ connectionString, ...this.config.connectOptions, atlas }); break; } catch (err: unknown) { const error = err instanceof Error ? err : new Error(String(err)); @@ -187,7 +196,6 @@ export class ConnectClusterTool extends AtlasToolBase { ); }); } - this.session.connectedAtlasCluster = undefined; throw lastError; } @@ -221,17 +229,19 @@ export class ConnectClusterTool extends AtlasToolBase { case "disconnected": default: { await this.session.disconnect(); - const connectionString = await this.prepareClusterConnection(projectId, clusterName); + const [connectionString, atlas] = await this.prepareClusterConnection(projectId, clusterName); // try to connect for about 5 minutes asynchronously - void this.connectToCluster(projectId, clusterName, connectionString).catch((err: unknown) => { - const error = err instanceof Error ? err : new Error(String(err)); - logger.error( - LogId.atlasConnectFailure, - "atlas-connect-cluster", - `error connecting to cluster: ${error.message}` - ); - }); + void this.connectToCluster(projectId, clusterName, connectionString, atlas).catch( + (err: unknown) => { + const error = err instanceof Error ? err : new Error(String(err)); + logger.error( + LogId.atlasConnectFailure, + "atlas-connect-cluster", + `error connecting to cluster: ${error.message}` + ); + } + ); break; } } diff --git a/src/tools/mongodb/mongodbTool.ts b/src/tools/mongodb/mongodbTool.ts index 9497d6d55..7071b818b 100644 --- a/src/tools/mongodb/mongodbTool.ts +++ b/src/tools/mongodb/mongodbTool.ts @@ -5,7 +5,6 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { ErrorCodes, MongoDBError } from "../../common/errors.js"; import logger, { LogId } from "../../common/logger.js"; import { Server } from "../../server.js"; -import { AnyConnectionState } from "../../common/connectionManager.js"; export const DbOperationArgs = { database: z.string().describe("Database name"), From 43b54a08fb3b7a5df307aeb6d9b18bd68f81afe0 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 12:51:27 +0200 Subject: [PATCH 12/26] chore: fixed linting issues --- src/common/connectionManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/connectionManager.ts b/src/common/connectionManager.ts index 081d096ef..160e12b13 100644 --- a/src/common/connectionManager.ts +++ b/src/common/connectionManager.ts @@ -129,7 +129,7 @@ export class ConnectionManager extends EventEmitter { return this.state; } - if (this.state.tag == "connected" || this.state.tag == "connecting") { + if (this.state.tag === "connected" || this.state.tag === "connecting") { try { await this.state.serviceProvider?.close(true); } finally { @@ -168,7 +168,7 @@ export class ConnectionManager extends EventEmitter { case "GSSAPI": return "kerberos"; case "PLAIN": - if (searchParams.get("authSource") == "$external") { + if (searchParams.get("authSource") === "$external") { return "ldap"; } break; From 89ba76d1180d2f9f9d297464574375e34c2461cd Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 13:02:53 +0200 Subject: [PATCH 13/26] chore: emit the close event We are not using it, but to be consistent with what we had before we are keeping it. --- src/common/session.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/session.ts b/src/common/session.ts index 2f1d6cbec..95b5162b2 100644 --- a/src/common/session.ts +++ b/src/common/session.ts @@ -100,6 +100,7 @@ export class Session extends EventEmitter { async close(): Promise { await this.disconnect(); await this.apiClient.close(); + this.emit("close"); } async connectToMongoDB(settings: ConnectionSettings): Promise { From 61d7b1ec023ad473d84e8834508a06c950b9f731 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 13:06:49 +0200 Subject: [PATCH 14/26] chore: add resource subscriptions and improve the resource prompt --- src/resources/common/debug.ts | 3 ++- src/server.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/resources/common/debug.ts b/src/resources/common/debug.ts index 60fcda070..609b4b8ef 100644 --- a/src/resources/common/debug.ts +++ b/src/resources/common/debug.ts @@ -13,7 +13,8 @@ export class DebugResource extends ReactiveResource( name: "debug-mongodb", uri: "debug://mongodb", config: { - description: "Debugging information for MongoDB connectivity issues.", + description: + "Debugging information for MongoDB connectivity issues. Tracks the last connectivity error and attempt information.", }, }, { diff --git a/src/server.ts b/src/server.ts index 9f1a959aa..209bec02b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -43,7 +43,7 @@ export class Server { this.registerResources(); await this.validateConfig(); - this.mcpServer.server.registerCapabilities({ logging: {} }); + this.mcpServer.server.registerCapabilities({ logging: {}, resources: { subscribe: true, listChanged: true } }); // TODO: Eventually we might want to make tools reactive too instead of relying on custom logic. this.registerTools(); From a33abb389671dbb4bf0e9a8e89578384cda8c34d Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 13:18:04 +0200 Subject: [PATCH 15/26] chore: Do not use anonymous tuples --- src/tools/atlas/connect/connectCluster.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index da038312c..20ee40ea0 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -65,7 +65,7 @@ export class ConnectClusterTool extends AtlasToolBase { private async prepareClusterConnection( projectId: string, clusterName: string - ): Promise<[string, AtlasClusterConnectionInfo]> { + ): Promise<{ connectionString: string; atlas: AtlasClusterConnectionInfo }> { const cluster = await inspectCluster(this.session.apiClient, projectId, clusterName); if (!cluster.connectionString) { @@ -124,7 +124,8 @@ export class ConnectClusterTool extends AtlasToolBase { cn.username = username; cn.password = password; cn.searchParams.set("authSource", "admin"); - return [cn.toString(), connectedAtlasCluster]; + + return { connectionString: cn.toString(), atlas: connectedAtlasCluster }; } private async connectToCluster( @@ -229,7 +230,7 @@ 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); // try to connect for about 5 minutes asynchronously void this.connectToCluster(projectId, clusterName, connectionString, atlas).catch( From cc927664a50fae444eb7bb07fa98048cbefd7ea6 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 13:20:20 +0200 Subject: [PATCH 16/26] chore: change the break to a return to make it easier to follow --- src/common/connectionManager.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common/connectionManager.ts b/src/common/connectionManager.ts index 160e12b13..e49e94026 100644 --- a/src/common/connectionManager.ts +++ b/src/common/connectionManager.ts @@ -171,13 +171,12 @@ export class ConnectionManager extends EventEmitter { if (searchParams.get("authSource") === "$external") { return "ldap"; } - break; + return "scram"; // default should catch also null, but eslint complains // about it. case null: default: return "scram"; } - return "scram"; } } From 0a77b97ef4ba07d91fdab787739aaf4858edeb19 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 13:20:37 +0200 Subject: [PATCH 17/26] chore: small refactor --- src/common/session.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/common/session.ts b/src/common/session.ts index 95b5162b2..689e687e0 100644 --- a/src/common/session.ts +++ b/src/common/session.ts @@ -64,9 +64,7 @@ export class Session extends EventEmitter { } async disconnect(): Promise { - const currentConnection = this.connectionManager.currentConnectionState; - const atlasCluster = - currentConnection.tag === "connected" ? currentConnection.connectedAtlasCluster : undefined; + const atlasCluster = this.connectedAtlasCluster; try { await this.connectionManager.disconnect(); From 6b5f49b5e1b774aad78f376180915a1eb030f7b8 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 13:37:53 +0200 Subject: [PATCH 18/26] chore: minor clean up of redundant params --- src/tools/atlas/connect/connectCluster.ts | 33 +++++++++-------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index 20ee40ea0..f587be6df 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -128,12 +128,7 @@ export class ConnectClusterTool extends AtlasToolBase { return { connectionString: cn.toString(), atlas: connectedAtlasCluster }; } - private async connectToCluster( - projectId: string, - clusterName: string, - connectionString: string, - atlas: AtlasClusterConnectionInfo - ): Promise { + private async connectToCluster(connectionString: string, atlas: AtlasClusterConnectionInfo): Promise { let lastError: Error | undefined = undefined; logger.debug( @@ -146,8 +141,8 @@ export class ConnectClusterTool extends AtlasToolBase { for (let i = 0; i < 600; i++) { if ( !this.session.connectedAtlasCluster || - this.session.connectedAtlasCluster.projectId != projectId || - this.session.connectedAtlasCluster.clusterName != clusterName + this.session.connectedAtlasCluster.projectId != atlas.projectId || + this.session.connectedAtlasCluster.clusterName != atlas.clusterName ) { throw new Error("Cluster connection aborted"); } @@ -174,8 +169,8 @@ export class ConnectClusterTool extends AtlasToolBase { if (lastError) { if ( - this.session.connectedAtlasCluster?.projectId == projectId && - this.session.connectedAtlasCluster?.clusterName == clusterName && + this.session.connectedAtlasCluster?.projectId == atlas.projectId && + this.session.connectedAtlasCluster?.clusterName == atlas.clusterName && this.session.connectedAtlasCluster?.username ) { void this.session.apiClient @@ -233,16 +228,14 @@ export class ConnectClusterTool extends AtlasToolBase { const { connectionString, atlas } = await this.prepareClusterConnection(projectId, clusterName); // try to connect for about 5 minutes asynchronously - void this.connectToCluster(projectId, clusterName, connectionString, atlas).catch( - (err: unknown) => { - const error = err instanceof Error ? err : new Error(String(err)); - logger.error( - LogId.atlasConnectFailure, - "atlas-connect-cluster", - `error connecting to cluster: ${error.message}` - ); - } - ); + void this.connectToCluster(connectionString, atlas).catch((err: unknown) => { + const error = err instanceof Error ? err : new Error(String(err)); + logger.error( + LogId.atlasConnectFailure, + "atlas-connect-cluster", + `error connecting to cluster: ${error.message}` + ); + }); break; } } From 1122e09a943cca6bb8e923e22825e3e4854b49cc Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 13:52:15 +0200 Subject: [PATCH 19/26] chore: ensure that we return connecting when not connected yet --- src/tools/atlas/connect/connectCluster.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index 99a9479ca..b5a31ca41 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -41,7 +41,7 @@ export class ConnectClusterTool extends AtlasToolBase { return "connected-to-other-cluster"; } - if (!this.session.isConnectedToMongoDB) { + if (this.session.connectionManager.currentConnectionState.tag !== "connected") { return "connecting"; } From da872b4cd78fd44d08f729976f9190cefb36b07e Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 14:02:09 +0200 Subject: [PATCH 20/26] chore: allow having the atlas cluster info independently of the connection state We used to support only having it when connected, but it might be useful also when errored --- src/common/connectionManager.ts | 14 +++++++++++--- src/common/session.ts | 7 +------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/common/connectionManager.ts b/src/common/connectionManager.ts index e49e94026..d1f549e6f 100644 --- a/src/common/connectionManager.ts +++ b/src/common/connectionManager.ts @@ -26,12 +26,12 @@ export type ConnectionStringAuthType = "scram" | "ldap" | "kerberos" | OIDCConne export interface ConnectionState { tag: ConnectionTag; connectionStringAuthType?: ConnectionStringAuthType; + connectedAtlasCluster?: AtlasClusterConnectionInfo; } export interface ConnectionStateConnected extends ConnectionState { tag: "connected"; serviceProvider: NodeDriverServiceProvider; - connectedAtlasCluster?: AtlasClusterConnectionInfo; } export interface ConnectionStateConnecting extends ConnectionState { @@ -104,7 +104,11 @@ export class ConnectionManager extends EventEmitter { }); } catch (error: unknown) { const errorReason = error instanceof Error ? error.message : `${error as string}`; - this.changeState("connection-errored", { tag: "errored", errorReason }); + this.changeState("connection-errored", { + tag: "errored", + errorReason, + connectedAtlasCluster: settings.atlas, + }); throw new MongoDBError(ErrorCodes.MisconfiguredConnectionString, errorReason); } @@ -119,7 +123,11 @@ export class ConnectionManager extends EventEmitter { }); } catch (error: unknown) { const errorReason = error instanceof Error ? error.message : `${error as string}`; - this.changeState("connection-errored", { tag: "errored", errorReason }); + this.changeState("connection-errored", { + tag: "errored", + errorReason, + connectedAtlasCluster: settings.atlas, + }); throw new MongoDBError(ErrorCodes.NotConnectedToMongoDB, errorReason); } } diff --git a/src/common/session.ts b/src/common/session.ts index 689e687e0..0baccc9bf 100644 --- a/src/common/session.ts +++ b/src/common/session.ts @@ -125,11 +125,6 @@ export class Session extends EventEmitter { } get connectedAtlasCluster(): AtlasClusterConnectionInfo | undefined { - const connectionState = this.connectionManager.currentConnectionState; - if (connectionState.tag === "connected") { - return connectionState.connectedAtlasCluster; - } - - return undefined; + return this.connectionManager.currentConnectionState.connectedAtlasCluster; } } From fe16acd312f490783c1ae14db7a12f55e4053ddd Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 14:15:59 +0200 Subject: [PATCH 21/26] chore: simplify query connection logic Now it doesn't need to be async because the connection itself attemps to ping, it just needs to check the current connection status and map the exact values --- src/tools/atlas/connect/connectCluster.ts | 51 +++++++++++------------ 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index b5a31ca41..043c2c595 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -23,10 +23,10 @@ export class ConnectClusterTool extends AtlasToolBase { clusterName: z.string().describe("Atlas cluster name"), }; - private async queryConnection( + private queryConnection( projectId: string, clusterName: string - ): Promise<"connected" | "disconnected" | "connecting" | "connected-to-other-cluster" | "unknown"> { + ): "connected" | "disconnected" | "connecting" | "connected-to-other-cluster" | "unknown" { if (!this.session.connectedAtlasCluster) { if (this.session.isConnectedToMongoDB) { return "connected-to-other-cluster"; @@ -34,32 +34,29 @@ export class ConnectClusterTool extends AtlasToolBase { return "disconnected"; } - if ( - this.session.connectedAtlasCluster.projectId !== projectId || - this.session.connectedAtlasCluster.clusterName !== clusterName - ) { - return "connected-to-other-cluster"; - } - - if (this.session.connectionManager.currentConnectionState.tag !== "connected") { - return "connecting"; - } + const currentConectionState = this.session.connectionManager.currentConnectionState; - try { - await this.session.serviceProvider.runCommand("admin", { - ping: 1, - }); - - return "connected"; - } catch (err: unknown) { - const error = err instanceof Error ? err : new Error(String(err)); - logger.debug( - LogId.atlasConnectFailure, - "atlas-connect-cluster", - `error querying cluster: ${error.message}` - ); - return "unknown"; + switch (currentConectionState.tag) { + case "connected": + if ( + this.session.connectedAtlasCluster.projectId !== projectId || + this.session.connectedAtlasCluster.clusterName !== clusterName + ) { + return "connected-to-other-cluster"; + } + break; + case "connecting": + case "disconnected": // we might still be calling Atlas APIs and not attempted yet to connect to MongoDB, but we are still "connecting" + return "connecting"; + case "errored": + logger.debug( + LogId.atlasConnectFailure, + "atlas-connect-cluster", + `error querying cluster: ${currentConectionState.errorReason}` + ); + return "unknown"; } + return "unknown"; } private async prepareClusterConnection( @@ -205,7 +202,7 @@ export class ConnectClusterTool extends AtlasToolBase { protected async execute({ projectId, clusterName }: ToolArgs): Promise { await ensureCurrentIpInAccessList(this.session.apiClient, projectId); for (let i = 0; i < 60; i++) { - const state = await this.queryConnection(projectId, clusterName); + const state = this.queryConnection(projectId, clusterName); switch (state) { case "connected": { return { From 17494ace18367effe7cdbb2aae8b4f5310dfa32e Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 14:21:31 +0200 Subject: [PATCH 22/26] chore: This is clearer on how the behavior should look like --- src/tools/atlas/connect/connectCluster.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index 043c2c595..7d61d0fb3 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -219,9 +219,9 @@ export class ConnectClusterTool extends AtlasToolBase { break; } case "connected-to-other-cluster": + await this.session.disconnect(); case "disconnected": default: { - await this.session.disconnect(); const { connectionString, atlas } = await this.prepareClusterConnection(projectId, clusterName); // try to connect for about 5 minutes asynchronously From 693c7551d0d6291fe2f0b5cfdce793dfa738b5b4 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 14:41:04 +0200 Subject: [PATCH 23/26] chore: finish the refactor and clean up --- src/tools/atlas/connect/connectCluster.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index 7d61d0fb3..3186cb9da 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -35,7 +35,6 @@ export class ConnectClusterTool extends AtlasToolBase { } const currentConectionState = this.session.connectionManager.currentConnectionState; - switch (currentConectionState.tag) { case "connected": if ( @@ -43,6 +42,8 @@ export class ConnectClusterTool extends AtlasToolBase { this.session.connectedAtlasCluster.clusterName !== clusterName ) { return "connected-to-other-cluster"; + } else { + return "connected"; } break; case "connecting": @@ -220,6 +221,7 @@ export class ConnectClusterTool extends AtlasToolBase { } case "connected-to-other-cluster": await this.session.disconnect(); + // eslint-disable-next-line no-fallthrough case "disconnected": default: { const { connectionString, atlas } = await this.prepareClusterConnection(projectId, clusterName); From a6efc470c35637f610503d00a0e6cca050df1dc1 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 14:50:06 +0200 Subject: [PATCH 24/26] chore: propagate connected atlas cluster when disconnecting --- src/common/connectionManager.ts | 7 +++++-- src/tools/atlas/connect/connectCluster.ts | 2 -- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/common/connectionManager.ts b/src/common/connectionManager.ts index d1f549e6f..9388c60db 100644 --- a/src/common/connectionManager.ts +++ b/src/common/connectionManager.ts @@ -141,11 +141,14 @@ export class ConnectionManager extends EventEmitter { try { await this.state.serviceProvider?.close(true); } finally { - this.changeState("connection-closed", { tag: "disconnected" }); + this.changeState("connection-closed", { + tag: "disconnected", + connectedAtlasCluster: this.state.connectedAtlasCluster, + }); } } - return { tag: "disconnected" }; + return { tag: "disconnected", connectedAtlasCluster: this.state.connectedAtlasCluster }; } get currentConnectionState(): AnyConnectionState { diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index 3186cb9da..b5392f84b 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -45,7 +45,6 @@ export class ConnectClusterTool extends AtlasToolBase { } else { return "connected"; } - break; case "connecting": case "disconnected": // we might still be calling Atlas APIs and not attempted yet to connect to MongoDB, but we are still "connecting" return "connecting"; @@ -57,7 +56,6 @@ export class ConnectClusterTool extends AtlasToolBase { ); return "unknown"; } - return "unknown"; } private async prepareClusterConnection( From 71dbf56b8d5e8d8fc9865f3bb7f446b3fad41011 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 15:15:36 +0200 Subject: [PATCH 25/26] chore: fix linter issues and some status mismatch --- src/tools/atlas/connect/connectCluster.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index b5392f84b..5bc9575ed 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -35,19 +35,19 @@ export class ConnectClusterTool extends AtlasToolBase { } const currentConectionState = this.session.connectionManager.currentConnectionState; + if ( + this.session.connectedAtlasCluster.projectId !== projectId || + this.session.connectedAtlasCluster.clusterName !== clusterName + ) { + return "connected-to-other-cluster"; + } + switch (currentConectionState.tag) { - case "connected": - if ( - this.session.connectedAtlasCluster.projectId !== projectId || - this.session.connectedAtlasCluster.clusterName !== clusterName - ) { - return "connected-to-other-cluster"; - } else { - return "connected"; - } case "connecting": case "disconnected": // we might still be calling Atlas APIs and not attempted yet to connect to MongoDB, but we are still "connecting" return "connecting"; + case "connected": + return "connected"; case "errored": logger.debug( LogId.atlasConnectFailure, @@ -218,10 +218,9 @@ export class ConnectClusterTool extends AtlasToolBase { break; } case "connected-to-other-cluster": - await this.session.disconnect(); - // eslint-disable-next-line no-fallthrough case "disconnected": default: { + await this.session.disconnect(); const { connectionString, atlas } = await this.prepareClusterConnection(projectId, clusterName); // try to connect for about 5 minutes asynchronously From 76e8ab544eea8639e28a59393059ed507cc42d52 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 6 Aug 2025 15:42:49 +0200 Subject: [PATCH 26/26] chore: clean up atlas resource handling --- src/common/connectionManager.ts | 3 +-- src/tools/atlas/connect/connectCluster.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/common/connectionManager.ts b/src/common/connectionManager.ts index 9388c60db..db33b21b6 100644 --- a/src/common/connectionManager.ts +++ b/src/common/connectionManager.ts @@ -143,12 +143,11 @@ export class ConnectionManager extends EventEmitter { } finally { this.changeState("connection-closed", { tag: "disconnected", - connectedAtlasCluster: this.state.connectedAtlasCluster, }); } } - return { tag: "disconnected", connectedAtlasCluster: this.state.connectedAtlasCluster }; + return { tag: "disconnected" }; } get currentConnectionState(): AnyConnectionState { diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index 5bc9575ed..1af1aa3dc 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -135,14 +135,6 @@ export class ConnectClusterTool extends AtlasToolBase { // try to connect for about 5 minutes for (let i = 0; i < 600; i++) { - if ( - !this.session.connectedAtlasCluster || - this.session.connectedAtlasCluster.projectId !== atlas.projectId || - this.session.connectedAtlasCluster.clusterName !== atlas.clusterName - ) { - throw new Error("Cluster connection aborted"); - } - try { lastError = undefined; @@ -161,6 +153,14 @@ export class ConnectClusterTool extends AtlasToolBase { await sleep(500); // wait for 500ms before retrying } + + if ( + !this.session.connectedAtlasCluster || + this.session.connectedAtlasCluster.projectId !== atlas.projectId || + this.session.connectedAtlasCluster.clusterName !== atlas.clusterName + ) { + throw new Error("Cluster connection aborted"); + } } if (lastError) {