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

mongodb-js / mongodb-mcp-server / 18552067365

16 Oct 2025 06:13AM UTC coverage: 82.527% (+0.3%) from 82.215%
18552067365

push

github

web-flow
chore: extend accuracy tests to support custom user and cluster config (#655)

1189 of 1575 branches covered (75.49%)

Branch coverage included in aggregate %.

5806 of 6901 relevant lines covered (84.13%)

70.79 hits per line

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

87.5
/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 class ConnectionStateConnected implements ConnectionState {
2✔
36
    public tag = "connected" as const;
2✔
37

38
    constructor(
2✔
39
        public serviceProvider: NodeDriverServiceProvider,
288✔
40
        public connectionStringAuthType?: ConnectionStringAuthType,
288✔
41
        public connectedAtlasCluster?: AtlasClusterConnectionInfo
288✔
42
    ) {}
288✔
43

44
    private _isSearchSupported?: boolean;
45

46
    public async isSearchSupported(): Promise<boolean> {
2✔
47
        if (this._isSearchSupported === undefined) {
6✔
48
            try {
6✔
49
                const dummyDatabase = "test";
6✔
50
                const dummyCollection = "test";
6✔
51
                // If a cluster supports search indexes, the call below will succeed
52
                // with a cursor otherwise will throw an Error
53
                await this.serviceProvider.getSearchIndexes(dummyDatabase, dummyCollection);
6✔
54
                this._isSearchSupported = true;
4✔
55
            } catch {
5✔
56
                this._isSearchSupported = false;
2✔
57
            }
2✔
58
        }
6✔
59

60
        return this._isSearchSupported;
6✔
61
    }
6✔
62
}
2✔
63

64
export interface ConnectionStateConnecting extends ConnectionState {
65
    tag: "connecting";
66
    serviceProvider: Promise<NodeDriverServiceProvider>;
67
    oidcConnectionType: OIDCConnectionAuthType;
68
    oidcLoginUrl?: string;
69
    oidcUserCode?: string;
70
}
71

72
export interface ConnectionStateDisconnected extends ConnectionState {
73
    tag: "disconnected";
74
}
75

76
export interface ConnectionStateErrored extends ConnectionState {
77
    tag: "errored";
78
    errorReason: string;
79
}
80

81
export type AnyConnectionState =
82
    | ConnectionStateConnected
83
    | ConnectionStateConnecting
84
    | ConnectionStateDisconnected
85
    | ConnectionStateErrored;
86

87
export interface ConnectionManagerEvents {
88
    "connection-request": [AnyConnectionState];
89
    "connection-success": [ConnectionStateConnected];
90
    "connection-time-out": [ConnectionStateErrored];
91
    "connection-close": [ConnectionStateDisconnected];
92
    "connection-error": [ConnectionStateErrored];
93
    close: [AnyConnectionState];
94
}
95

96
export abstract class ConnectionManager {
2✔
97
    public clientName: string;
98
    protected readonly _events: EventEmitter<ConnectionManagerEvents>;
99
    readonly events: Pick<EventEmitter<ConnectionManagerEvents>, "on" | "off" | "once">;
100
    private state: AnyConnectionState;
101

102
    constructor() {
2✔
103
        this.clientName = "unknown";
97✔
104
        this.events = this._events = new EventEmitter<ConnectionManagerEvents>();
97✔
105
        this.state = { tag: "disconnected" };
97✔
106
    }
97✔
107

108
    get currentConnectionState(): AnyConnectionState {
2✔
109
        return this.state;
6,561✔
110
    }
6,561✔
111

112
    protected changeState<Event extends keyof ConnectionManagerEvents, State extends ConnectionManagerEvents[Event][0]>(
2✔
113
        event: Event,
616✔
114
        newState: State
616✔
115
    ): State {
616✔
116
        this.state = newState;
616✔
117
        // TypeScript doesn't seem to be happy with the spread operator and generics
118
        // eslint-disable-next-line
119
        this._events.emit(event, ...([newState] as any));
616✔
120
        return newState;
616✔
121
    }
616✔
122

123
    setClientName(clientName: string): void {
2✔
124
        this.clientName = clientName;
87✔
125
    }
87✔
126

127
    abstract connect(settings: ConnectionSettings): Promise<AnyConnectionState>;
128
    abstract disconnect(): Promise<ConnectionStateDisconnected | ConnectionStateErrored>;
129
    abstract close(): Promise<void>;
130
}
2✔
131

132
export class MCPConnectionManager extends ConnectionManager {
2✔
133
    private deviceId: DeviceId;
134
    private bus: EventEmitter;
135

136
    constructor(
2✔
137
        private userConfig: UserConfig,
97✔
138
        private driverOptions: DriverOptions,
97✔
139
        private logger: LoggerBase,
97✔
140
        deviceId: DeviceId,
97✔
141
        bus?: EventEmitter
97✔
142
    ) {
97✔
143
        super();
97✔
144
        this.bus = bus ?? new EventEmitter();
97✔
145
        this.bus.on("mongodb-oidc-plugin:auth-failed", this.onOidcAuthFailed.bind(this));
97✔
146
        // eslint-disable-next-line @typescript-eslint/no-misused-promises
147
        this.bus.on("mongodb-oidc-plugin:auth-succeeded", this.onOidcAuthSucceeded.bind(this));
97✔
148
        this.deviceId = deviceId;
97✔
149
    }
97✔
150

151
    override async connect(settings: ConnectionSettings): Promise<AnyConnectionState> {
2✔
152
        this._events.emit("connection-request", this.currentConnectionState);
309✔
153

154
        if (this.currentConnectionState.tag === "connected" || this.currentConnectionState.tag === "connecting") {
309!
155
            await this.disconnect();
13✔
156
        }
13✔
157

158
        let serviceProvider: Promise<NodeDriverServiceProvider>;
309✔
159
        let connectionInfo: ConnectionInfo;
309✔
160
        let connectionStringAuthType: ConnectionStringAuthType = "scram";
309✔
161

162
        try {
309✔
163
            settings = { ...settings };
309✔
164
            const appNameComponents: AppNameComponents = {
309✔
165
                appName: `${packageInfo.mcpServerName} ${packageInfo.version}`,
309✔
166
                deviceId: this.deviceId.get(),
309✔
167
                clientName: this.clientName,
309✔
168
            };
309✔
169

170
            settings.connectionString = await setAppNameParamIfMissing({
309✔
171
                connectionString: settings.connectionString,
309✔
172
                components: appNameComponents,
309✔
173
            });
309✔
174

175
            connectionInfo = generateConnectionInfoFromCliArgs({
307✔
176
                ...this.userConfig,
307✔
177
                ...this.driverOptions,
307✔
178
                connectionSpecifier: settings.connectionString,
307✔
179
            });
307✔
180

181
            if (connectionInfo.driverOptions.oidc) {
309!
182
                connectionInfo.driverOptions.oidc.allowedFlows ??= ["auth-code"];
7✔
183
                connectionInfo.driverOptions.oidc.notifyDeviceFlow ??= this.onOidcNotifyDeviceFlow.bind(this);
7✔
184
            }
7✔
185

186
            connectionInfo.driverOptions.proxy ??= { useEnvironmentVariableProxies: true };
307✔
187
            connectionInfo.driverOptions.applyProxyToOIDC ??= true;
307✔
188

189
            connectionStringAuthType = MCPConnectionManager.inferConnectionTypeFromSettings(
307✔
190
                this.userConfig,
307✔
191
                connectionInfo
307✔
192
            );
307✔
193

194
            serviceProvider = NodeDriverServiceProvider.connect(
307✔
195
                connectionInfo.connectionString,
307✔
196
                {
307✔
197
                    productDocsLink: "https://github.com/mongodb-js/mongodb-mcp-server/",
307✔
198
                    productName: "MongoDB MCP",
307✔
199
                    ...connectionInfo.driverOptions,
307✔
200
                },
307✔
201
                undefined,
307✔
202
                this.bus
307✔
203
            );
307✔
204
        } catch (error: unknown) {
309!
205
            const errorReason = error instanceof Error ? error.message : `${error as string}`;
2!
206
            this.changeState("connection-error", {
2✔
207
                tag: "errored",
2✔
208
                errorReason,
2✔
209
                connectionStringAuthType,
2✔
210
                connectedAtlasCluster: settings.atlas,
2✔
211
            });
2✔
212
            throw new MongoDBError(ErrorCodes.MisconfiguredConnectionString, errorReason);
2✔
213
        }
2✔
214

215
        try {
307✔
216
            if (connectionStringAuthType.startsWith("oidc")) {
309!
217
                return this.changeState("connection-request", {
7✔
218
                    tag: "connecting",
7✔
219
                    serviceProvider,
7✔
220
                    connectedAtlasCluster: settings.atlas,
7✔
221
                    connectionStringAuthType,
7✔
222
                    oidcConnectionType: connectionStringAuthType as OIDCConnectionAuthType,
7✔
223
                });
7✔
224
            }
7✔
225

226
            return this.changeState(
300✔
227
                "connection-success",
300✔
228
                new ConnectionStateConnected(await serviceProvider, connectionStringAuthType, settings.atlas)
300✔
229
            );
281✔
230
        } catch (error: unknown) {
302✔
231
            const errorReason = error instanceof Error ? error.message : `${error as string}`;
19!
232
            this.changeState("connection-error", {
19✔
233
                tag: "errored",
19✔
234
                errorReason,
19✔
235
                connectionStringAuthType,
19✔
236
                connectedAtlasCluster: settings.atlas,
19✔
237
            });
19✔
238
            throw new MongoDBError(ErrorCodes.NotConnectedToMongoDB, errorReason);
19✔
239
        }
19✔
240
    }
309✔
241

242
    override async disconnect(): Promise<ConnectionStateDisconnected | ConnectionStateErrored> {
2✔
243
        if (this.currentConnectionState.tag === "disconnected" || this.currentConnectionState.tag === "errored") {
635✔
244
            return this.currentConnectionState;
359✔
245
        }
359✔
246

247
        if (this.currentConnectionState.tag === "connected" || this.currentConnectionState.tag === "connecting") {
635!
248
            try {
276✔
249
                if (this.currentConnectionState.tag === "connected") {
276✔
250
                    await this.currentConnectionState.serviceProvider?.close();
276✔
251
                }
276✔
252
                if (this.currentConnectionState.tag === "connecting") {
276!
253
                    const serviceProvider = await this.currentConnectionState.serviceProvider;
×
254
                    await serviceProvider.close();
×
255
                }
×
256
            } finally {
276✔
257
                this.changeState("connection-close", {
276✔
258
                    tag: "disconnected",
276✔
259
                });
276✔
260
            }
276✔
261
        }
276✔
262

263
        return { tag: "disconnected" };
276✔
264
    }
635✔
265

266
    override async close(): Promise<void> {
2✔
267
        try {
597✔
268
            await this.disconnect();
597✔
269
        } catch (err: unknown) {
597!
270
            const error = err instanceof Error ? err : new Error(String(err));
×
271
            this.logger.error({
×
272
                id: LogId.mongodbDisconnectFailure,
×
273
                context: "ConnectionManager",
×
274
                message: `Error when closing ConnectionManager: ${error.message}`,
×
275
            });
×
276
        } finally {
597✔
277
            this._events.emit("close", this.currentConnectionState);
597✔
278
        }
597✔
279
    }
597✔
280

281
    private onOidcAuthFailed(error: unknown): void {
2✔
282
        if (
×
283
            this.currentConnectionState.tag === "connecting" &&
×
284
            this.currentConnectionState.connectionStringAuthType?.startsWith("oidc")
×
285
        ) {
×
286
            void this.disconnectOnOidcError(error);
×
287
        }
×
288
    }
×
289

290
    private async onOidcAuthSucceeded(): Promise<void> {
2✔
291
        if (
9✔
292
            this.currentConnectionState.tag === "connecting" &&
9✔
293
            this.currentConnectionState.connectionStringAuthType?.startsWith("oidc")
7✔
294
        ) {
9✔
295
            this.changeState(
7✔
296
                "connection-success",
7✔
297
                new ConnectionStateConnected(
7✔
298
                    await this.currentConnectionState.serviceProvider,
7✔
299
                    this.currentConnectionState.connectionStringAuthType,
7✔
300
                    this.currentConnectionState.connectedAtlasCluster
7✔
301
                )
7✔
302
            );
7✔
303
        }
7✔
304

305
        this.logger.info({
9✔
306
            id: LogId.oidcFlow,
9✔
307
            context: "mongodb-oidc-plugin:auth-succeeded",
9✔
308
            message: "Authenticated successfully.",
9✔
309
        });
9✔
310
    }
9✔
311

312
    private onOidcNotifyDeviceFlow(flowInfo: { verificationUrl: string; userCode: string }): void {
2✔
313
        if (
1✔
314
            this.currentConnectionState.tag === "connecting" &&
1✔
315
            this.currentConnectionState.connectionStringAuthType?.startsWith("oidc")
1✔
316
        ) {
1✔
317
            this.changeState("connection-request", {
1✔
318
                ...this.currentConnectionState,
1✔
319
                tag: "connecting",
1✔
320
                connectionStringAuthType: "oidc-device-flow",
1✔
321
                oidcLoginUrl: flowInfo.verificationUrl,
1✔
322
                oidcUserCode: flowInfo.userCode,
1✔
323
            });
1✔
324
        }
1✔
325

326
        this.logger.info({
1✔
327
            id: LogId.oidcFlow,
1✔
328
            context: "mongodb-oidc-plugin:notify-device-flow",
1✔
329
            message: "OIDC Flow changed automatically to device flow.",
1✔
330
        });
1✔
331
    }
1✔
332

333
    static inferConnectionTypeFromSettings(
2✔
334
        config: UserConfig,
316✔
335
        settings: { connectionString: string }
316✔
336
    ): ConnectionStringAuthType {
316✔
337
        const connString = new ConnectionString(settings.connectionString);
316✔
338
        const searchParams = connString.typedSearchParams<MongoClientOptions>();
316✔
339

340
        switch (searchParams.get("authMechanism")) {
316✔
341
            case "MONGODB-OIDC": {
316!
342
                if (config.transport === "stdio" && config.browser) {
11✔
343
                    return "oidc-auth-flow";
7✔
344
                }
7✔
345

346
                if (config.transport === "http" && config.httpHost === "127.0.0.1" && config.browser) {
11✔
347
                    return "oidc-auth-flow";
1✔
348
                }
1✔
349

350
                return "oidc-device-flow";
3✔
351
            }
3✔
352
            case "MONGODB-X509":
316!
353
                return "x.509";
1✔
354
            case "GSSAPI":
316!
355
                return "kerberos";
1✔
356
            case "PLAIN":
316!
357
                if (searchParams.get("authSource") === "$external") {
2✔
358
                    return "ldap";
1✔
359
                }
1✔
360
                return "scram";
1✔
361
            // default should catch also null, but eslint complains
362
            // about it.
363
            case null:
316✔
364
            default:
316✔
365
                return "scram";
301✔
366
        }
316✔
367
    }
316✔
368

369
    private async disconnectOnOidcError(error: unknown): Promise<void> {
2✔
370
        try {
×
371
            await this.disconnect();
×
372
        } catch (error: unknown) {
×
373
            this.logger.warning({
×
374
                id: LogId.oidcFlow,
×
375
                context: "disconnectOnOidcError",
×
376
                message: String(error),
×
377
            });
×
378
        } finally {
×
379
            this.changeState("connection-error", { tag: "errored", errorReason: String(error) });
×
380
        }
×
381
    }
×
382
}
2✔
383

384
/**
385
 * Consumers of MCP server library have option to bring their own connection
386
 * management if they need to. To support that, we enable injecting connection
387
 * manager implementation through a factory function.
388
 */
389
export type ConnectionManagerFactoryFn = (createParams: {
390
    logger: LoggerBase;
391
    deviceId: DeviceId;
392
    userConfig: UserConfig;
393
}) => Promise<ConnectionManager>;
394

395
export const createMCPConnectionManager: ConnectionManagerFactoryFn = ({ logger, deviceId, userConfig }) => {
2✔
396
    const driverOptions = setupDriverConfig({
7✔
397
        config: userConfig,
7✔
398
        defaults: defaultDriverOptions,
7✔
399
    });
7✔
400

401
    return Promise.resolve(new MCPConnectionManager(userConfig, driverOptions, logger, deviceId));
7✔
402
};
7✔
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

© 2026 Coveralls, Inc