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

mongodb-js / mongodb-mcp-server / 25675733856

11 May 2026 02:16PM UTC coverage: 79.45%. Remained the same
25675733856

push

github

web-flow
chore: Azure/bicep changes (#1134)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

1786 of 2500 branches covered (71.44%)

Branch coverage included in aggregate %.

3352 of 3967 relevant lines covered (84.5%)

161.35 hits per line

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

85.96
/src/common/connectionManager.ts
1
import { EventEmitter } from "events";
2
import { MongoServerError } from "mongodb";
3
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
4
import { generateConnectionInfoFromCliArgs, type ConnectionInfo } from "@mongosh/arg-parser";
5
import type { DeviceId } from "../helpers/deviceId.js";
6
import { type UserConfig } from "./config/userConfig.js";
7
import { MongoDBError, ErrorCodes } from "./errors.js";
8
import { type LoggerBase, LogId } from "./logging/index.js";
9
import { packageInfo } from "./packageInfo.js";
10
import { type AppNameComponents, setAppNameParamIfMissing } from "../helpers/connectionOptions.js";
11
import {
12
    getConnectionStringInfo,
13
    type ConnectionStringInfo,
14
    type AtlasClusterConnectionInfo,
15
} from "./connectionInfo.js";
16

17
export type { ConnectionStringInfo, ConnectionStringAuthType, AtlasClusterConnectionInfo } from "./connectionInfo.js";
18

19
export interface ConnectionSettings extends Omit<ConnectionInfo, "driverOptions"> {
20
    driverOptions?: ConnectionInfo["driverOptions"];
21
    atlas?: AtlasClusterConnectionInfo;
22
}
23

24
export type ConnectionTag = "connected" | "connecting" | "disconnected" | "errored";
25
export type OIDCConnectionAuthType = "oidc-auth-flow" | "oidc-device-flow";
26

27
export interface ConnectionState {
28
    tag: ConnectionTag;
29
    connectionStringInfo?: ConnectionStringInfo;
30
    connectedAtlasCluster?: AtlasClusterConnectionInfo;
31
}
32

33
const SEARCH_PROBE_COLLECTION_NAME = "test";
62✔
34

35
/** See https://github.com/mongodb/mongo/blob/master/src/mongo/base/error_codes.yml (SearchNotEnabled). */
36
const MONGODB_SEARCH_NOT_ENABLED_ERROR_CODE = 31082;
62✔
37

38
export const defaultDriverOptions: ConnectionInfo["driverOptions"] = {
62✔
39
    readConcern: {
40
        level: "local",
41
    },
42
    readPreference: "secondaryPreferred",
43
    writeConcern: {
44
        w: "majority",
45
    },
46
    timeoutMS: 30_000,
47
    proxy: { useEnvironmentVariableProxies: true },
48
    applyProxyToOIDC: true,
49
};
50

51
export class ConnectionStateConnected implements ConnectionState {
52
    public tag = "connected" as const;
425✔
53

54
    constructor(
55
        public serviceProvider: NodeDriverServiceProvider,
425✔
56
        public connectionStringInfo?: ConnectionStringInfo,
425✔
57
        public connectedAtlasCluster?: AtlasClusterConnectionInfo
425✔
58
    ) {}
59

60
    private _isSearchSupported?: boolean;
61

62
    public async isSearchSupported(logger: LoggerBase): Promise<boolean> {
63
        if (this._isSearchSupported === undefined) {
212✔
64
            this._isSearchSupported = await this.probeSearchCapability(logger);
109✔
65
        }
66

67
        return this._isSearchSupported;
212✔
68
    }
69

70
    private async probeSearchCapability(logger: LoggerBase): Promise<boolean> {
71
        const databases = await this.buildSearchProbeDatabaseCandidates(logger);
109✔
72

73
        for (const databaseName of databases) {
109✔
74
            try {
122✔
75
                await this.serviceProvider.getSearchIndexes(databaseName, SEARCH_PROBE_COLLECTION_NAME);
122✔
76
                logger.debug({
67✔
77
                    id: LogId.searchCapabilityProbe,
78
                    context: "ConnectionStateConnected",
79
                    message: "Atlas Search capability probe succeeded",
80
                });
81
                return true;
67✔
82
            } catch (probeError: unknown) {
83
                if (
55✔
84
                    probeError instanceof MongoServerError &&
123✔
85
                    (probeError.code === MONGODB_SEARCH_NOT_ENABLED_ERROR_CODE ||
86
                        probeError.codeName === "SearchNotEnabled")
87
                ) {
88
                    logger.debug({
38✔
89
                        id: LogId.searchCapabilityProbe,
90
                        context: "ConnectionStateConnected",
91
                        message: "Atlas Search capability probe: search not enabled on cluster",
92
                    });
93

94
                    return false;
38✔
95
                }
96

97
                logger.debug({
17✔
98
                    id: LogId.searchCapabilityProbe,
99
                    context: "ConnectionStateConnected",
100
                    message: "Atlas Search capability probe: inconclusive error for database candidate, trying next",
101
                });
102
            }
103
        }
104

105
        logger.debug({
4✔
106
            id: LogId.searchCapabilityProbe,
107
            context: "ConnectionStateConnected",
108
            message: "Atlas Search capability probe: no success and no SearchNotEnabled; assuming search is supported",
109
        });
110

111
        return true;
4✔
112
    }
113

114
    /**
115
     * Build an ordered list of database names to try for the search index probe.
116
     * Prefers the driver's initial database from the connection string (when not
117
     * a system DB), then other non-system databases from listDatabases, then the
118
     * fallback #mongodb-mcp database.
119
     */
120
    private async buildSearchProbeDatabaseCandidates(logger: LoggerBase): Promise<string[]> {
121
        type ListDatabasesDocument = { databases?: { name?: string }[] };
122
        let listedNames: string[] = [];
109✔
123
        try {
109✔
124
            const raw = (await this.serviceProvider.listDatabases("")) as ListDatabasesDocument;
109✔
125
            const rows = raw.databases;
107✔
126
            if (Array.isArray(rows)) {
107!
127
                listedNames = rows
107✔
128
                    .map((row) => row.name)
1,168✔
129
                    .filter((name): name is string => typeof name === "string" && name.length > 0);
1,168✔
130
            }
131
        } catch {
132
            logger.debug({
2✔
133
                id: LogId.searchCapabilityProbe,
134
                context: "ConnectionStateConnected",
135
                message: "listDatabases failed while building Atlas Search probe candidates",
136
            });
137
        }
138

139
        // System databases that should be skipped when searching for accessible databases
140
        const SYSTEM_DATABASES = new Set(["admin", "local", "config"]);
109✔
141

142
        const nonSystem = listedNames
109✔
143
            .filter((name) => !SYSTEM_DATABASES.has(name))
1,168✔
144
            .slice(0, 10)
145
            .sort((a, b) => a.localeCompare(b));
391✔
146

147
        const result = new Set<string>();
109✔
148
        const initialDb = this.serviceProvider.initialDb;
109✔
149
        if (initialDb.length > 0 && !SYSTEM_DATABASES.has(initialDb)) {
109✔
150
            result.add(initialDb);
105✔
151
        }
152

153
        for (const name of nonSystem) {
109✔
154
            result.add(name);
484✔
155
        }
156

157
        result.add("#mongodb-mcp");
109✔
158

159
        return [...result];
109✔
160
    }
161
}
162

163
export interface ConnectionStateConnecting extends ConnectionState {
164
    tag: "connecting";
165
    serviceProvider: Promise<NodeDriverServiceProvider>;
166
    oidcConnectionType: OIDCConnectionAuthType;
167
    oidcLoginUrl?: string;
168
    oidcUserCode?: string;
169
}
170

171
export interface ConnectionStateDisconnected extends ConnectionState {
172
    tag: "disconnected";
173
}
174

175
export interface ConnectionStateErrored extends ConnectionState {
176
    tag: "errored";
177
    errorReason: string;
178
}
179

180
export type AnyConnectionState =
181
    | ConnectionStateConnected
182
    | ConnectionStateConnecting
183
    | ConnectionStateDisconnected
184
    | ConnectionStateErrored;
185

186
export interface ConnectionManagerEvents {
187
    "connection-request": [AnyConnectionState];
188
    "connection-success": [ConnectionStateConnected];
189
    "connection-time-out": [ConnectionStateErrored];
190
    "connection-close": [ConnectionStateDisconnected];
191
    "connection-error": [ConnectionStateErrored];
192
    close: [AnyConnectionState];
193
}
194

195
export abstract class ConnectionManager {
196
    public clientName: string;
197
    protected readonly _events: EventEmitter<ConnectionManagerEvents>;
198
    readonly events: Pick<EventEmitter<ConnectionManagerEvents>, "on" | "off" | "once">;
199
    private state: AnyConnectionState;
200

201
    constructor() {
202
        this.clientName = "unknown";
248✔
203
        this.events = this._events = new EventEmitter<ConnectionManagerEvents>();
248✔
204
        this.state = { tag: "disconnected" };
248✔
205
    }
206

207
    get currentConnectionState(): AnyConnectionState {
208
        return this.state;
9,776✔
209
    }
210

211
    protected changeState<Event extends keyof ConnectionManagerEvents, State extends ConnectionManagerEvents[Event][0]>(
212
        event: Event,
213
        newState: State
214
    ): State {
215
        this.state = newState;
886✔
216
        // TypeScript doesn't seem to be happy with the spread operator and generics
217
        // eslint-disable-next-line
218
        this._events.emit(event, ...([newState] as any));
886✔
219
        return newState;
886✔
220
    }
221

222
    setClientName(clientName: string): void {
223
        this.clientName = clientName;
189✔
224
    }
225

226
    abstract connect(settings: ConnectionSettings): Promise<AnyConnectionState>;
227
    abstract disconnect(): Promise<ConnectionStateDisconnected | ConnectionStateErrored>;
228
    abstract close(): Promise<void>;
229
}
230

231
/**
232
 * Default {@link ConnectionManager} implementation used by the MongoDB MCP
233
 * server.
234
 *
235
 * Establishes and tears down MongoDB connections via mongosh's
236
 * NodeDriverServiceProvider, applying MCP-specific defaults such as the
237
 * `appName` (composed from package info, device id and client name) and driver
238
 * options (read/write concerns, proxy and OIDC settings).
239
 *
240
 * Tracks connection lifecycle as an {@link AnyConnectionState} and emits
241
 * `connection-request`, `connection-success`, `connection-error`,
242
 * `connection-close` and `close` events on {@link ConnectionManager.events}.
243
 * For OIDC connection strings it stays in the `connecting` state until the OIDC
244
 * plugin reports auth success or failure (via the shared event bus), surfacing
245
 * device-flow verification URL and user code when applicable.
246
 */
247
export class MCPConnectionManager extends ConnectionManager {
248
    private deviceId: DeviceId;
249
    private bus: EventEmitter;
250

251
    /**
252
     * @param userConfig - Active user configuration; used when classifying the
253
     * connection string (e.g. Atlas vs. local).
254
     * @param logger - Logger used for OIDC and disconnect diagnostics.
255
     * @param deviceId - Provider of the stable device identifier embedded in
256
     * the connection's `appName`.
257
     * @param bus - Optional event emitter shared with the OIDC plugin to
258
     * receive `mongodb-oidc-plugin:auth-*` notifications. A fresh emitter is
259
     * created when omitted.
260
     */
261
    constructor(
262
        private userConfig: UserConfig,
248✔
263
        private logger: LoggerBase,
248✔
264
        deviceId: DeviceId,
265
        bus?: EventEmitter
266
    ) {
267
        super();
248✔
268
        this.bus = bus ?? new EventEmitter();
248✔
269
        this.bus.on("mongodb-oidc-plugin:auth-failed", this.onOidcAuthFailed.bind(this));
248✔
270
        // eslint-disable-next-line @typescript-eslint/no-misused-promises
271
        this.bus.on("mongodb-oidc-plugin:auth-succeeded", this.onOidcAuthSucceeded.bind(this));
248✔
272
        this.deviceId = deviceId;
248✔
273
    }
274

275
    /**
276
     * Opens a new MongoDB connection from the supplied {@link ConnectionSettings},
277
     * disconnecting any prior connection first.
278
     *
279
     * Resolves to a `connected` state for non-OIDC auth and to a `connecting`
280
     * state for OIDC flows (which transition to `connected` once the OIDC
281
     * plugin reports success on the shared event bus). On failure, transitions
282
     * to an `errored` state and throws a {@link MongoDBError} with either
283
     * {@link ErrorCodes.MisconfiguredConnectionString} or
284
     * {@link ErrorCodes.NotConnectedToMongoDB}.
285
     */
286
    override async connect(settings: ConnectionSettings): Promise<AnyConnectionState> {
287
        this._events.emit("connection-request", this.currentConnectionState);
446✔
288

289
        if (this.currentConnectionState.tag === "connected" || this.currentConnectionState.tag === "connecting") {
446✔
290
            await this.disconnect();
14✔
291
        }
292

293
        let serviceProvider: Promise<NodeDriverServiceProvider>;
294
        let connectionStringInfo: ConnectionStringInfo = { authType: "scram", hostType: "unknown" };
446✔
295

296
        try {
446✔
297
            settings = { ...settings };
446✔
298
            const appNameComponents: AppNameComponents = {
446✔
299
                appName: `${packageInfo.mcpServerName} ${packageInfo.version}`,
300
                deviceId: this.deviceId.get(),
301
                clientName: this.clientName,
302
            };
303

304
            settings.connectionString = await setAppNameParamIfMissing({
446✔
305
                connectionString: settings.connectionString,
306
                components: appNameComponents,
307
            });
308

309
            const connectionInfo: ConnectionInfo = settings.driverOptions
444✔
310
                ? {
311
                      connectionString: settings.connectionString,
312
                      driverOptions: settings.driverOptions,
313
                  }
314
                : generateConnectionInfoFromCliArgs({
315
                      ...defaultDriverOptions,
316
                      connectionSpecifier: settings.connectionString,
317
                  });
318

319
            if (connectionInfo.driverOptions.oidc) {
446✔
320
                connectionInfo.driverOptions.oidc.allowedFlows ??= ["auth-code"];
8✔
321
                connectionInfo.driverOptions.oidc.notifyDeviceFlow ??= this.onOidcNotifyDeviceFlow.bind(this);
8✔
322
            }
323

324
            connectionInfo.driverOptions.proxy ??= { useEnvironmentVariableProxies: true };
444✔
325
            connectionInfo.driverOptions.applyProxyToOIDC ??= true;
444✔
326

327
            connectionStringInfo = getConnectionStringInfo(
444✔
328
                connectionInfo.connectionString,
329
                this.userConfig,
330
                settings.atlas
331
            );
332

333
            serviceProvider = NodeDriverServiceProvider.connect(
444✔
334
                connectionInfo.connectionString,
335
                {
336
                    productDocsLink: "https://github.com/mongodb-js/mongodb-mcp-server/",
337
                    productName: "MongoDB MCP",
338
                    ...connectionInfo.driverOptions,
339
                },
340
                undefined,
341
                this.bus
342
            );
343
        } catch (error: unknown) {
344
            const errorReason = error instanceof Error ? error.message : `${error as string}`;
2!
345
            this.changeState("connection-error", {
2✔
346
                tag: "errored",
347
                errorReason,
348
                connectionStringInfo,
349
                connectedAtlasCluster: settings.atlas,
350
            });
351
            throw new MongoDBError(ErrorCodes.MisconfiguredConnectionString, errorReason);
2✔
352
        }
353

354
        try {
444✔
355
            if (connectionStringInfo.authType.startsWith("oidc")) {
444✔
356
                return this.changeState("connection-request", {
7✔
357
                    tag: "connecting",
358
                    serviceProvider,
359
                    connectedAtlasCluster: settings.atlas,
360
                    connectionStringInfo,
361
                    oidcConnectionType: connectionStringInfo.authType as OIDCConnectionAuthType,
362
                });
363
            }
364

365
            return this.changeState(
437✔
366
                "connection-success",
367
                new ConnectionStateConnected(await serviceProvider, connectionStringInfo, settings.atlas)
368
            );
369
        } catch (error: unknown) {
370
            const errorReason = error instanceof Error ? error.message : `${error as string}`;
19!
371
            this.changeState("connection-error", {
19✔
372
                tag: "errored",
373
                errorReason,
374
                connectionStringInfo,
375
                connectedAtlasCluster: settings.atlas,
376
            });
377
            throw new MongoDBError(ErrorCodes.NotConnectedToMongoDB, errorReason);
19✔
378
        }
379
    }
380

381
    /**
382
     * Closes the underlying NodeDriverServiceProvider (awaiting it first when
383
     * the manager is still in the `connecting` state) and emits
384
     * `connection-close`. No-op when already `disconnected` or `errored`, in
385
     * which case the current state is returned as-is.
386
     */
387
    override async disconnect(): Promise<ConnectionStateDisconnected | ConnectionStateErrored> {
388
        if (this.currentConnectionState.tag === "disconnected" || this.currentConnectionState.tag === "errored") {
1,047✔
389
            return this.currentConnectionState;
639✔
390
        }
391

392
        if (this.currentConnectionState.tag === "connected" || this.currentConnectionState.tag === "connecting") {
408!
393
            try {
408✔
394
                if (this.currentConnectionState.tag === "connected") {
408!
395
                    await this.currentConnectionState.serviceProvider?.close();
408✔
396
                }
397
                if (this.currentConnectionState.tag === "connecting") {
408!
398
                    const serviceProvider = await this.currentConnectionState.serviceProvider;
×
399
                    await serviceProvider.close();
×
400
                }
401
            } finally {
402
                this.changeState("connection-close", {
408✔
403
                    tag: "disconnected",
404
                });
405
            }
406
        }
407

408
        return { tag: "disconnected" };
408✔
409
    }
410

411
    /**
412
     * Permanently shuts the manager down: best-effort disconnect (errors are
413
     * logged, not thrown) followed by emission of the `close` event with the
414
     * final connection state.
415
     */
416
    override async close(): Promise<void> {
417
        try {
1,007✔
418
            await this.disconnect();
1,007✔
419
        } catch (err: unknown) {
420
            const error = err instanceof Error ? err : new Error(String(err));
×
421
            this.logger.error({
×
422
                id: LogId.mongodbDisconnectFailure,
423
                context: "ConnectionManager",
424
                message: `Error when closing ConnectionManager: ${error.message}`,
425
            });
426
        } finally {
427
            this._events.emit("close", this.currentConnectionState);
1,007✔
428
        }
429
    }
430

431
    private onOidcAuthFailed(error: unknown): void {
432
        if (
×
433
            this.currentConnectionState.tag === "connecting" &&
×
434
            this.currentConnectionState.connectionStringInfo?.authType?.startsWith("oidc")
435
        ) {
436
            void this.disconnectOnOidcError(error);
×
437
        }
438
    }
439

440
    private async onOidcAuthSucceeded(): Promise<void> {
441
        if (
9✔
442
            this.currentConnectionState.tag === "connecting" &&
16✔
443
            this.currentConnectionState.connectionStringInfo?.authType?.startsWith("oidc")
444
        ) {
445
            this.changeState(
7✔
446
                "connection-success",
447
                new ConnectionStateConnected(
448
                    await this.currentConnectionState.serviceProvider,
449
                    this.currentConnectionState.connectionStringInfo,
450
                    this.currentConnectionState.connectedAtlasCluster
451
                )
452
            );
453
        }
454

455
        this.logger.info({
9✔
456
            id: LogId.oidcFlow,
457
            context: "mongodb-oidc-plugin:auth-succeeded",
458
            message: "Authenticated successfully.",
459
        });
460
    }
461

462
    private onOidcNotifyDeviceFlow(flowInfo: { verificationUrl: string; userCode: string }): void {
463
        if (
1!
464
            this.currentConnectionState.tag === "connecting" &&
2✔
465
            this.currentConnectionState.connectionStringInfo?.authType?.startsWith("oidc")
466
        ) {
467
            this.changeState("connection-request", {
1✔
468
                ...this.currentConnectionState,
469
                tag: "connecting",
470
                connectionStringInfo: {
471
                    ...this.currentConnectionState.connectionStringInfo,
472
                    authType: "oidc-device-flow",
473
                },
474
                oidcLoginUrl: flowInfo.verificationUrl,
475
                oidcUserCode: flowInfo.userCode,
476
            });
477
        }
478

479
        this.logger.info({
1✔
480
            id: LogId.oidcFlow,
481
            context: "mongodb-oidc-plugin:notify-device-flow",
482
            message: "OIDC Flow changed automatically to device flow.",
483
        });
484
    }
485

486
    private async disconnectOnOidcError(error: unknown): Promise<void> {
487
        try {
×
488
            await this.disconnect();
×
489
        } catch (error: unknown) {
490
            this.logger.warning({
×
491
                id: LogId.oidcFlow,
492
                context: "disconnectOnOidcError",
493
                message: String(error),
494
            });
495
        } finally {
496
            this.changeState("connection-error", { tag: "errored", errorReason: String(error) });
×
497
        }
498
    }
499
}
500

501
/**
502
 * Consumers of MCP server library have option to bring their own connection
503
 * management if they need to. To support that, we enable injecting connection
504
 * manager implementation through a factory function.
505
 */
506
export type ConnectionManagerFactoryFn = (createParams: {
507
    logger: LoggerBase;
508
    deviceId: DeviceId;
509
    userConfig: UserConfig;
510
}) => Promise<ConnectionManager>;
511

512
export const defaultCreateConnectionManager: ConnectionManagerFactoryFn = ({ logger, deviceId, userConfig }) => {
62✔
513
    return Promise.resolve(new MCPConnectionManager(userConfig, logger, deviceId));
83✔
514
};
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