@@ -64,11 +64,21 @@ interface PoolEntry {
6464 closing : boolean
6565}
6666
67+ /**
68+ * An in-flight `createEntry`. `evictServer` sets `invalidated` on the records for its
69+ * server so a connect racing a config edit/delete is one-shot instead of pooled stale.
70+ * Lives only until the create settles (cleared in `finally`), so it's self-bounded.
71+ */
72+ interface PendingCreate {
73+ serverId : string
74+ /** `flag.invalidated` is set by `evictServer` racing this create → it one-shots instead of pooling. */
75+ flag : { invalidated : boolean }
76+ promise : Promise < PoolEntry >
77+ }
78+
6779export class McpConnectionPool {
6880 private entries = new Map < string , PoolEntry > ( )
69- private pending = new Map < string , Promise < PoolEntry > > ( )
70- /** Per-server counter bumped by `evictServer`; an in-flight create built against an older value is retired on completion. */
71- private serverGenerations = new Map < string , number > ( )
81+ private pending = new Map < string , PendingCreate > ( )
7282 private idleCheckTimer : ReturnType < typeof setInterval > | null = null
7383 private disposed = false
7484
@@ -91,7 +101,7 @@ export class McpConnectionPool {
91101 private async resolveEntry ( params : AcquireParams ) : Promise < PoolEntry > {
92102 while ( true ) {
93103 const pending = this . pending . get ( params . key )
94- if ( pending ) return pending
104+ if ( pending ) return pending . promise
95105
96106 const current = this . entries . get ( params . key )
97107 if ( ! current ) return this . createEntry ( params )
@@ -136,10 +146,12 @@ export class McpConnectionPool {
136146 // Re-check: the `await` in `resolveEntry` yields, so two acquires can both
137147 // reach here; the first registers the pending create, the rest join.
138148 const inFlight = this . pending . get ( params . key )
139- if ( inFlight ) return inFlight
149+ if ( inFlight ) return inFlight . promise
140150
151+ // Declared before the create closure so `evictServer` can invalidate a create
152+ // that's racing a config edit/delete (the closure reads it after its `await`).
153+ const flag = { invalidated : false }
141154 const creation = ( async ( ) => {
142- const generation = this . serverGenerations . get ( params . serverId ) ?? 0
143155 const client = await params . create ( )
144156 const now = Date . now ( )
145157 const entry : PoolEntry = {
@@ -157,7 +169,7 @@ export class McpConnectionPool {
157169 // don't pool a connection built against the now-stale config. The current
158170 // borrower still uses it once (as connect-per-op would for an in-flight
159171 // request); it's disconnected on release, and the next acquire builds fresh.
160- if ( this . disposed || ( this . serverGenerations . get ( params . serverId ) ?? 0 ) !== generation ) {
172+ if ( this . disposed || flag . invalidated ) {
161173 entry . retired = true
162174 return entry
163175 }
@@ -171,10 +183,15 @@ export class McpConnectionPool {
171183 return entry
172184 } ) ( )
173185
174- this . pending . set ( params . key , creation )
175- return creation . finally ( ( ) => {
176- if ( this . pending . get ( params . key ) === creation ) this . pending . delete ( params . key )
177- } )
186+ const record : PendingCreate = {
187+ serverId : params . serverId ,
188+ flag,
189+ promise : creation . finally ( ( ) => {
190+ if ( this . pending . get ( params . key ) === record ) this . pending . delete ( params . key )
191+ } ) ,
192+ }
193+ this . pending . set ( params . key , record )
194+ return record . promise
178195 }
179196
180197 private async release ( entry : PoolEntry , poison : boolean ) : Promise < void > {
@@ -215,9 +232,11 @@ export class McpConnectionPool {
215232
216233 /** Retire every connection for a server (all users) — config changed or deleted. */
217234 async evictServer ( serverId : string , reason : string ) : Promise < void > {
218- // Bump first so an in-flight create for this server retires on completion
219- // instead of pooling a connection built against the now-stale config.
220- this . serverGenerations . set ( serverId , ( this . serverGenerations . get ( serverId ) ?? 0 ) + 1 )
235+ // Flag in-flight creates first so one racing this eviction is one-shot on
236+ // completion instead of pooling a connection built against the now-stale config.
237+ for ( const record of this . pending . values ( ) ) {
238+ if ( record . serverId === serverId ) record . flag . invalidated = true
239+ }
221240 for ( const entry of this . entries . values ( ) ) {
222241 if ( entry . serverId === serverId ) this . retire ( entry , reason )
223242 }
@@ -262,7 +281,6 @@ export class McpConnectionPool {
262281 for ( const entry of entries ) entry . retired = true
263282 this . entries . clear ( )
264283 this . pending . clear ( )
265- this . serverGenerations . clear ( )
266284 void Promise . allSettled ( entries . map ( ( entry ) => entry . client . disconnect ( ) ) )
267285 }
268286}
0 commit comments