1
- import type { UserConfig , DriverOptions } from "./config.js" ;
2
- import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver" ;
3
- import EventEmitter from "events" ;
4
- import { setAppNameParamIfMissing } from "../helpers/connectionOptions.js" ;
5
- import { packageInfo } from "./packageInfo.js" ;
6
- import ConnectionString from "mongodb-connection-string-url" ;
1
+ import { EventEmitter } from "events" ;
7
2
import type { MongoClientOptions } from "mongodb" ;
8
- import { ErrorCodes , MongoDBError } from "./errors.js" ;
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" ;
9
6
import type { DeviceId } from "../helpers/deviceId.js" ;
10
- import type { AppNameComponents } from "../helpers/connectionOptions .js" ;
11
- import type { CompositeLogger } from "./logger .js" ;
12
- import { LogId } from "./logger.js" ;
13
- import type { ConnectionInfo } from "@mongosh/arg-parser " ;
14
- import { generateConnectionInfoFromCliArgs } from "@mongosh/arg-parser " ;
7
+ import { defaultDriverOptions , setupDriverConfig , type DriverOptions , type UserConfig } from "./config .js" ;
8
+ import { MongoDBError , ErrorCodes } from "./errors .js" ;
9
+ import { type LoggerBase , LogId } from "./logger.js" ;
10
+ import { packageInfo } from "./packageInfo.js " ;
11
+ import { type AppNameComponents , setAppNameParamIfMissing } from "../helpers/connectionOptions.js " ;
15
12
16
13
export interface AtlasClusterConnectionInfo {
17
14
username : string ;
@@ -71,39 +68,76 @@ export interface ConnectionManagerEvents {
71
68
"connection-error" : [ ConnectionStateErrored ] ;
72
69
}
73
70
74
- export class ConnectionManager extends EventEmitter < ConnectionManagerEvents > {
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 {
84
+ protected clientName : string ;
85
+ protected readonly _events ;
86
+ readonly events : Pick < EventEmitter < ConnectionManagerEvents > , "on" | "off" | "once" > ;
75
87
private state : AnyConnectionState ;
88
+
89
+ constructor ( ) {
90
+ this . clientName = "unknown" ;
91
+ this . events = this . _events = new EventEmitter < ConnectionManagerEvents > ( ) ;
92
+ this . state = { tag : "disconnected" } ;
93
+ }
94
+
95
+ get currentConnectionState ( ) : AnyConnectionState {
96
+ return this . state ;
97
+ }
98
+
99
+ protected changeState < Event extends keyof ConnectionManagerEvents , State extends ConnectionManagerEvents [ Event ] [ 0 ] > (
100
+ event : Event ,
101
+ newState : State
102
+ ) : State {
103
+ this . state = newState ;
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 ) ) ;
107
+ return newState ;
108
+ }
109
+
110
+ setClientName ( clientName : string ) : void {
111
+ this . clientName = clientName ;
112
+ }
113
+
114
+ abstract connect ( settings : ConnectionSettings ) : Promise < AnyConnectionState > ;
115
+
116
+ abstract disconnect ( ) : Promise < ConnectionStateDisconnected | ConnectionStateErrored > ;
117
+ }
118
+
119
+ export class MCPConnectionManager extends ConnectionManager {
76
120
private deviceId : DeviceId ;
77
- private clientName : string ;
78
121
private bus : EventEmitter ;
79
122
80
123
constructor (
81
124
private userConfig : UserConfig ,
82
125
private driverOptions : DriverOptions ,
83
- private logger : CompositeLogger ,
126
+ private logger : LoggerBase ,
84
127
deviceId : DeviceId ,
85
128
bus ?: EventEmitter
86
129
) {
87
130
super ( ) ;
88
-
89
131
this . bus = bus ?? new EventEmitter ( ) ;
90
- this . state = { tag : "disconnected" } ;
91
-
92
132
this . bus . on ( "mongodb-oidc-plugin:auth-failed" , this . onOidcAuthFailed . bind ( this ) ) ;
93
133
this . bus . on ( "mongodb-oidc-plugin:auth-succeeded" , this . onOidcAuthSucceeded . bind ( this ) ) ;
94
-
95
134
this . deviceId = deviceId ;
96
- this . clientName = "unknown" ;
97
- }
98
-
99
- setClientName ( clientName : string ) : void {
100
- this . clientName = clientName ;
101
135
}
102
136
103
137
async connect ( settings : ConnectionSettings ) : Promise < AnyConnectionState > {
104
- this . emit ( "connection-request" , this . state ) ;
138
+ this . _events . emit ( "connection-request" , this . currentConnectionState ) ;
105
139
106
- if ( this . state . tag === "connected" || this . state . tag === "connecting" ) {
140
+ if ( this . currentConnectionState . tag === "connected" || this . currentConnectionState . tag === "connecting" ) {
107
141
await this . disconnect ( ) ;
108
142
}
109
143
@@ -138,7 +172,7 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
138
172
connectionInfo . driverOptions . proxy ??= { useEnvironmentVariableProxies : true } ;
139
173
connectionInfo . driverOptions . applyProxyToOIDC ??= true ;
140
174
141
- connectionStringAuthType = ConnectionManager . inferConnectionTypeFromSettings (
175
+ connectionStringAuthType = MCPConnectionManager . inferConnectionTypeFromSettings (
142
176
this . userConfig ,
143
177
connectionInfo
144
178
) ;
@@ -165,7 +199,10 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
165
199
}
166
200
167
201
try {
168
- const connectionType = ConnectionManager . inferConnectionTypeFromSettings ( this . userConfig , connectionInfo ) ;
202
+ const connectionType = MCPConnectionManager . inferConnectionTypeFromSettings (
203
+ this . userConfig ,
204
+ connectionInfo
205
+ ) ;
169
206
if ( connectionType . startsWith ( "oidc" ) ) {
170
207
void this . pingAndForget ( serviceProvider ) ;
171
208
@@ -199,13 +236,13 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
199
236
}
200
237
201
238
async disconnect ( ) : Promise < ConnectionStateDisconnected | ConnectionStateErrored > {
202
- if ( this . state . tag === "disconnected" || this . state . tag === "errored" ) {
203
- return this . state ;
239
+ if ( this . currentConnectionState . tag === "disconnected" || this . currentConnectionState . tag === "errored" ) {
240
+ return this . currentConnectionState ;
204
241
}
205
242
206
- if ( this . state . tag === "connected" || this . state . tag === "connecting" ) {
243
+ if ( this . currentConnectionState . tag === "connected" || this . currentConnectionState . tag === "connecting" ) {
207
244
try {
208
- await this . state . serviceProvider ?. close ( true ) ;
245
+ await this . currentConnectionState . serviceProvider ?. close ( true ) ;
209
246
} finally {
210
247
this . changeState ( "connection-close" , {
211
248
tag : "disconnected" ,
@@ -216,30 +253,21 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
216
253
return { tag : "disconnected" } ;
217
254
}
218
255
219
- get currentConnectionState ( ) : AnyConnectionState {
220
- return this . state ;
221
- }
222
-
223
- changeState < Event extends keyof ConnectionManagerEvents , State extends ConnectionManagerEvents [ Event ] [ 0 ] > (
224
- event : Event ,
225
- newState : State
226
- ) : State {
227
- this . state = newState ;
228
- // TypeScript doesn't seem to be happy with the spread operator and generics
229
- // eslint-disable-next-line
230
- this . emit ( event , ...( [ newState ] as any ) ) ;
231
- return newState ;
232
- }
233
-
234
256
private onOidcAuthFailed ( error : unknown ) : void {
235
- if ( this . state . tag === "connecting" && this . state . connectionStringAuthType ?. startsWith ( "oidc" ) ) {
257
+ if (
258
+ this . currentConnectionState . tag === "connecting" &&
259
+ this . currentConnectionState . connectionStringAuthType ?. startsWith ( "oidc" )
260
+ ) {
236
261
void this . disconnectOnOidcError ( error ) ;
237
262
}
238
263
}
239
264
240
265
private onOidcAuthSucceeded ( ) : void {
241
- if ( this . state . tag === "connecting" && this . state . connectionStringAuthType ?. startsWith ( "oidc" ) ) {
242
- this . changeState ( "connection-success" , { ...this . state , tag : "connected" } ) ;
266
+ if (
267
+ this . currentConnectionState . tag === "connecting" &&
268
+ this . currentConnectionState . connectionStringAuthType ?. startsWith ( "oidc" )
269
+ ) {
270
+ this . changeState ( "connection-success" , { ...this . currentConnectionState , tag : "connected" } ) ;
243
271
}
244
272
245
273
this . logger . info ( {
@@ -250,9 +278,12 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
250
278
}
251
279
252
280
private onOidcNotifyDeviceFlow ( flowInfo : { verificationUrl : string ; userCode : string } ) : void {
253
- if ( this . state . tag === "connecting" && this . state . connectionStringAuthType ?. startsWith ( "oidc" ) ) {
281
+ if (
282
+ this . currentConnectionState . tag === "connecting" &&
283
+ this . currentConnectionState . connectionStringAuthType ?. startsWith ( "oidc" )
284
+ ) {
254
285
this . changeState ( "connection-request" , {
255
- ...this . state ,
286
+ ...this . currentConnectionState ,
256
287
tag : "connecting" ,
257
288
connectionStringAuthType : "oidc-device-flow" ,
258
289
oidcLoginUrl : flowInfo . verificationUrl ,
@@ -329,3 +360,23 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
329
360
}
330
361
}
331
362
}
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 } ) => {
376
+ const driverOptions = setupDriverConfig ( {
377
+ config : userConfig ,
378
+ defaults : defaultDriverOptions ,
379
+ } ) ;
380
+
381
+ return Promise . resolve ( new MCPConnectionManager ( userConfig , driverOptions , logger , deviceId ) ) ;
382
+ } ;
0 commit comments