• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mongodb-js / mongodb-mcp-server / 17433820852

03 Sep 2025 12:43PM UTC coverage: 80.976% (-0.3%) from 81.234%
17433820852

Pull #500

github

web-flow
Merge c2edf9f26 into 94dfc08cc
Pull Request #500: feat: add more details about atlas connect flow - MCP-124

901 of 1201 branches covered (75.02%)

Branch coverage included in aggregate %.

42 of 53 new or added lines in 2 files covered. (79.25%)

12 existing lines in 3 files now uncovered.

4594 of 5585 relevant lines covered (82.26%)

44.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.94
/src/common/connectionManager.ts
1
import { EventEmitter } from "events";
2✔
2
import type { MongoClientOptions } from "mongodb";
3
import ConnectionString from "mongodb-connection-string-url";
2✔
4
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
2✔
5
import { type ConnectionInfo, generateConnectionInfoFromCliArgs } from "@mongosh/arg-parser";
2✔
6
import type { DeviceId } from "../helpers/deviceId.js";
7
import { defaultDriverOptions, setupDriverConfig, type DriverOptions, type UserConfig } from "./config.js";
2✔
8
import { MongoDBError, ErrorCodes } from "./errors.js";
2✔
9
import { type LoggerBase, LogId } from "./logger.js";
2✔
10
import { packageInfo } from "./packageInfo.js";
2✔
11
import { type AppNameComponents, setAppNameParamIfMissing } from "../helpers/connectionOptions.js";
2✔
12

13
export interface AtlasClusterConnectionInfo {
14
    username: string;
15
    projectId: string;
16
    clusterName: string;
17
    expiryDate: Date;
18
}
19

20
export interface ConnectionSettings {
21
    connectionString: string;
22
    atlas?: AtlasClusterConnectionInfo;
23
}
24

25
type ConnectionTag = "connected" | "connecting" | "disconnected" | "errored";
26
type OIDCConnectionAuthType = "oidc-auth-flow" | "oidc-device-flow";
27
export type ConnectionStringAuthType = "scram" | "ldap" | "kerberos" | OIDCConnectionAuthType | "x.509";
28

29
export interface ConnectionState {
30
    tag: ConnectionTag;
31
    connectionStringAuthType?: ConnectionStringAuthType;
32
    connectedAtlasCluster?: AtlasClusterConnectionInfo;
33
}
34

35
export interface ConnectionStateConnected extends ConnectionState {
36
    tag: "connected";
37
    serviceProvider: NodeDriverServiceProvider;
38
}
39

40
export interface ConnectionStateConnecting extends ConnectionState {
41
    tag: "connecting";
42
    serviceProvider: NodeDriverServiceProvider;
43
    oidcConnectionType: OIDCConnectionAuthType;
44
    oidcLoginUrl?: string;
45
    oidcUserCode?: string;
46
}
47

48
export interface ConnectionStateDisconnected extends ConnectionState {
49
    tag: "disconnected";
50
}
51

52
export interface ConnectionStateErrored extends ConnectionState {
53
    tag: "errored";
54
    errorReason: string;
55
}
56

57
export type AnyConnectionState =
58
    | ConnectionStateConnected
59
    | ConnectionStateConnecting
60
    | ConnectionStateDisconnected
61
    | ConnectionStateErrored;
62

63
export interface ConnectionManagerEvents {
64
    "connection-request": [AnyConnectionState];
65
    "connection-success": [ConnectionStateConnected];
66
    "connection-time-out": [ConnectionStateErrored];
67
    "connection-close": [ConnectionStateDisconnected];
68
    "connection-error": [ConnectionStateErrored];
69
}
70

71
/**
72
 * For a few tests, we need the changeState method to force a connection state
73
 * which is we have this type to typecast the actual ConnectionManager with
74
 * public changeState (only to make TS happy).
75
 */
76
export type TestConnectionManager = ConnectionManager & {
77
    changeState<Event extends keyof ConnectionManagerEvents, State extends ConnectionManagerEvents[Event][0]>(
78
        event: Event,
79
        newState: State
80
    ): State;
81
};
82

