Skip to content

Commit 437a731

Browse files
Merge remote-tracking branch 'origin/main' into chore/MCP-131-injectable-connection-manager
2 parents b079a6d + a06b443 commit 437a731

File tree

6 files changed

+152
-57
lines changed

6 files changed

+152
-57
lines changed

src/common/connectionManager.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,11 @@ export type AnyConnectionState =
6161
| ConnectionStateErrored;
6262

6363
export interface ConnectionManagerEvents {
64-
"connection-requested": [AnyConnectionState];
65-
"connection-succeeded": [ConnectionStateConnected];
66-
"connection-timed-out": [ConnectionStateErrored];
67-
"connection-closed": [ConnectionStateDisconnected];
68-
"connection-errored": [ConnectionStateErrored];
64+
"connection-request": [AnyConnectionState];
65+
"connection-success": [ConnectionStateConnected];
66+
"connection-time-out": [ConnectionStateErrored];
67+
"connection-close": [ConnectionStateDisconnected];
68+
"connection-error": [ConnectionStateErrored];
6969
}
7070

7171
/**
@@ -136,14 +136,15 @@ export class MCPConnectionManager extends ConnectionManager {
136136
}
137137

138138
async connect(settings: ConnectionSettings): Promise<AnyConnectionState> {
139-
this._events.emit("connection-requested", this.currentConnectionState);
139+
this._events.emit("connection-request", this.currentConnectionState);
140140

141141
if (this.currentConnectionState.tag === "connected" || this.currentConnectionState.tag === "connecting") {
142142
await this.disconnect();
143143
}
144144

145145
let serviceProvider: NodeDriverServiceProvider;
146146
let connectionInfo: ConnectionInfo;
147+
let connectionStringAuthType: ConnectionStringAuthType = "scram";
147148

148149
try {
149150
settings = { ...settings };
@@ -172,6 +173,11 @@ export class MCPConnectionManager extends ConnectionManager {
172173
connectionInfo.driverOptions.proxy ??= { useEnvironmentVariableProxies: true };
173174
connectionInfo.driverOptions.applyProxyToOIDC ??= true;
174175

176+
connectionStringAuthType = MCPConnectionManager.inferConnectionTypeFromSettings(
177+
this.userConfig,
178+
connectionInfo
179+
);
180+
175181
serviceProvider = await NodeDriverServiceProvider.connect(
176182
connectionInfo.connectionString,
177183
{
@@ -184,9 +190,10 @@ export class MCPConnectionManager extends ConnectionManager {
184190
);
185191
} catch (error: unknown) {
186192
const errorReason = error instanceof Error ? error.message : `${error as string}`;
187-
this.changeState("connection-errored", {
193+
this.changeState("connection-error", {
188194
tag: "errored",
189195
errorReason,
196+
connectionStringAuthType,
190197
connectedAtlasCluster: settings.atlas,
191198
});
192199
throw new MongoDBError(ErrorCodes.MisconfiguredConnectionString, errorReason);
@@ -200,7 +207,7 @@ export class MCPConnectionManager extends ConnectionManager {
200207
if (connectionType.startsWith("oidc")) {
201208
void this.pingAndForget(serviceProvider);
202209

203-
return this.changeState("connection-requested", {
210+
return this.changeState("connection-request", {
204211
tag: "connecting",
205212
connectedAtlasCluster: settings.atlas,
206213
serviceProvider,
@@ -211,17 +218,18 @@ export class MCPConnectionManager extends ConnectionManager {
211218

212219
await serviceProvider?.runCommand?.("admin", { hello: 1 });
213220

214-
return this.changeState("connection-succeeded", {
221+
return this.changeState("connection-success", {
215222
tag: "connected",
216223
connectedAtlasCluster: settings.atlas,
217224
serviceProvider,
218225
connectionStringAuthType: connectionType,
219226
});
220227
} catch (error: unknown) {
221228
const errorReason = error instanceof Error ? error.message : `${error as string}`;
222-
this.changeState("connection-errored", {
229+
this.changeState("connection-error", {
223230
tag: "errored",
224231
errorReason,
232+
connectionStringAuthType,
225233
connectedAtlasCluster: settings.atlas,
226234
});
227235
throw new MongoDBError(ErrorCodes.NotConnectedToMongoDB, errorReason);
@@ -237,7 +245,7 @@ export class MCPConnectionManager extends ConnectionManager {
237245
try {
238246
await this.currentConnectionState.serviceProvider?.close(true);
239247
} finally {
240-
this.changeState("connection-closed", {
248+
this.changeState("connection-close", {
241249
tag: "disconnected",
242250
});
243251
}
@@ -260,7 +268,7 @@ export class MCPConnectionManager extends ConnectionManager {
260268
this.currentConnectionState.tag === "connecting" &&
261269
this.currentConnectionState.connectionStringAuthType?.startsWith("oidc")
262270
) {
263-
this.changeState("connection-succeeded", { ...this.currentConnectionState, tag: "connected" });
271+
this.changeState("connection-success", { ...this.currentConnectionState, tag: "connected" });
264272
}
265273

266274
this.logger.info({
@@ -275,7 +283,7 @@ export class MCPConnectionManager extends ConnectionManager {
275283
this.currentConnectionState.tag === "connecting" &&
276284
this.currentConnectionState.connectionStringAuthType?.startsWith("oidc")
277285
) {
278-
this.changeState("connection-requested", {
286+
this.changeState("connection-request", {
279287
...this.currentConnectionState,
280288
tag: "connecting",
281289
connectionStringAuthType: "oidc-device-flow",
@@ -349,7 +357,7 @@ export class MCPConnectionManager extends ConnectionManager {
349357
message: String(error),
350358
});
351359
} finally {
352-
this.changeState("connection-errored", { tag: "errored", errorReason: String(error) });
360+
this.changeState("connection-error", { tag: "errored", errorReason: String(error) });
353361
}
354362
}
355363
}

src/common/session.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
ConnectionManager,
1111
ConnectionSettings,
1212
ConnectionStateConnected,
13+
ConnectionStateErrored,
1314
} from "./connectionManager.js";
1415
import type { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
1516
import { ErrorCodes, MongoDBError } from "./errors.js";
@@ -28,7 +29,7 @@ export type SessionEvents = {
2829
connect: [];
2930
close: [];
3031
disconnect: [];
31-
"connection-error": [string];
32+
"connection-error": [ConnectionStateErrored];
3233
};
3334

3435
export class Session extends EventEmitter<SessionEvents> {
@@ -66,14 +67,10 @@ export class Session extends EventEmitter<SessionEvents> {
6667
this.apiClient = new ApiClient({ baseUrl: apiBaseUrl, credentials }, logger);
6768
this.exportsManager = exportsManager;
6869
this.connectionManager = connectionManager;
69-
this.connectionManager.events.on("connection-succeeded", () => this.emit("connect"));
70-
this.connectionManager.events.on("connection-timed-out", (error) =>
71-
this.emit("connection-error", error.errorReason)
72-
);
73-
this.connectionManager.events.on("connection-closed", () => this.emit("disconnect"));
74-
this.connectionManager.events.on("connection-errored", (error) =>
75-
this.emit("connection-error", error.errorReason)
76-
);
70+
this.connectionManager.events.on("connection-success", () => this.emit("connect"));
71+
this.connectionManager.events.on("connection-time-out", (error) => this.emit("connection-error", error));
72+
this.connectionManager.events.on("connection-close", () => this.emit("disconnect"));
73+
this.connectionManager.events.on("connection-error", (error) => this.emit("connection-error", error));
7774
}
7875

7976
setMcpClient(mcpClient: Implementation | undefined): void {
@@ -140,13 +137,7 @@ export class Session extends EventEmitter<SessionEvents> {
140137
}
141138

142139
async connectToMongoDB(settings: ConnectionSettings): Promise<void> {
143-
try {
144-
await this.connectionManager.connect({ ...settings });
145-
} catch (error: unknown) {
146-
const message = error instanceof Error ? error.message : (error as string);
147-
this.emit("connection-error", message);
148-
throw error;
149-
}
140+
await this.connectionManager.connect({ ...settings });
150141
}
151142

152143
get isConnectedToMongoDB(): boolean {

src/resources/common/debug.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { ReactiveResource } from "../resource.js";
22
import type { Telemetry } from "../../telemetry/telemetry.js";
33
import type { Session, UserConfig } from "../../lib.js";
4+
import type { AtlasClusterConnectionInfo, ConnectionStateErrored } from "../../common/connectionManager.js";
45

56
type ConnectionStateDebuggingInformation = {
67
readonly tag: "connected" | "connecting" | "disconnected" | "errored";
78
readonly connectionStringAuthType?: "scram" | "ldap" | "kerberos" | "oidc-auth-flow" | "oidc-device-flow" | "x.509";
8-
readonly oidcLoginUrl?: string;
9-
readonly oidcUserCode?: string;
109
readonly errorReason?: string;
10+
readonly connectedAtlasCluster?: AtlasClusterConnectionInfo;
1111
};
1212

1313
export class DebugResource extends ReactiveResource<
@@ -35,15 +35,21 @@ export class DebugResource extends ReactiveResource<
3535
}
3636
reduce(
3737
eventName: "connect" | "disconnect" | "close" | "connection-error",
38-
event: string | undefined
38+
event: ConnectionStateErrored | undefined
3939
): ConnectionStateDebuggingInformation {
40-
void event;
41-
4240
switch (eventName) {
4341
case "connect":
4442
return { tag: "connected" };
45-
case "connection-error":
46-
return { tag: "errored", errorReason: event };
43+
case "connection-error": {
44+
return {
45+
tag: "errored",
46+
connectionStringAuthType: event?.connectionStringAuthType,
47+
connectedAtlasCluster: event?.connectedAtlasCluster,
48+
errorReason:
49+
event?.errorReason ??
50+
"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.",
51+
};
52+
}
4753
case "disconnect":
4854
case "close":
4955
return { tag: "disconnected" };
@@ -59,6 +65,13 @@ export class DebugResource extends ReactiveResource<
5965
break;
6066
case "errored":
6167
result += `The user is not connected to a MongoDB cluster because of an error.\n`;
68+
if (this.current.connectedAtlasCluster) {
69+
result += `Attempted connecting to Atlas Cluster "${this.current.connectedAtlasCluster.clusterName}" in project with id "${this.current.connectedAtlasCluster.projectId}".\n`;
70+
}
71+
72+
if (this.current.connectionStringAuthType !== undefined) {
73+
result += `The inferred authentication mechanism is "${this.current.connectionStringAuthType}".\n`;
74+
}
6275
result += `<error>${this.current.errorReason}</error>`;
6376
break;
6477
case "connecting":

tests/integration/common/connectionManager.oidc.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ describe.skipIf(process.platform !== "linux")("ConnectionManager OIDC Tests", as
133133
await connectionManager.disconnect();
134134
// for testing, force disconnecting AND setting the connection to closed to reset the
135135
// state of the connection manager
136-
connectionManager.changeState("connection-closed", { tag: "disconnected" });
136+
connectionManager.changeState("connection-close", { tag: "disconnected" });
137137

138138
await integration.connectMcpClient();
139139
}, DEFAULT_TIMEOUT);

tests/integration/common/connectionManager.test.ts

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,27 @@ describeWithMongoDB("Connection Manager", (integration) => {
2020
await connectionManager().disconnect();
2121
// for testing, force disconnecting AND setting the connection to closed to reset the
2222
// state of the connection manager
23-
connectionManager().changeState("connection-closed", { tag: "disconnected" });
23+
connectionManager().changeState("connection-close", { tag: "disconnected" });
2424
});
2525

2626
describe("when successfully connected", () => {
2727
type ConnectionManagerSpies = {
28-
"connection-requested": (event: ConnectionManagerEvents["connection-requested"][0]) => void;
29-
"connection-succeeded": (event: ConnectionManagerEvents["connection-succeeded"][0]) => void;
30-
"connection-timed-out": (event: ConnectionManagerEvents["connection-timed-out"][0]) => void;
31-
"connection-closed": (event: ConnectionManagerEvents["connection-closed"][0]) => void;
32-
"connection-errored": (event: ConnectionManagerEvents["connection-errored"][0]) => void;
28+
"connection-request": (event: ConnectionManagerEvents["connection-request"][0]) => void;
29+
"connection-success": (event: ConnectionManagerEvents["connection-success"][0]) => void;
30+
"connection-time-out": (event: ConnectionManagerEvents["connection-time-out"][0]) => void;
31+
"connection-close": (event: ConnectionManagerEvents["connection-close"][0]) => void;
32+
"connection-error": (event: ConnectionManagerEvents["connection-error"][0]) => void;
3333
};
3434

3535
let connectionManagerSpies: ConnectionManagerSpies;
3636

3737
beforeEach(async () => {
3838
connectionManagerSpies = {
39-
"connection-requested": vi.fn(),
40-
"connection-succeeded": vi.fn(),
41-
"connection-timed-out": vi.fn(),
42-
"connection-closed": vi.fn(),
43-
"connection-errored": vi.fn(),
39+
"connection-request": vi.fn(),
40+
"connection-success": vi.fn(),
41+
"connection-time-out": vi.fn(),
42+
"connection-close": vi.fn(),
43+
"connection-error": vi.fn(),
4444
};
4545

4646
for (const [event, spy] of Object.entries(connectionManagerSpies)) {
@@ -63,11 +63,11 @@ describeWithMongoDB("Connection Manager", (integration) => {
6363
});
6464

6565
it("should notify that the connection was requested", () => {
66-
expect(connectionManagerSpies["connection-requested"]).toHaveBeenCalledOnce();
66+
expect(connectionManagerSpies["connection-request"]).toHaveBeenCalledOnce();
6767
});
6868

6969
it("should notify that the connection was successful", () => {
70-
expect(connectionManagerSpies["connection-succeeded"]).toHaveBeenCalledOnce();
70+
expect(connectionManagerSpies["connection-success"]).toHaveBeenCalledOnce();
7171
});
7272

7373
describe("when disconnects", () => {
@@ -76,7 +76,7 @@ describeWithMongoDB("Connection Manager", (integration) => {
7676
});
7777

7878
it("should notify that it was disconnected before connecting", () => {
79-
expect(connectionManagerSpies["connection-closed"]).toHaveBeenCalled();
79+
expect(connectionManagerSpies["connection-close"]).toHaveBeenCalled();
8080
});
8181

8282
it("should be marked explicitly as disconnected", () => {
@@ -92,11 +92,11 @@ describeWithMongoDB("Connection Manager", (integration) => {
9292
});
9393

9494
it("should notify that it was disconnected before connecting", () => {
95-
expect(connectionManagerSpies["connection-closed"]).toHaveBeenCalled();
95+
expect(connectionManagerSpies["connection-close"]).toHaveBeenCalled();
9696
});
9797

9898
it("should notify that it was connected again", () => {
99-
expect(connectionManagerSpies["connection-succeeded"]).toHaveBeenCalled();
99+
expect(connectionManagerSpies["connection-success"]).toHaveBeenCalled();
100100
});
101101

102102
it("should be marked explicitly as connected", () => {
@@ -116,11 +116,53 @@ describeWithMongoDB("Connection Manager", (integration) => {
116116
});
117117

118118
it("should notify that it was disconnected before connecting", () => {
119-
expect(connectionManagerSpies["connection-closed"]).toHaveBeenCalled();
119+
expect(connectionManagerSpies["connection-close"]).toHaveBeenCalled();
120120
});
121121

122122
it("should notify that it failed connecting", () => {
123-
expect(connectionManagerSpies["connection-errored"]).toHaveBeenCalled();
123+
expect(connectionManagerSpies["connection-error"]).toHaveBeenCalledWith({
124+
tag: "errored",
125+
connectedAtlasCluster: undefined,
126+
connectionStringAuthType: "scram",
127+
errorReason: "Unable to parse localhost:xxxxx with URL",
128+
});
129+
});
130+
131+
it("should be marked explicitly as connected", () => {
132+
expect(connectionManager().currentConnectionState.tag).toEqual("errored");
133+
});
134+
});
135+
136+
describe("when fails to connect to a new atlas cluster", () => {
137+
const atlas = {
138+
username: "",
139+
projectId: "",
140+
clusterName: "My Atlas Cluster",
141+
expiryDate: new Date(),
142+
};
143+
144+
beforeEach(async () => {
145+
try {
146+
await connectionManager().connect({
147+
connectionString: "mongodb://localhost:xxxxx",
148+
atlas,
149+
});
150+
} catch (_error: unknown) {
151+
void _error;
152+
}
153+
});
154+
155+
it("should notify that it was disconnected before connecting", () => {
156+
expect(connectionManagerSpies["connection-close"]).toHaveBeenCalled();
157+
});
158+
159+
it("should notify that it failed connecting", () => {
160+
expect(connectionManagerSpies["connection-error"]).toHaveBeenCalledWith({
161+
tag: "errored",
162+
connectedAtlasCluster: atlas,
163+
connectionStringAuthType: "scram",
164+
errorReason: "Unable to parse localhost:xxxxx with URL",
165+
});
124166
});
125167

126168
it("should be marked explicitly as connected", () => {

0 commit comments

Comments
 (0)