Skip to content

Client mcpServers survive only until the lazy session materializes; every backend reload recreates the session without them (follow-up to #42) #193

Description

@jbellis

Summary

#42 (fixed in 767224c) forwards the ACP client's mcpServers to the backend's session/create once, when the lazy session materializes. The backend treats mcpServers as per-load runtime config rather than persisted session state, and the bridge forgets the set after materialization, so every later backend reload rebuilds the session with no client MCP servers. In practice, a session that sits idle for more than five minutes loses its client MCP tools on the next prompt, for the rest of its life.

Observed

zcode-acp 0.37.x, ZCode desktop 3.11.2 / CLI 0.16.5. Line numbers below are from main at 8f61b4a (0.38.0).

The ACP client passes a stdio server ({ name, command, args, env }) in session/new mcpServers. Its tools are available on the first prompt. After the session idles past BACKEND_RESIDENT_TTL_MS (5 min, src/server.ts:77) and receives another prompt, the tools are gone and do not come back. The same happens after a backend respawn.

Root cause

  • src/handlers/session.ts:274 and :361: pendingSessions.set(…, { cwd, mcpServers: params.mcpServers }) is the only place the set is kept.
  • :522–524: forwarded to session/create at materialization. :535: pendingSessions.delete(acpSid); after this nothing remembers them. The only per-session state that survives materialization is sessionCwds.
  • reloadBackendSession (:2451): zcParams is { sessionId, workspace } only (:2457–2459), so the backend resume carries no mcpServers. It is called from the ensureRealSession idle-eviction reload (:457), the -32004 session is not active recovery (:1324), backend respawn (:1571) and the post-close drain (:2146).
  • loadSession builds its resume params with sessionId/workspace only, while resumeSession forwards params.mcpServers (:795–796).
  • Durable-record rehydration (:493) rebuilds pending = { cwd }, and LazySessionRecord (src/lazy-sessions.ts:41–46) has no mcpServers field, so a bridge restart also loses them.
  • Backend side (zcode.cjs, CLI 0.16.5): session/create and session/resume both accept mcpServers and both run protocolMcpServersToRuntimeMcpConfig, which returns undefined for an absent or empty array, so a resume without them yields no mcp runtime config. The servers are not persisted with the session record. That is the contract the bridge has to satisfy on every load, not only the first.

Suggested fix

Make the client's MCP set session-lifetime state and re-send it on every backend load.

  1. src/server.ts: add sessionMcpServers = new Map<string, McpServer[]>() beside sessionCwds; do not clear it at materialization; remove it on close together with sessionCwds.
  2. src/handlers/session.ts: populate it in newSession (274, 361), resumeSession (795) and loadSession; read it in the ensureRealSession create path (522), which also repairs 493; add mcpServers to zcParams in reloadBackendSession (2457) when known; forward params.mcpServers in loadSession to match resumeSession.
  3. Optionally persist mcpServers in LazySessionRecord so the set survives a bridge restart.

Tests: session/new with mcpServers followed by an eviction reload asserts the backend session/resume params carry the same servers; session/load forwards them; alias-store round trip.

Why it looks intermittent

Short sessions finish inside the five-minute window and never hit a reload. Only long, intermittently idle sessions show it, which makes it easy to misattribute to the client.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions