Skip to content

Commit c531a17

Browse files
chore: remove generic from abstract ConnectionManager
1 parent 998140f commit c531a17

File tree

14 files changed

+266
-285
lines changed

14 files changed

+266
-285
lines changed

src/common/connectionManager.ts

Lines changed: 242 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import { EventEmitter } from "events";
2-
import type { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
2+
import type { MongoClientOptions } from "mongodb";
3+
import ConnectionString from "mongodb-connection-string-url";
4+
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
5+
import { type ConnectionInfo, generateConnectionInfoFromCliArgs } from "@mongosh/arg-parser";
6+
import type { DeviceId } from "../helpers/deviceId.js";
7+
import type { DriverOptions, UserConfig } from "./config.js";
8+
import { MongoDBError, ErrorCodes } from "./errors.js";
9+
import { type CompositeLogger, LogId } from "./logger.js";
10+
import { packageInfo } from "./packageInfo.js";
11+
import { type AppNameComponents, setAppNameParamIfMissing } from "../helpers/connectionOptions.js";
312

413
export interface AtlasClusterConnectionInfo {
514
username: string;
@@ -54,12 +63,12 @@ export interface ConnectionManagerEvents {
5463
"connection-errored": [ConnectionStateErrored];
5564
}
5665

57-
export interface MCPConnectParams {
66+
export interface ConnectionSettings {
5867
connectionString: string;
5968
atlas?: AtlasClusterConnectionInfo;
6069
}
6170

62-
export abstract class ConnectionManager<ConnectParams extends MCPConnectParams = MCPConnectParams> {
71+
export abstract class ConnectionManager {
6372
protected clientName: string = "unknown";
6473

6574
protected readonly _events = new EventEmitter<ConnectionManagerEvents>();
@@ -86,7 +95,236 @@ export abstract class ConnectionManager<ConnectParams extends MCPConnectParams =
8695
this.clientName = clientName;
8796
}
8897

89-
abstract connect(connectParams: ConnectParams): Promise<AnyConnectionState>;
98+
abstract connect(settings: ConnectionSettings): Promise<AnyConnectionState>;
9099

91100
abstract disconnect(): Promise<ConnectionStateDisconnected | ConnectionStateErrored>;
92101
}
102+
103+
export class MCPConnectionManager extends ConnectionManager {
104+
private deviceId: DeviceId;
105+
private bus: EventEmitter;
106+
107+
constructor(
108+
private userConfig: UserConfig,
109+
private driverOptions: DriverOptions,
110+
private logger: CompositeLogger,
111+
deviceId: DeviceId,
112+
bus?: EventEmitter
113+
) {
114+
super();
115+
this.bus = bus ?? new EventEmitter();
116+
this.bus.on("mongodb-oidc-plugin:auth-failed", this.onOidcAuthFailed.bind(this));
117+
this.bus.on("mongodb-oidc-plugin:auth-succeeded", this.onOidcAuthSucceeded.bind(this));
118+
this.deviceId = deviceId;
119+
this.clientName = "unknown";
120+
}
121+
122+
async connect(connectParams: ConnectionSettings): Promise<AnyConnectionState> {
123+
this._events.emit("connection-requested", this.state);
124+
125+
if (this.state.tag === "connected" || this.state.tag === "connecting") {
126+
await this.disconnect();
127+
}
128+
129+
let serviceProvider: NodeDriverServiceProvider;
130+
let connectionInfo: ConnectionInfo;
131+
132+
try {
133+
connectParams = { ...connectParams };
134+
const appNameComponents: AppNameComponents = {
135+
appName: `${packageInfo.mcpServerName} ${packageInfo.version}`,
136+
deviceId: this.deviceId.get(),
137+
clientName: this.clientName,
138+
};
139+
140+
connectParams.connectionString = await setAppNameParamIfMissing({
141+
connectionString: connectParams.connectionString,
142+
components: appNameComponents,
143+
});
144+
145+
connectionInfo = generateConnectionInfoFromCliArgs({
146+
...this.userConfig,
147+
...this.driverOptions,
148+
connectionSpecifier: connectParams.connectionString,
149+
});
150+
151+
if (connectionInfo.driverOptions.oidc) {
152+
connectionInfo.driverOptions.oidc.allowedFlows ??= ["auth-code"];
153+
connectionInfo.driverOptions.oidc.notifyDeviceFlow ??= this.onOidcNotifyDeviceFlow.bind(this);
154+
}
155+
156+
connectionInfo.driverOptions.proxy ??= { useEnvironmentVariableProxies: true };
157+
connectionInfo.driverOptions.applyProxyToOIDC ??= true;
158+
159+
serviceProvider = await NodeDriverServiceProvider.connect(
160+
connectionInfo.connectionString,
161+
{
162+
productDocsLink: "https://github.com/mongodb-js/mongodb-mcp-server/",
163+
productName: "MongoDB MCP",
164+
...connectionInfo.driverOptions,
165+
},
166+
undefined,
167+
this.bus
168+
);
169+
} catch (error: unknown) {
170+
const errorReason = error instanceof Error ? error.message : `${error as string}`;
171+
this.changeState("connection-errored", {
172+
tag: "errored",
173+
errorReason,
174+
connectedAtlasCluster: connectParams.atlas,
175+
});
176+
throw new MongoDBError(ErrorCodes.MisconfiguredConnectionString, errorReason);
177+
}
178+
179+
try {
180+
const connectionType = MCPConnectionManager.inferConnectionTypeFromSettings(
181+
this.userConfig,
182+
connectionInfo
183+
);
184+
if (connectionType.startsWith("oidc")) {
185+
void this.pingAndForget(serviceProvider);
186+
187+
return this.changeState("connection-requested", {
188+
tag: "connecting",
189+
connectedAtlasCluster: connectParams.atlas,
190+
serviceProvider,
191+
connectionStringAuthType: connectionType,
192+
oidcConnectionType: connectionType as OIDCConnectionAuthType,
193+
});
194+
}
195+
196+
await serviceProvider?.runCommand?.("admin", { hello: 1 });
197+
198+
return this.changeState("connection-succeeded", {
199+
tag: "connected",
200+
connectedAtlasCluster: connectParams.atlas,
201+
serviceProvider,
202+
connectionStringAuthType: connectionType,
203+
});
204+
} catch (error: unknown) {
205+
const errorReason = error instanceof Error ? error.message : `${error as string}`;
206+
this.changeState("connection-errored", {
207+
tag: "errored",
208+
errorReason,
209+
connectedAtlasCluster: connectParams.atlas,
210+
});
211+
throw new MongoDBError(ErrorCodes.NotConnectedToMongoDB, errorReason);
212+
}
213+
}
214+
215+
async disconnect(): Promise<ConnectionStateDisconnected | ConnectionStateErrored> {
216+
if (this.state.tag === "disconnected" || this.state.tag === "errored") {
217+
return this.state;
218+
}
219+
220+
if (this.state.tag === "connected" || this.state.tag === "connecting") {
221+
try {
222+
await this.state.serviceProvider?.close(true);
223+
} finally {
224+
this.changeState("connection-closed", {
225+
tag: "disconnected",
226+
});
227+
}
228+
}
229+
230+
return { tag: "disconnected" };
231+
}
232+
233+
private onOidcAuthFailed(error: unknown): void {
234+
if (this.state.tag === "connecting" && this.state.connectionStringAuthType?.startsWith("oidc")) {
235+
void this.disconnectOnOidcError(error);
236+
}
237+
}
238+
239+
private onOidcAuthSucceeded(): void {
240+
if (this.state.tag === "connecting" && this.state.connectionStringAuthType?.startsWith("oidc")) {
241+
this.changeState("connection-succeeded", { ...this.state, tag: "connected" });
242+
}
243+
244+
this.logger.info({
245+
id: LogId.oidcFlow,
246+
context: "mongodb-oidc-plugin:auth-succeeded",
247+
message: "Authenticated successfully.",
248+
});
249+
}
250+
251+
private onOidcNotifyDeviceFlow(flowInfo: { verificationUrl: string; userCode: string }): void {
252+
if (this.state.tag === "connecting" && this.state.connectionStringAuthType?.startsWith("oidc")) {
253+
this.changeState("connection-requested", {
254+
...this.state,
255+
tag: "connecting",
256+
connectionStringAuthType: "oidc-device-flow",
257+
oidcLoginUrl: flowInfo.verificationUrl,
258+
oidcUserCode: flowInfo.userCode,
259+
});
260+
}
261+
262+
this.logger.info({
263+
id: LogId.oidcFlow,
264+
context: "mongodb-oidc-plugin:notify-device-flow",
265+
message: "OIDC Flow changed automatically to device flow.",
266+
});
267+
}
268+
269+
static inferConnectionTypeFromSettings(
270+
config: UserConfig,
271+
settings: { connectionString: string }
272+
): ConnectionStringAuthType {
273+
const connString = new ConnectionString(settings.connectionString);
274+
const searchParams = connString.typedSearchParams<MongoClientOptions>();
275+
276+
switch (searchParams.get("authMechanism")) {
277+
case "MONGODB-OIDC": {
278+
if (config.transport === "stdio" && config.browser) {
279+
return "oidc-auth-flow";
280+
}
281+
282+
if (config.transport === "http" && config.httpHost === "127.0.0.1" && config.browser) {
283+
return "oidc-auth-flow";
284+
}
285+
286+
return "oidc-device-flow";
287+
}
288+
case "MONGODB-X509":
289+
return "x.509";
290+
case "GSSAPI":
291+
return "kerberos";
292+
case "PLAIN":
293+
if (searchParams.get("authSource") === "$external") {
294+
return "ldap";
295+
}
296+
return "scram";
297+
// default should catch also null, but eslint complains
298+
// about it.
299+
case null:
300+
default:
301+
return "scram";
302+
}
303+
}
304+
305+
private async pingAndForget(serviceProvider: NodeDriverServiceProvider): Promise<void> {
306+
try {
307+
await serviceProvider?.runCommand?.("admin", { hello: 1 });
308+
} catch (error: unknown) {
309+
this.logger.warning({
310+
id: LogId.oidcFlow,
311+
context: "pingAndForget",
312+
message: String(error),
313+
});
314+
}
315+
}
316+
317+
private async disconnectOnOidcError(error: unknown): Promise<void> {
318+
try {
319+
await this.disconnect();
320+
} catch (error: unknown) {
321+
this.logger.warning({
322+
id: LogId.oidcFlow,
323+
context: "disconnectOnOidcError",
324+
message: String(error),
325+
});
326+
} finally {
327+
this.changeState("connection-errored", { tag: "errored", errorReason: String(error) });
328+
}
329+
}
330+
}

0 commit comments

Comments
 (0)