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

mongodb-js / mongodb-mcp-server / 17668190160

12 Sep 2025 07:49AM UTC coverage: 81.428% (-0.1%) from 81.528%
17668190160

Pull #544

github

web-flow
Merge 67008787c into d6b84c7bd
Pull Request #544: feat: Allow configuration of temporary user timeout when connecting to atlas cluster

963 of 1280 branches covered (75.23%)

Branch coverage included in aggregate %.

8 of 8 new or added lines in 2 files covered. (100.0%)

6 existing lines in 1 file now uncovered.

4855 of 5865 relevant lines covered (82.78%)

44.71 hits per line

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

87.78
/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: EventEmitter<ConnectionManagerEvents>;
86
    readonly events: Pick<EventEmitter<ConnectionManagerEvents>, "on" | "off" | "once">;
87
    private state: AnyConnectionState;
88

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

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

99
    protected changeState<Event extends keyof ConnectionManagerEvents, State extends ConnectionManagerEvents[Event][0]>(
2✔
100
        event: Event,
472✔
101
        newState: State
472✔
102
    ): State {
472✔
103
        this.state = newState;
472✔
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));
472✔
107
        return newState;
472✔
108
    }
472✔
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,
70✔
125
        private driverOptions: DriverOptions,
70✔
126
        private logger: LoggerBase,
70✔
127
        deviceId: DeviceId,
70✔
128
        bus?: EventEmitter
70✔
129
    ) {
70✔
130
        super();
70✔
131
        this.bus = bus ?? new EventEmitter();
70✔
132
        this.bus.on("mongodb-oidc-plugin:auth-failed", this.onOidcAuthFailed.bind(this));
70✔
133
        this.bus.on("mongodb-oidc-plugin:auth-succeeded", this.onOidcAuthSucceeded.bind(this));
70✔
134
        this.deviceId = deviceId;
70✔
135
    }
70✔
136

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

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

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

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

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

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

167
            if (connectionInfo.driverOptions.oidc) {
234!
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 };
234✔
173
            connectionInfo.driverOptions.applyProxyToOIDC ??= true;
234✔
174

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

180
            serviceProvider = await NodeDriverServiceProvider.connect(
234✔
181
                connectionInfo.connectionString,
234✔
182
                {
234✔
183
                    productDocsLink: "https://github.com/mongodb-js/mongodb-mcp-server/",
234✔
184
                    productName: "MongoDB MCP",
234✔
185
                    ...connectionInfo.driverOptions,
234✔
186
                },
234✔
187
                undefined,
234✔
188
                this.bus
234✔
189
            );
234✔
190
        } catch (error: unknown) {
230!
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 {
222✔
202
            if (connectionStringAuthType.startsWith("oidc")) {
234!
203
                void this.pingAndForget(serviceProvider);
7✔
204

205
                return this.changeState("connection-request", {
7✔
206
                    tag: "connecting",
7✔
207
                    connectedAtlasCluster: settings.atlas,
7✔
208
                    serviceProvider,
7✔
209
                    connectionStringAuthType,
7✔
210
                    oidcConnectionType: connectionStringAuthType as OIDCConnectionAuthType,
7✔
211
                });
7✔
212
            }
7✔
213

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

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

234
    async disconnect(): Promise<ConnectionStateDisconnected | ConnectionStateErrored> {
2✔
235
        if (this.currentConnectionState.tag === "disconnected" || this.currentConnectionState.tag === "errored") {
498✔
236
            return this.currentConnectionState;
291✔
237
        }
291✔
238

239
        if (this.currentConnectionState.tag === "connected" || this.currentConnectionState.tag === "connecting") {
498!
240
            try {
207✔
241
                await this.currentConnectionState.serviceProvider?.close(true);
207✔
242
            } finally {
207✔
243
                this.changeState("connection-close", {
207✔
244
                    tag: "disconnected",
207✔
245
                });
207✔
246
            }
207✔
247
        }
207✔
248

249
        return { tag: "disconnected" };
207✔
250
    }
498✔
251

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

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

269
        this.logger.info({
9✔
270
            id: LogId.oidcFlow,
9✔
271
            context: "mongodb-oidc-plugin:auth-succeeded",
9✔
272
            message: "Authenticated successfully.",
9✔
273
        });
9✔
274
    }
9✔
275

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

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

297
    static inferConnectionTypeFromSettings(
2✔
298
        config: UserConfig,
243✔
299
        settings: { connectionString: string }
243✔
300
    ): ConnectionStringAuthType {
243✔
301
        const connString = new ConnectionString(settings.connectionString);
243✔
302
        const searchParams = connString.typedSearchParams<MongoClientOptions>();
243✔
303

304
        switch (searchParams.get("authMechanism")) {
243✔
305
            case "MONGODB-OIDC": {
243!
306
                if (config.transport === "stdio" && config.browser) {
11✔
307
                    return "oidc-auth-flow";
7✔
308
                }
7✔
309

310
                if (config.transport === "http" && config.httpHost === "127.0.0.1" && config.browser) {
11✔
311
                    return "oidc-auth-flow";
1✔
312
                }
1✔
313

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

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

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

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

371
export const createMCPConnectionManager: ConnectionManagerFactoryFn = ({ logger, deviceId, userConfig }) => {
2✔
372
    const driverOptions = setupDriverConfig({
7✔
373
        config: userConfig,
7✔
374
        defaults: defaultDriverOptions,
7✔
375
    });
7✔
376

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

© 2025 Coveralls, Inc