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" ;
72import 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" ;
96import 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 " ;
1512
1613export interface AtlasClusterConnectionInfo {
1714 username : string ;
@@ -71,39 +68,76 @@ export interface ConnectionManagerEvents {
7168 "connection-error" : [ ConnectionStateErrored ] ;
7269}
7370
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" > ;
7587 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 {
76120 private deviceId : DeviceId ;
77- private clientName : string ;
78121 private bus : EventEmitter ;
79122
80123 constructor (
81124 private userConfig : UserConfig ,
82125 private driverOptions : DriverOptions ,
83- private logger : CompositeLogger ,
126+ private logger : LoggerBase ,
84127 deviceId : DeviceId ,
85128 bus ?: EventEmitter
86129 ) {
87130 super ( ) ;
88-
89131 this . bus = bus ?? new EventEmitter ( ) ;
90- this . state = { tag : "disconnected" } ;
91-
92132 this . bus . on ( "mongodb-oidc-plugin:auth-failed" , this . onOidcAuthFailed . bind ( this ) ) ;
93133 this . bus . on ( "mongodb-oidc-plugin:auth-succeeded" , this . onOidcAuthSucceeded . bind ( this ) ) ;
94-
95134 this . deviceId = deviceId ;
96- this . clientName = "unknown" ;
97- }
98-
99- setClientName ( clientName : string ) : void {
100- this . clientName = clientName ;
101135 }
102136
103137 async connect ( settings : ConnectionSettings ) : Promise < AnyConnectionState > {
104- this . emit ( "connection-request" , this . state ) ;
138+ this . _events . emit ( "connection-request" , this . currentConnectionState ) ;
105139
106- if ( this . state . tag === "connected" || this . state . tag === "connecting" ) {
140+ if ( this . currentConnectionState . tag === "connected" || this . currentConnectionState . tag === "connecting" ) {
107141 await this . disconnect ( ) ;
108142 }
109143
@@ -138,7 +172,7 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
138172 connectionInfo . driverOptions . proxy ??= { useEnvironmentVariableProxies : true } ;
139173 connectionInfo . driverOptions . applyProxyToOIDC ??= true ;
140174
141- connectionStringAuthType = ConnectionManager . inferConnectionTypeFromSettings (
175+ connectionStringAuthType = MCPConnectionManager . inferConnectionTypeFromSettings (
142176 this . userConfig ,
143177 connectionInfo
144178 ) ;
@@ -165,7 +199,10 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
165199 }
166200
167201 try {
168- const connectionType = ConnectionManager . inferConnectionTypeFromSettings ( this . userConfig , connectionInfo ) ;
202+ const connectionType = MCPConnectionManager . inferConnectionTypeFromSettings (
203+ this . userConfig ,
204+ connectionInfo
205+ ) ;
169206 if ( connectionType . startsWith ( "oidc" ) ) {
170207 void this . pingAndForget ( serviceProvider ) ;
171208
@@ -199,13 +236,13 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
199236 }
200237
201238 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 ;
204241 }
205242
206- if ( this . state . tag === "connected" || this . state . tag === "connecting" ) {
243+ if ( this . currentConnectionState . tag === "connected" || this . currentConnectionState . tag === "connecting" ) {
207244 try {
208- await this . state . serviceProvider ?. close ( true ) ;
245+ await this . currentConnectionState . serviceProvider ?. close ( true ) ;
209246 } finally {
210247 this . changeState ( "connection-close" , {
211248 tag : "disconnected" ,
@@ -216,30 +253,21 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
216253 return { tag : "disconnected" } ;
217254 }
218255
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-
234256 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+ ) {
236261 void this . disconnectOnOidcError ( error ) ;
237262 }
238263 }
239264
240265 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" } ) ;
243271 }
244272
245273 this . logger . info ( {
@@ -250,9 +278,12 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
250278 }
251279
252280 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+ ) {
254285 this . changeState ( "connection-request" , {
255- ...this . state ,
286+ ...this . currentConnectionState ,
256287 tag : "connecting" ,
257288 connectionStringAuthType : "oidc-device-flow" ,
258289 oidcLoginUrl : flowInfo . verificationUrl ,
@@ -329,3 +360,23 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
329360 }
330361 }
331362}
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