Skip to content

feat(den): move the external MCP session pool inside @openwork/enterprise-mcp-client, then retire the SDK client path #3493

Description

@benjaminshafii

Context

PR #3208 introduced an in-process session pool for external MCP capability operations (one downstream handshake reused across search_capabilities/execute_capability instead of a fresh initialize per operation). While writing its e2e spec we found the pool was bypassed on the live path: external-mcp-client-runtime.ts had standardized list/call/inspect on @openwork/enterprise-mcp-client (#2810), while the pool lives in the den-api SDK client (external-mcp-client.ts).

The stopgap shipped in #3208 flips tool operations (listExternalMcpTools / callExternalMcpTool / inspectExternalMcpToolCall) back to the pooled SDK client, while connect/OAuth stays on the enterprise client. That splits one logical concern across two clients and partially reverses #2810's standardization.

Goal

Make @openwork/enterprise-mcp-client the single runtime again — now with pooling:

  1. Move the session pool inside the enterprise client (or its Den adapter). runEnterpriseMcpOperation in ee/apps/den-api/src/capability-sources/enterprise-mcp-client-adapter.ts currently builds a client per operation and already threads lifecycleDeadline — that is where pooled acquisition slots in. Preserve the feat(den): reuse downstream MCP sessions across capability operations #3208 pool contract: per-member keying (connection row revision + membership + connected-account revision), idle/absolute TTLs, LRU, single-flight connects, evict + exactly one retry on session-shaped failures, invalidation hooks on token save/refresh/invalidate and connection mutations, and the DEN_EXTERNAL_MCP_SESSION_REUSE=0 kill switch.
  2. Flip external-mcp-client-runtime.ts tool ops back to the enterprise client.
  3. Retire the SDK client path (external-mcp-client.ts) once nothing routes through it.

Safety net

evals/specs/org-connector-session-reuse.test.ts proves the behavior from outside Den and is client-agnostic:

  • search→execute = one downstream initialize, same session id;
  • member isolation (distinct session id + token per member, negative halves asserted);
  • credential rotation forces a fresh handshake;
  • killed session → evict + exactly one retry;
  • kill switch restores session-per-operation.

It must stay green through steps 1–3 unchanged. It has already demonstrated it discriminates: with tool ops routed to the (unpooled) enterprise client, the reuse claim fails.

Non-goals

  • Any change to OAuth/DCR/PKCE handling — that is exactly what the enterprise client owns today and why we are consolidating onto it rather than away from it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions