1
1
import { EventEmitter } from "events" ;
2
- import type { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver" ;
2
+ import type { MongoClientOptions } from "mongodb" ;
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" ;
6
+ import type { DeviceId } from "../helpers/deviceId.js" ;
7
+ import type { DriverOptions , UserConfig } from "./config.js" ;
8
+ import { MongoDBError , ErrorCodes } from "./errors.js" ;
9
+ import { type CompositeLogger , LogId } from "./logger.js" ;
10
+ import { packageInfo } from "./packageInfo.js" ;
11
+ import { type AppNameComponents , setAppNameParamIfMissing } from "../helpers/connectionOptions.js" ;
3
12
4
13
export interface AtlasClusterConnectionInfo {
5
14
username : string ;
@@ -54,12 +63,12 @@ export interface ConnectionManagerEvents {
54
63
"connection-errored" : [ ConnectionStateErrored ] ;
55
64
}
56
65
57
- export interface MCPConnectParams {
66
+ export interface ConnectionSettings {
58
67
connectionString : string ;
59
68
atlas ?: AtlasClusterConnectionInfo ;
60
69
}
61
70
62
- export abstract class ConnectionManager < ConnectParams extends MCPConnectParams = MCPConnectParams > {
71
+ export abstract class ConnectionManager {
63
72
protected clientName : string = "unknown" ;
64
73
65
74
protected readonly _events = new EventEmitter < ConnectionManagerEvents > ( ) ;
@@ -86,7 +95,236 @@ export abstract class ConnectionManager<ConnectParams extends MCPConnectParams =
86
95
this . clientName = clientName ;
87
96
}
88
97
89
- abstract connect ( connectParams : ConnectParams ) : Promise < AnyConnectionState > ;
98
+ abstract connect ( settings : ConnectionSettings ) : Promise < AnyConnectionState > ;
90
99
91
100
abstract disconnect ( ) : Promise < ConnectionStateDisconnected | ConnectionStateErrored > ;
92
101
}
102
+
103
+ export class MCPConnectionManager extends ConnectionManager {
104
+ private deviceId : DeviceId ;
105
+ private bus : EventEmitter ;
106
+
107
+ constructor (
108
+ private userConfig : UserConfig ,
109
+ private driverOptions : DriverOptions ,
110
+ private logger : CompositeLogger ,
111
+ deviceId : DeviceId ,
112
+ bus ?: EventEmitter
113
+ ) {
114
+ super ( ) ;
115
+ this . bus = bus ?? new EventEmitter ( ) ;
116
+ this . bus . on ( "mongodb-oidc-plugin:auth-failed" , this . onOidcAuthFailed . bind ( this ) ) ;
117
+ this . bus . on ( "mongodb-oidc-plugin:auth-succeeded" , this . onOidcAuthSucceeded . bind ( this ) ) ;
118
+ this . deviceId = deviceId ;
119
+ this . clientName = "unknown" ;
120
+ }
121
+
122
+ async connect ( connectParams : ConnectionSettings ) : Promise < AnyConnectionState > {
123
+ this . _events . emit ( "connection-requested" , this . state ) ;
124
+
125
+ if ( this . state . tag === "connected" || this . state . tag === "connecting" ) {
126
+ await this . disconnect ( ) ;
127
+ }
128
+
129
+ let serviceProvider : NodeDriverServiceProvider ;
130
+ let connectionInfo : ConnectionInfo ;
131
+
132
+ try {
133
+ connectParams = { ...connectParams } ;
134
+ const appNameComponents : AppNameComponents = {
135
+ appName : `${ packageInfo . mcpServerName } ${ packageInfo . version } ` ,
136
+ deviceId : this . deviceId . get ( ) ,
137
+ clientName : this . clientName ,
138
+ } ;
139
+
140
+ connectParams . connectionString = await setAppNameParamIfMissing ( {
141
+ connectionString : connectParams . connectionString ,
142
+ components : appNameComponents ,
143
+ } ) ;
144
+
145
+ connectionInfo = generateConnectionInfoFromCliArgs ( {
146
+ ...this . userConfig ,
147
+ ...this . driverOptions ,
148
+ connectionSpecifier : connectParams . connectionString ,
149
+ } ) ;
150
+
151
+ if ( connectionInfo . driverOptions . oidc ) {
152
+ connectionInfo . driverOptions . oidc . allowedFlows ??= [ "auth-code" ] ;
153
+ connectionInfo . driverOptions . oidc . notifyDeviceFlow ??= this . onOidcNotifyDeviceFlow . bind ( this ) ;
154
+ }
155
+
156
+ connectionInfo . driverOptions . proxy ??= { useEnvironmentVariableProxies : true } ;
157
+ connectionInfo . driverOptions . applyProxyToOIDC ??= true ;
158
+
159
+ serviceProvider = await NodeDriverServiceProvider . connect (
160
+ connectionInfo . connectionString ,
161
+ {
162
+ productDocsLink : "https://github.com/mongodb-js/mongodb-mcp-server/" ,
163
+ productName : "MongoDB MCP" ,
164
+ ...connectionInfo . driverOptions ,
165
+ } ,
166
+ undefined ,
167
+ this . bus
168
+ ) ;
169
+ } catch ( error : unknown ) {
170
+ const errorReason = error instanceof Error ? error . message : `${ error as string } ` ;
171
+ this . changeState ( "connection-errored" , {
172
+ tag : "errored" ,
173
+ errorReason,
174
+ connectedAtlasCluster : connectParams . atlas ,
175
+ } ) ;
176
+ throw new MongoDBError ( ErrorCodes . MisconfiguredConnectionString , errorReason ) ;
177
+ }
178
+
179
+ try {
180
+ const connectionType = MCPConnectionManager . inferConnectionTypeFromSettings (
181
+ this . userConfig ,
182
+ connectionInfo
183
+ ) ;
184
+ if ( connectionType . startsWith ( "oidc" ) ) {
185
+ void this . pingAndForget ( serviceProvider ) ;
186
+
187
+ return this . changeState ( "connection-requested" , {
188
+ tag : "connecting" ,
189
+ connectedAtlasCluster : connectParams . atlas ,
190
+ serviceProvider,
191
+ connectionStringAuthType : connectionType ,
192
+ oidcConnectionType : connectionType as OIDCConnectionAuthType ,
193
+ } ) ;
194
+ }
195
+
196
+ await serviceProvider ?. runCommand ?.( "admin" , { hello : 1 } ) ;
197
+
198
+ return this . changeState ( "connection-succeeded" , {
199
+ tag : "connected" ,
200
+ connectedAtlasCluster : connectParams . atlas ,
201
+ serviceProvider,
202
+ connectionStringAuthType : connectionType ,
203
+ } ) ;
204
+ } catch ( error : unknown ) {
205
+ const errorReason = error instanceof Error ? error . message : `${ error as string } ` ;
206
+ this . changeState ( "connection-errored" , {
207
+ tag : "errored" ,
208
+ errorReason,
209
+ connectedAtlasCluster : connectParams . atlas ,
210
+ } ) ;
211
+ throw new MongoDBError ( ErrorCodes . NotConnectedToMongoDB , errorReason ) ;
212
+ }
213
+ }
214
+
215
+ async disconnect ( ) : Promise < ConnectionStateDisconnected | ConnectionStateErrored > {
216
+ if ( this . state . tag === "disconnected" || this . state . tag === "errored" ) {
217
+ return this . state ;
218
+ }
219
+
220
+ if ( this . state . tag === "connected" || this . state . tag === "connecting" ) {
221
+ try {
222
+ await this . state . serviceProvider ?. close ( true ) ;
223
+ } finally {
224
+ this . changeState ( "connection-closed" , {
225
+ tag : "disconnected" ,
226
+ } ) ;
227
+ }
228
+ }
229
+
230
+ return { tag : "disconnected" } ;
231
+ }
232
+
233
+ private onOidcAuthFailed ( error : unknown ) : void {
234
+ if ( this . state . tag === "connecting" && this . state . connectionStringAuthType ?. startsWith ( "oidc" ) ) {
235
+ void this . disconnectOnOidcError ( error ) ;
236
+ }
237
+ }
238
+
239
+ private onOidcAuthSucceeded ( ) : void {
240
+ if ( this . state . tag === "connecting" && this . state . connectionStringAuthType ?. startsWith ( "oidc" ) ) {
241
+ this . changeState ( "connection-succeeded" , { ...this . state , tag : "connected" } ) ;
242
+ }
243
+
244
+ this . logger . info ( {
245
+ id : LogId . oidcFlow ,
246
+ context : "mongodb-oidc-plugin:auth-succeeded" ,
247
+ message : "Authenticated successfully." ,
248
+ } ) ;
249
+ }
250
+
251
+ private onOidcNotifyDeviceFlow ( flowInfo : { verificationUrl : string ; userCode : string } ) : void {
252
+ if ( this . state . tag === "connecting" && this . state . connectionStringAuthType ?. startsWith ( "oidc" ) ) {
253
+ this . changeState ( "connection-requested" , {
254
+ ...this . state ,
255
+ tag : "connecting" ,
256
+ connectionStringAuthType : "oidc-device-flow" ,
257
+ oidcLoginUrl : flowInfo . verificationUrl ,
258
+ oidcUserCode : flowInfo . userCode ,
259
+ } ) ;
260
+ }
261
+
262
+ this . logger . info ( {
263
+ id : LogId . oidcFlow ,
264
+ context : "mongodb-oidc-plugin:notify-device-flow" ,
265
+ message : "OIDC Flow changed automatically to device flow." ,
266
+ } ) ;
267
+ }
268
+
269
+ static inferConnectionTypeFromSettings (
270
+ config : UserConfig ,
271
+ settings : { connectionString : string }
272
+ ) : ConnectionStringAuthType {
273
+ const connString = new ConnectionString ( settings . connectionString ) ;
274
+ const searchParams = connString . typedSearchParams < MongoClientOptions > ( ) ;
275
+
276
+ switch ( searchParams . get ( "authMechanism" ) ) {
277
+ case "MONGODB-OIDC" : {
278
+ if ( config . transport === "stdio" && config . browser ) {
279
+ return "oidc-auth-flow" ;
280
+ }
281
+
282
+ if ( config . transport === "http" && config . httpHost === "127.0.0.1" && config . browser ) {
283
+ return "oidc-auth-flow" ;
284
+ }
285
+
286
+ return "oidc-device-flow" ;
287
+ }
288
+ case "MONGODB-X509" :
289
+ return "x.509" ;
290
+ case "GSSAPI" :
291
+ return "kerberos" ;
292
+ case "PLAIN" :
293
+ if ( searchParams . get ( "authSource" ) === "$external" ) {
294
+ return "ldap" ;
295
+ }
296
+ return "scram" ;
297
+ // default should catch also null, but eslint complains
298
+ // about it.
299
+ case null :
300
+ default :
301
+ return "scram" ;
302
+ }
303
+ }
304
+
305
+ private async pingAndForget ( serviceProvider : NodeDriverServiceProvider ) : Promise < void > {
306
+ try {
307
+ await serviceProvider ?. runCommand ?.( "admin" , { hello : 1 } ) ;
308
+ } catch ( error : unknown ) {
309
+ this . logger . warning ( {
310
+ id : LogId . oidcFlow ,
311
+ context : "pingAndForget" ,
312
+ message : String ( error ) ,
313
+ } ) ;
314
+ }
315
+ }
316
+
317
+ private async disconnectOnOidcError ( error : unknown ) : Promise < void > {
318
+ try {
319
+ await this . disconnect ( ) ;
320
+ } catch ( error : unknown ) {
321
+ this . logger . warning ( {
322
+ id : LogId . oidcFlow ,
323
+ context : "disconnectOnOidcError" ,
324
+ message : String ( error ) ,
325
+ } ) ;
326
+ } finally {
327
+ this . changeState ( "connection-errored" , { tag : "errored" , errorReason : String ( error ) } ) ;
328
+ }
329
+ }
330
+ }
0 commit comments