MCP host plumbing: lazy-spawn manager, mock server, and agent loop wiring

Gadoes builds the foundation the entire MCP feature set sits on: a singleton process manager for stdio-launched MCP servers, an in-process mock server for unit tests, and the tool-dispatch wiring in the existing assistant loop. Three commits, roughly 1,300 lines of new code, plus a large package-lock addition for `@modelcontextprotocol/sdk`.

integrationinfrastructure

Chunk 1 - McpClientManager. A lazy-spawn singleton at backend/src/lib/mcp/clientManager.ts. Servers aren't started until the first callTool() or listTools() call. Key behavioural details: 15-minute idle TTL with auto-dispose, per-server request queue (max 10 concurrent), exponential backoff on reconnect, and a permanently_failed state after 3 consecutive spawn failures. dispose() runs on SIGTERM/SIGINT. The matching types.ts defines McpServerConfig, McpToolDefinition, McpServerState, McpCallResult, McpHealthResult.

One gotcha: clientManager.ts imports a circuitBreaker module from Chunk 18, but circuitBreaker.ts was never committed to this branch - it lived untracked until the cleanup commit e29591e. On a clean checkout, the build will not link without that file. Take e29591e (or cherry-pick circuitBreaker.ts and circuitBreaker.test.ts directly) before trying to build.

Chunk 2 - Mock MCP server. backend/src/__mocks__/mockMcpServer.ts uses @modelcontextprotocol/sdk's InMemoryTransport to run a real MCP server in-process. Tests configure scripted responses via respondWith(toolName, response) and call cleanup() when done. No child processes, no port allocation. This is the infrastructure that makes the per-source adapter tests work.

Chunk 3 - Agent loop wiring. chatTools.ts gets isMcpTool(), parseMcpToolName(), and mcpToolToOpenAI(). The naming convention is mcp__{serverName}__{toolName}. runToolCalls() dispatches matching tool calls to McpClientManager.callTool(), emitting an mcp_tool_call_start SSE event before the call so the UI can show a spinner. Open-circuit sources are removed from activeTools and an unavailability note is appended to the system prompt. runLLMStream() accepts mcpServerTools and openCircuitSources parameters.

Package changes: @modelcontextprotocol/sdk@^1.29.0 as a runtime dependency, @modelcontextprotocol/server-filesystem and vitest@^4.1.5 as dev dependencies. The package-lock diff is 3,200+ lines.

So what Every later MCP chunk in this fork depends on these three commits. The concurrency model (queue, backoff, idle TTL) is solid, and the mock server is the kind of test infra that earns its keep across a whole feature rollout. The missing `circuitBreaker.ts` is the main adoption trap - read the note above before checking out Chunk 1.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

6 commits from Gadoes/dispumike, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
735c9261 Chunk 1: MCP Host Plumbing (fix test suite) Gadoes 2026-05-02 ↗ GitHub
commit body
- Fixed StdioClientTransport mock to use class constructor (arrow fn can't be used with `new`)
- Fixed callTool() to catch spawn errors and return structured source_unavailable error
- Fixed afterEach to use resetAllMocks() instead of clearAllMocks() to prevent mockImplementationOnce queue leakage between tests
- Fixed dispose() test to clear mockClose call count before the assertion

All 10 Chunk 1 tests pass.

Acceptance criteria:
[x] listTools() returns at least one tool from a running MCP server
[x] No spawn occurs on second warm call within idle TTL
[x] Child process exit triggers reconnect within 1s (fake timers)
[x] 3 consecutive spawn failures → permanently_failed state
[x] dispose() kills all spawned processes (spy verification)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8af29ffc Chunk 1: MCP host plumbing - McpClientManager lazy-spawn singleton Gadoes 2026-05-02 ↗ GitHub
commit body
- Added spike-mcp-host.ts POC: validates StdioClientTransport + server-filesystem stdio server (14 tools, listTools + callTool both pass)
- Implemented McpClientManager singleton with lazy-spawn, 15-min idle TTL, per-server request queue (max 10), auto-reconnect with exponential backoff, permanently_failed state after 3 failures, and dispose() for SIGTERM/SIGINT cleanup
- Added types.ts with McpServerConfig, McpToolDefinition, McpServerState, McpCallResult, McpHealthResult
- Installed @modelcontextprotocol/server-filesystem, vitest, @vitest/coverage-v8
- All 10 Vitest tests pass (cold-start, warm reuse, idle disposal, reconnect, permanently_failed, queue throttle, dispose, health check)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
da93e2e5 Chunk 2: Mock MCP Server for Tests Gadoes 2026-05-02 ↗ GitHub
commit body
- Created backend/src/__mocks__/mockMcpServer.ts - in-process MCP server
  using @modelcontextprotocol/sdk's InMemoryTransport + Server
- Pre-defined tool set: search_cases, retrieve_case
- respondWith(toolName, response) for scripted responses
- createMockMcpServer() returns started server + connected McpClientManager
- 5 unit tests: scripted responses, listTools, respondWith overrides,
  default responses, unknown tool error

Acceptance criteria:
[x] createMockMcpServer() usable in any Vitest test file without spawning child processes
[x] Returns deterministic responses per respondWith() config

Tests: 15 pass (10 Chunk 1 + 5 Chunk 2)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
f4c51b40 Merge Chunk 2: Mock MCP Server for Tests Gadoes 2026-05-02 ↗ GitHub
4f4fdc5d Chunk 3: Tool Registration in Agent Loop Gadoes 2026-05-02 ↗ GitHub
commit body
- Added isMcpTool(), parseMcpToolName(), mcpToolToOpenAI() helpers in chatTools.ts
- Extended runToolCalls() with optional mcpManager param; detects mcp__ prefix
  and dispatches to McpClientManager.callTool()
- Emits mcp_tool_call_start SSE event per MCP tool call
- Extended runLLMStream() with mcpServerTools, openCircuitSources, mcpManager params
- MCP tools appended to activeTools after existing TOOLS and WORKFLOW_TOOLS
- Open-circuit sources excluded from activeTools; unavailability note appended
  to system prompt (spec Section 2.7)
- Active MCP source names and tools listed in system prompt

Acceptance criteria:
[x] LLM emits MCP tool call by prefixed name (mcp__{server}__{tool})
[x] runToolCalls() dispatches to McpClientManager
[x] Tool result returned in { role: "tool", tool_call_id, content } format
[x] mcp_tool_call_start SSE event emitted with correct fields
[x] Open-circuit source tools absent from activeTools

Tests: 24 pass (10+5+9)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3091a892 Merge Chunk 3: Tool Registration in Agent Loop Gadoes 2026-05-02 ↗ GitHub

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-26.md from inside the repo you want the changes in.

⬇ Download capture-thread-26.md