11import { 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" ;
312
413export interface AtlasClusterConnectionInfo {
514 username : string ;
@@ -54,12 +63,12 @@ export interface ConnectionManagerEvents {
5463 "connection-errored" : [ ConnectionStateErrored ] ;
5564}
5665
57- export interface MCPConnectParams {
66+ export interface ConnectionSettings {
5867 connectionString : string ;
5968 atlas ?: AtlasClusterConnectionInfo ;
6069}
6170
62- export abstract class ConnectionManager < ConnectParams extends MCPConnectParams = MCPConnectParams > {
71+ export abstract class ConnectionManager {
6372 protected clientName : string = "unknown" ;
6473
6574 protected readonly _events = new EventEmitter < ConnectionManagerEvents > ( ) ;
@@ -86,7 +95,236 @@ export abstract class ConnectionManager<ConnectParams extends MCPConnectParams =
8695 this . clientName = clientName ;
8796 }
8897
89- abstract connect ( connectParams : ConnectParams ) : Promise < AnyConnectionState > ;
98+ abstract connect ( settings : ConnectionSettings ) : Promise < AnyConnectionState > ;
9099
91100 abstract disconnect ( ) : Promise < ConnectionStateDisconnected | ConnectionStateErrored > ;
92101}
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