83
export abstract class ConnectionManager {
2✔
84
    protected clientName: string;
85
    protected readonly _events;
86
    readonly events: Pick<EventEmitter<ConnectionManagerEvents>, "on" | "off" | "once">;
87
    private state: AnyConnectionState;
88

89
    constructor() {
2✔
90
        this.clientName = "unknown";
69✔
91
        this.events = this._events = new EventEmitter<ConnectionManagerEvents>();
69✔
92
        this.state = { tag: "disconnected" };
69✔
93
    }
69✔
94

95
    get currentConnectionState(): AnyConnectionState {
2✔
96
        return this.state;
4,103✔
97
    }
4,103✔
98

99
    protected changeState<Event extends keyof ConnectionManagerEvents, State extends ConnectionManagerEvents[Event][0]>(
2✔
100
        event: Event,
468✔
101
        newState: State
468✔
102
    ): State {
468✔
103
        this.state = newState;
468✔
104
        // TypeScript doesn't seem to be happy with the spread operator and generics
105
        // eslint-disable-next-line
106
        this._events.emit(event, ...([newState] as any));
468✔
107
        return newState;
468✔
108
    }
468✔
109

110
    setClientName(clientName: string): void {
2✔
111
        this.clientName = clientName;
62✔
112
    }
62✔
113

114
    abstract connect(settings: ConnectionSettings): Promise<AnyConnectionState>;
115

116
    abstract disconnect(): Promise<ConnectionStateDisconnected | ConnectionStateErrored>;
117
}
2✔
118

