Skip to content
8 changes: 8 additions & 0 deletions src/common/connectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {

let serviceProvider: NodeDriverServiceProvider;
let connectionInfo: ConnectionInfo;
let connectionStringAuthType: ConnectionStringAuthType = "scram";

try {
settings = { ...settings };
Expand Down Expand Up @@ -137,6 +138,11 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
connectionInfo.driverOptions.proxy ??= { useEnvironmentVariableProxies: true };
connectionInfo.driverOptions.applyProxyToOIDC ??= true;

connectionStringAuthType = ConnectionManager.inferConnectionTypeFromSettings(
this.userConfig,
connectionInfo
);

serviceProvider = await NodeDriverServiceProvider.connect(
connectionInfo.connectionString,
{
Expand All @@ -152,6 +158,7 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
this.changeState("connection-errored", {
tag: "errored",
errorReason,
connectionStringAuthType,
connectedAtlasCluster: settings.atlas,
});
throw new MongoDBError(ErrorCodes.MisconfiguredConnectionString, errorReason);
Expand Down Expand Up @@ -184,6 +191,7 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
this.changeState("connection-errored", {
tag: "errored",
errorReason,
connectionStringAuthType,
connectedAtlasCluster: settings.atlas,
});
throw new MongoDBError(ErrorCodes.NotConnectedToMongoDB, errorReason);
Expand Down
15 changes: 5 additions & 10 deletions src/common/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
ConnectionManager,
ConnectionSettings,
ConnectionStateConnected,
ConnectionStateErrored,
} from "./connectionManager.js";
import type { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
import { ErrorCodes, MongoDBError } from "./errors.js";
Expand All @@ -28,7 +29,7 @@ export type SessionEvents = {
connect: [];
close: [];
disconnect: [];
"connection-error": [string];
"connection-error": [ConnectionStateErrored];
};

export class Session extends EventEmitter<SessionEvents> {
Expand Down Expand Up @@ -67,9 +68,9 @@ export class Session extends EventEmitter<SessionEvents> {
this.exportsManager = exportsManager;
this.connectionManager = 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-timed-out", (error) => this.emit("connection-error", error));
this.connectionManager.on("connection-closed", () => this.emit("disconnect"));
this.connectionManager.on("connection-errored", (error) => this.emit("connection-error", error.errorReason));
this.connectionManager.on("connection-errored", (error) => this.emit("connection-error", error));
}

setMcpClient(mcpClient: Implementation | undefined): void {
Expand Down Expand Up @@ -136,13 +137,7 @@ export class Session extends EventEmitter<SessionEvents> {
}

async connectToMongoDB(settings: ConnectionSettings): Promise<void> {
try {
await this.connectionManager.connect({ ...settings });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : (error as string);
this.emit("connection-error", message);
throw error;
}
await this.connectionManager.connect({ ...settings });
}

get isConnectedToMongoDB(): boolean {
Expand Down
25 changes: 18 additions & 7 deletions src/resources/common/debug.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { ReactiveResource } from "../resource.js";
import type { Telemetry } from "../../telemetry/telemetry.js";
import type { Session, UserConfig } from "../../lib.js";
import type { AtlasClusterConnectionInfo, ConnectionStateErrored } from "../../common/connectionManager.js";

type ConnectionStateDebuggingInformation = {
readonly tag: "connected" | "connecting" | "disconnected" | "errored";
readonly connectionStringAuthType?: "scram" | "ldap" | "kerberos" | "oidc-auth-flow" | "oidc-device-flow" | "x.509";
readonly oidcLoginUrl?: string;
readonly oidcUserCode?: string;
readonly errorReason?: string;
readonly connectedAtlasCluster?: AtlasClusterConnectionInfo;
};

export class DebugResource extends ReactiveResource<
Expand Down Expand Up @@ -35,15 +35,21 @@ export class DebugResource extends ReactiveResource<
}
reduce(
eventName: "connect" | "disconnect" | "close" | "connection-error",
event: string | undefined
event: ConnectionStateErrored | undefined
): ConnectionStateDebuggingInformation {
void event;

switch (eventName) {
case "connect":
return { tag: "connected" };
case "connection-error":
return { tag: "errored", errorReason: event };
case "connection-error": {
return {
tag: "errored",
connectionStringAuthType: event?.connectionStringAuthType,
connectedAtlasCluster: event?.connectedAtlasCluster,
errorReason:
event?.errorReason ??
"Could not find a reason. This might be a bug in the MCP Server. Please open an issue in https://github.com/mongodb-js/mongodb-mcp-server.",
};
}
case "disconnect":
case "close":
return { tag: "disconnected" };
Expand All @@ -59,6 +65,11 @@ export class DebugResource extends ReactiveResource<
break;
case "errored":
result += `The user is not connected to a MongoDB cluster because of an error.\n`;
if (this.current.connectedAtlasCluster) {
result += `Attempted connecting to Atlas Cluster "${this.current.connectedAtlasCluster.clusterName}" in project with id "${this.current.connectedAtlasCluster.projectId}".\n`;
}

result += `The inferred authentication mechanism is "${this.current.connectionStringAuthType ?? "could-not-infer"}".\n`;
result += `<error>${this.current.errorReason}</error>`;
break;
case "connecting":
Expand Down
44 changes: 43 additions & 1 deletion tests/integration/common/connectionManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,49 @@ describeWithMongoDB("Connection Manager", (integration) => {
});

it("should notify that it failed connecting", () => {
expect(connectionManagerSpies["connection-errored"]).toHaveBeenCalled();
expect(connectionManagerSpies["connection-errored"]).toHaveBeenCalledWith({
tag: "errored",
connectedAtlasCluster: undefined,
connectionStringAuthType: "scram",
errorReason: "Unable to parse localhost:xxxxx with URL",
});
});

it("should be marked explicitly as connected", () => {
expect(connectionManager().currentConnectionState.tag).toEqual("errored");
});
});

describe("when fails to connect to a new atlas cluster", () => {
const atlas = {
username: "",
projectId: "",
clusterName: "My Atlas Cluster",
expiryDate: new Date(),
};

beforeEach(async () => {
try {
await connectionManager().connect({
connectionString: "mongodb://localhost:xxxxx",
atlas,
});
} 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"]).toHaveBeenCalledWith({
tag: "errored",
connectedAtlasCluster: atlas,
connectionStringAuthType: "scram",
errorReason: "Unable to parse localhost:xxxxx with URL",
});
});

it("should be marked explicitly as connected", () => {
Expand Down
43 changes: 42 additions & 1 deletion tests/unit/resources/common/debug.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,51 @@ describe("debug resource", () => {
});

it("should be disconnected and contain an error when an error event occurred", () => {
debugResource.reduceApply("connection-error", "Error message from the server");
debugResource.reduceApply("connection-error", {
tag: "errored",
errorReason: "Error message from the server",
});

const output = debugResource.toOutput();

expect(output).toContain(`The user is not connected to a MongoDB cluster because of an error.`);
expect(output).toContain(`<error>Error message from the server</error>`);
});

it("should show the inferred authentication type", () => {
debugResource.reduceApply("connection-error", {
tag: "errored",
connectionStringAuthType: "scram",
errorReason: "Error message from the server",
});

const output = debugResource.toOutput();

expect(output).toContain(`The user is not connected to a MongoDB cluster because of an error.`);
expect(output).toContain(`The inferred authentication mechanism is "scram".`);
expect(output).toContain(`<error>Error message from the server</error>`);
});

it("should show the atlas cluster information when provided", () => {
debugResource.reduceApply("connection-error", {
tag: "errored",
connectionStringAuthType: "scram",
errorReason: "Error message from the server",
connectedAtlasCluster: {
clusterName: "My Test Cluster",
projectId: "COFFEEFABADA",
username: "",
expiryDate: new Date(),
},
});

const output = debugResource.toOutput();

expect(output).toContain(`The user is not connected to a MongoDB cluster because of an error.`);
expect(output).toContain(
`Attempted connecting to Atlas Cluster "My Test Cluster" in project with id "COFFEEFABADA".`
);
expect(output).toContain(`The inferred authentication mechanism is "scram".`);
expect(output).toContain(`<error>Error message from the server</error>`);
});
});
Loading