Skip to content

Commit fc2ee4c

Browse files
committed
improvement(mcp): dedupe create thunks into buildClient, harden dispose against liveness race
1 parent de3527f commit fc2ee4c

2 files changed

Lines changed: 37 additions & 45 deletions

File tree

apps/sim/lib/mcp/connection-pool.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
* disconnect the client themselves.
1818
*
1919
* Config-change invalidation is the caller's job via `evictServer` (wired to the
20-
* service's `clearCache`), not a per-acquire timestamp check — `updatedAt` is also
21-
* bumped by status telemetry, so it can't distinguish a config edit. Cross-process,
22-
* a config edit is bounded by max age (and self-heals when a stale connection errors).
20+
* service's `evictServerConnections`), not a per-acquire timestamp check — `updatedAt`
21+
* is also bumped by status telemetry, so it can't distinguish a config edit.
22+
* Cross-process, a config edit is bounded by max age (self-heals when a stale
23+
* connection errors).
2324
*/
2425

2526
import { createLogger } from '@sim/logger'
@@ -243,10 +244,13 @@ export class McpConnectionPool {
243244
clearInterval(this.idleCheckTimer)
244245
this.idleCheckTimer = null
245246
}
246-
const clients = [...this.entries.values()].map((e) => e.client)
247+
const entries = [...this.entries.values()]
248+
// Mark retired before disconnecting so an in-flight liveness ping observes it
249+
// and can't hand out a torn-down client (the retired-before-disconnect invariant).
250+
for (const entry of entries) entry.retired = true
247251
this.entries.clear()
248252
this.pending.clear()
249-
void Promise.allSettled(clients.map((client) => client.disconnect()))
253+
void Promise.allSettled(entries.map((entry) => entry.client.disconnect()))
250254
}
251255
}
252256

apps/sim/lib/mcp/service.ts

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,30 @@ class McpService {
336336
return `${serverId}:${workspaceId ?? ''}:${userId ?? ''}`
337337
}
338338

339+
/**
340+
* A `create` thunk for {@link withServerClient} that resolves env vars + SSRF-pins
341+
* and connects. Deferred so a pool hit skips this work; `extraHeaders` (per-request)
342+
* are merged in and force the caller to bypass the pool.
343+
*/
344+
private buildClient(
345+
config: McpServerConfig,
346+
userId: string,
347+
workspaceId: string,
348+
extraHeaders?: Record<string, string>
349+
): () => Promise<McpClient> {
350+
return async () => {
351+
const { config: resolvedConfig, resolvedIP } = await this.resolveConfigEnvVars(
352+
config,
353+
userId,
354+
workspaceId
355+
)
356+
if (extraHeaders) {
357+
resolvedConfig.headers = { ...resolvedConfig.headers, ...extraHeaders }
358+
}
359+
return this.createClient(resolvedConfig, resolvedIP, userId)
360+
}
361+
}
362+
339363
/**
340364
* Run `fn` against a connected client. When `allowPool`, borrow from the warm
341365
* pool (`create` runs only on a miss, so a hit skips env resolution + DNS); a
@@ -399,25 +423,13 @@ class McpService {
399423
}
400424

401425
const hasExtraHeaders = Boolean(extraHeaders && Object.keys(extraHeaders).length > 0)
402-
const create = async () => {
403-
const { config: resolvedConfig, resolvedIP } = await this.resolveConfigEnvVars(
404-
config,
405-
userId,
406-
workspaceId
407-
)
408-
if (hasExtraHeaders) {
409-
resolvedConfig.headers = { ...resolvedConfig.headers, ...extraHeaders }
410-
}
411-
return this.createClient(resolvedConfig, resolvedIP, userId)
412-
}
413-
414426
const result = await this.withServerClient(
415427
{
416428
key: this.poolKey(serverId, workspaceId, userId),
417429
serverId,
418430
allowPool: !hasExtraHeaders,
419431
},
420-
create,
432+
this.buildClient(config, userId, workspaceId, hasExtraHeaders ? extraHeaders : undefined),
421433
(client) => client.callTool(toolCall)
422434
)
423435
logger.info(`[${requestId}] Successfully executed tool ${toolCall.name}`)
@@ -649,21 +661,13 @@ class McpService {
649661
}
650662

651663
try {
652-
const create = async () => {
653-
const { config: resolvedConfig, resolvedIP } = await this.resolveConfigEnvVars(
654-
config,
655-
userId,
656-
workspaceId
657-
)
658-
return this.createClient(resolvedConfig, resolvedIP, userId)
659-
}
660664
const tools = await this.withServerClient(
661665
{
662666
key: this.poolKey(config.id, workspaceId, userId),
663667
serverId: config.id,
664668
allowPool: true,
665669
},
666-
create,
670+
this.buildClient(config, userId, workspaceId),
667671
(client) => client.listTools()
668672
)
669673
logger.debug(
@@ -870,21 +874,13 @@ class McpService {
870874
}
871875
authType = config.authType
872876

873-
const create = async () => {
874-
const { config: resolvedConfig, resolvedIP } = await this.resolveConfigEnvVars(
875-
config,
876-
userId,
877-
workspaceId
878-
)
879-
return this.createClient(resolvedConfig, resolvedIP, userId)
880-
}
881877
const tools = await this.withServerClient(
882878
{
883879
key: this.poolKey(serverId, workspaceId, userId),
884880
serverId,
885881
allowPool: true,
886882
},
887-
create,
883+
this.buildClient(config, userId, workspaceId),
888884
(client) => client.listTools()
889885
)
890886
logger.info(`[${requestId}] Discovered ${tools.length} tools from server ${config.name}`)
@@ -946,21 +942,13 @@ class McpService {
946942

947943
for (const config of servers) {
948944
try {
949-
const create = async () => {
950-
const { config: resolvedConfig, resolvedIP } = await this.resolveConfigEnvVars(
951-
config,
952-
userId,
953-
workspaceId
954-
)
955-
return this.createClient(resolvedConfig, resolvedIP, userId)
956-
}
957945
const tools = await this.withServerClient(
958946
{
959947
key: this.poolKey(config.id, workspaceId, userId),
960948
serverId: config.id,
961949
allowPool: true,
962950
},
963-
create,
951+
this.buildClient(config, userId, workspaceId),
964952
(client) => client.listTools()
965953
)
966954

0 commit comments

Comments
 (0)