119
export class MCPConnectionManager extends ConnectionManager {
2✔
120
    private deviceId: DeviceId;
121
    private bus: EventEmitter;
122

123
    constructor(
2✔
124
        private userConfig: UserConfig,
69✔
125
        private driverOptions: DriverOptions,
69✔
126
        private logger: LoggerBase,
69✔
127
        deviceId: DeviceId,
69✔
128
        bus?: EventEmitter
69✔
129
    ) {
69✔
130
        super();
69✔
131
        this.bus = bus ?? new EventEmitter();
69✔
132
        this.bus.on("mongodb-oidc-plugin:auth-failed", this.onOidcAuthFailed.bind(this));
69✔
133
        this.bus.on("mongodb-oidc-plugin:auth-succeeded", this.onOidcAuthSucceeded.bind(this));
69✔
134
        this.deviceId = deviceId;
69✔
135
    }
69✔
136

137
    async connect(settings: ConnectionSettings): Promise<AnyConnectionState> {
2✔
138
        this._events.emit("connection-request", this.currentConnectionState);
238✔
139

140
        if (this.currentConnectionState.tag === "connected" || this.currentConnectionState.tag === "connecting") {
238✔
141
            await this.disconnect();
13✔
142
        }
13✔
143

144
        let serviceProvider: NodeDriverServiceProvider;
238✔
145
        let connectionInfo: ConnectionInfo;
238✔
146
        let connectionStringAuthType: ConnectionStringAuthType = "scram";
238✔
147

148
        try {
238✔
149
            settings = { ...settings };
238✔
150
            const appNameComponents: AppNameComponents = {
238✔
151
                appName: `${packageInfo.mcpServerName} ${packageInfo.version}`,
238✔
152
                deviceId: this.deviceId.get(),
238✔
153
                clientName: this.clientName,
238✔
154
            };
238✔
155

156
            settings.connectionString = await setAppNameParamIfMissing({
238✔
157
                connectionString: settings.connectionString,
238✔
158
                components: appNameComponents,
238✔
159
            });
238✔
160

161
            connectionInfo = generateConnectionInfoFromCliArgs({
238✔
162
                ...this.userConfig,
238✔
163
                ...this.driverOptions,
238✔
164
                connectionSpecifier: settings.connectionString,
238✔
165
            });
238✔
166

167
            if (connectionInfo.driverOptions.oidc) {
238!
168
                connectionInfo.driverOptions.oidc.allowedFlows ??= ["auth-code"];
7✔
169
                connectionInfo.driverOptions.oidc.notifyDeviceFlow ??= this.onOidcNotifyDeviceFlow.bind(this);
7✔
170
            }
7✔
171

172
            connectionInfo.driverOptions.proxy ??= { useEnvironmentVariableProxies: true };
238✔
173
            connectionInfo.driverOptions.applyProxyToOIDC ??= true;
238✔
174

175
            connectionStringAuthType = MCPConnectionManager.inferConnectionTypeFromSettings(
238✔
176
                this.userConfig,
238✔
177
                connectionInfo
238✔
178
            );
238✔
179

180
            serviceProvider = await NodeDriverServiceProvider.connect(
238✔
181
                connectionInfo.connectionString,
238✔
182
                {
238✔
183
                    productDocsLink: "https://github.com/mongodb-js/mongodb-mcp-server/",
238✔
184
                    productName: "MongoDB MCP",
238✔
185
                    ...connectionInfo.driverOptions,
238✔
186
                },
238✔
187
                undefined,
238✔
188
                this.bus
238✔
189
            );
238✔
190
        } catch (error: unknown) {
234!
191
            const errorReason = error instanceof Error ? error.message : `${error as string}`;
12!
192
            this.changeState("connection-error", {
12✔
193
                tag: "errored",
12✔
194
                errorReason,
12✔
195
                connectionStringAuthType,
12✔
196
                connectedAtlasCluster: settings.atlas,
12✔
197
            });
12✔
198
            throw new MongoDBError(ErrorCodes.MisconfiguredConnectionString, errorReason);
12✔
199
        }
12✔
200

201
        try {
226✔
202
            const connectionType = MCPConnectionManager.inferConnectionTypeFromSettings(
226✔
203
                this.userConfig,
226✔
204
                connectionInfo
226✔
205
            );
226✔
206
            if (connectionType.startsWith("oidc")) {
238!
207
                void this.pingAndForget(serviceProvider);
7✔
208

209
                return this.changeState("connection-request", {
7✔
210
                    tag: "connecting",
7✔
211
                    connectedAtlasCluster: settings.atlas,
7✔
212
                    serviceProvider,
7✔
213
                    connectionStringAuthType: connectionType,
7✔
214
                    oidcConnectionType: connectionType as OIDCConnectionAuthType,
7✔
215
                });
7✔
216
            }
7✔
217

218
            await serviceProvider?.runCommand?.("admin", { hello: 1 });
219✔
219

220
            return this.changeState("connection-success", {
202✔
221
                tag: "connected",
202✔
222
                connectedAtlasCluster: settings.atlas,
202✔
223
                serviceProvider,
202✔
224
                connectionStringAuthType: connectionType,
202✔
225
            });
202✔
226
        } catch (error: unknown) {
231!
227
            const errorReason = error instanceof Error ? error.message : `${error as string}`;
17!
228
            this.changeState("connection-error", {
17✔
229
                tag: "errored",
17✔
230
                errorReason,
17✔
231
                connectionStringAuthType,
17✔
232
                connectedAtlasCluster: settings.atlas,
17✔
233
            });
17✔
234
            throw new MongoDBError(ErrorCodes.NotConnectedToMongoDB, errorReason);
17✔
235
        }
17✔
236
    }
238✔
237

238
    async disconnect(): Promise<ConnectionStateDisconnected | ConnectionStateErrored> {
2✔
239
        if (this.currentConnectionState.tag === "disconnected" || this.currentConnectionState.tag === "errored") {
490✔
240
            return this.currentConnectionState;
291✔
241
        }
291✔
242

243
        if (this.currentConnectionState.tag === "connected" || this.currentConnectionState.tag === "connecting") {
490!
244
            try {
199✔
245
                await this.currentConnectionState.serviceProvider?.close(true);
199✔
246
            } finally {
199✔
247
                this.changeState("connection-close", {
199✔
248
                    tag: "disconnected",
199✔
249
                });
199✔
250
            }
199✔
251
        }
199✔
252

253
        return { tag: "disconnected" };
199✔
254
    }
490✔
255

256
    private onOidcAuthFailed(error: unknown): void {
2✔
257
        if (
×
258
            this.currentConnectionState.tag === "connecting" &&
×
259
            this.currentConnectionState.connectionStringAuthType?.startsWith("oidc")
×
260
        ) {
×
261
            void this.disconnectOnOidcError(error);
×
262
        }
×
263
    }
×
264

265
    private onOidcAuthSucceeded(): void {
2✔
266
        if (
9✔
267
            this.currentConnectionState.tag === "connecting" &&
9✔
268
            this.currentConnectionState.connectionStringAuthType?.startsWith("oidc")
7✔
269
        ) {
9✔
270
            this.changeState("connection-success", { ...this.currentConnectionState, tag: "connected" });
7✔
271
        }
7✔
272

273
        this.logger.info({
9✔
274
            id: LogId.oidcFlow,
9✔
275
            context: "mongodb-oidc-plugin:auth-succeeded",
9✔
276
            message: "Authenticated successfully.",
9✔
277
        });
9✔
278
    }
9✔
279

280
    private onOidcNotifyDeviceFlow(flowInfo: { verificationUrl: string; userCode: string }): void {
2✔
281
        if (
1✔
282
            this.currentConnectionState.tag === "connecting" &&
1✔
283
            this.currentConnectionState.connectionStringAuthType?.startsWith("oidc")
1✔
284
        ) {
1✔
285
            this.changeState("connection-request", {
1✔
286
                ...this.currentConnectionState,
1✔
287
                tag: "connecting",
1✔
288
                connectionStringAuthType: "oidc-device-flow",
1✔
289
                oidcLoginUrl: flowInfo.verificationUrl,
1✔
290
                oidcUserCode: flowInfo.userCode,
1✔
291
            });
1✔
292
        }
1✔
293

294
        this.logger.info({
1✔
295
            id: LogId.oidcFlow,
1✔
296
            context: "mongodb-oidc-plugin:notify-device-flow",
1✔
297
            message: "OIDC Flow changed automatically to device flow.",
1✔
298
        });
1✔
299
    }
1✔
300

301
    static inferConnectionTypeFromSettings(
2✔
302
        config: UserConfig,
473✔
303
        settings: { connectionString: string }
473✔
304
    ): ConnectionStringAuthType {
473✔
305
        const connString = new ConnectionString(settings.connectionString);
473✔
306
        const searchParams = connString.typedSearchParams<MongoClientOptions>();
473✔
307

308
        switch (searchParams.get("authMechanism")) {
473✔
309
            case "MONGODB-OIDC": {
473!
310
                if (config.transport === "stdio" && config.browser) {
18✔
311
                    return "oidc-auth-flow";
13✔
312
                }
13✔
313

314
                if (config.transport === "http" && config.httpHost === "127.0.0.1" && config.browser) {
18✔
315
                    return "oidc-auth-flow";
1✔
316
                }
1✔
317

318
                return "oidc-device-flow";
4✔
319
            }
4✔
320
            case "MONGODB-X509":
473!
321
                return "x.509";
1✔
322
            case "GSSAPI":
473!
323
                return "kerberos";
1✔
324
            case "PLAIN":
473!
325
                if (searchParams.get("authSource") === "$external") {
2✔
326
                    return "ldap";
1✔
327
                }
1✔
328
                return "scram";
1✔
329
            // default should catch also null, but eslint complains
330
            // about it.
331
            case null:
473✔
332
            default:
473✔
333
                return "scram";
451✔
334
        }
473✔
335
    }
473✔
336

337
    private async pingAndForget(serviceProvider: NodeDriverServiceProvider): Promise<void> {
2✔
338
        try {
7✔
339
            await serviceProvider?.runCommand?.("admin", { hello: 1 });
7✔
340
        } catch (error: unknown) {
7!
UNCOV
341
            this.logger.warning({
×
UNCOV
342
                id: LogId.oidcFlow,
×
UNCOV
343
                context: "pingAndForget",
×
UNCOV
344
                message: String(error),
×
UNCOV
345
            });
×
UNCOV
346
        }
×
347
    }
7✔
348

349
    private async disconnectOnOidcError(error: unknown): Promise<void> {
2✔
350
        try {
×
351
            await this.disconnect();
×
352
        } catch (error: unknown) {
×
353
            this.logger.warning({
×
354
                id: LogId.oidcFlow,
×
355
                context: "disconnectOnOidcError",
×
356
                message: String(error),
×
357
            });
×
358
        } finally {
×
359
            this.changeState("connection-error", { tag: "errored", errorReason: String(error) });
×
360
        }
×
361
    }
×
362
}
2✔
363

364
/**
365
 * Consumers of MCP server library have option to bring their own connection
366
 * management if they need to. To support that, we enable injecting connection
367
 * manager implementation through a factory function.
368
 */
369
export type ConnectionManagerFactoryFn = (createParams: {
370
    logger: LoggerBase;
371
    deviceId: DeviceId;
372
    userConfig: UserConfig;
373
}) => Promise<ConnectionManager>;
374

375
export const createMCPConnectionManager: ConnectionManagerFactoryFn = ({ logger, deviceId, userConfig }) => {
2✔
376
    const driverOptions = setupDriverConfig({
6✔
377
        config: userConfig,
6✔
378
        defaults: defaultDriverOptions,
6✔
379
    });
6✔
380

381
    return Promise.resolve(new MCPConnectionManager(userConfig, driverOptions, logger, deviceId));
6✔
382
};
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc