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.
src/server.ts: add sessionMcpServers = new Map<string, McpServer[]>() beside sessionCwds; do not clear it at materialization; remove it on close together with sessionCwds.
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.
- 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.
Summary
#42 (fixed in 767224c) forwards the ACP client's
mcpServersto the backend'ssession/createonce, when the lazy session materializes. The backend treatsmcpServersas 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
mainat 8f61b4a (0.38.0).The ACP client passes a stdio server (
{ name, command, args, env }) insession/newmcpServers. Its tools are available on the first prompt. After the session idles pastBACKEND_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:274and:361:pendingSessions.set(…, { cwd, mcpServers: params.mcpServers })is the only place the set is kept.:522–524: forwarded tosession/createat materialization.:535:pendingSessions.delete(acpSid); after this nothing remembers them. The only per-session state that survives materialization issessionCwds.reloadBackendSession(:2451):zcParamsis{ sessionId, workspace }only (:2457–2459), so the backend resume carries nomcpServers. It is called from theensureRealSessionidle-eviction reload (:457), the-32004 session is not activerecovery (:1324), backend respawn (:1571) and the post-close drain (:2146).loadSessionbuilds its resume params withsessionId/workspaceonly, whileresumeSessionforwardsparams.mcpServers(:795–796).:493) rebuildspending = { cwd }, andLazySessionRecord(src/lazy-sessions.ts:41–46) has nomcpServersfield, so a bridge restart also loses them.zcode.cjs, CLI 0.16.5):session/createandsession/resumeboth acceptmcpServersand both runprotocolMcpServersToRuntimeMcpConfig, which returnsundefinedfor an absent or empty array, so a resume without them yields nomcpruntime 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.
src/server.ts: addsessionMcpServers = new Map<string, McpServer[]>()besidesessionCwds; do not clear it at materialization; remove it on close together withsessionCwds.src/handlers/session.ts: populate it innewSession(274, 361),resumeSession(795) andloadSession; read it in theensureRealSessioncreate path (522), which also repairs 493; addmcpServerstozcParamsinreloadBackendSession(2457) when known; forwardparams.mcpServersinloadSessionto matchresumeSession.mcpServersinLazySessionRecordso the set survives a bridge restart.Tests:
session/newwithmcpServersfollowed by an eviction reload asserts the backendsession/resumeparams carry the same servers;session/loadforwards 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.