Second slice (docs/MCP.md). Still wired to nothing: S3 is the router line in
agent.js. Split pure/IO the way the codebase already does (sse.js thin IO,
translate.js pure), so the parts most likely to be wrong are testable without
spawning:
- mcpProtocol.js (pure) — newline-delimited JSON-RPC framing and tool-result
flattening. Both are genuinely easy to get wrong: a pipe does not respect
message boundaries, so a framer that assumes "one chunk = one message" works
until a big tools/list arrives; and a stray non-JSON log line on stdout (a
well-known MCP footgun) must be skipped rather than desync the stream. Results
are typed content blocks but tool_result is a STRING, so non-text degrades to a
placeholder (never raw base64), failures take the agent's `ERROR: ` prefix, and
the whole thing is capped — the generic tool path bounds nothing.
- mcpClient.js (IO) — spawn, handshake, request/response, teardown.
Four things it owes the agent that the generic tool path does not provide:
timeouts (tools run sequentially, so one wedged server would hang the whole run),
a capped flattened string, call() that NEVER throws (a failure returns an
`ERROR: ` string so one bad server cannot break the turn loop), and deterministic
reaping via reapMcp() for New Chat / deactivate — a detached child otherwise
outlives the window, exactly the reason reapCommands() exists.
Security choices: shell:false (args are argv, never re-parsed by a shell), and
server→client requests (sampling/*, elicitation/*) are explicitly REFUSED rather
than ignored — a server must not be able to drive our model or prompt the user.
This module deliberately does not decide whether a server MAY start; that gate is
S4.
Tested against a real subprocess, not mocks: test/fixtures/mock-mcp-server.js
speaks just enough protocol to drive the client, with tools that succeed, fail,
and never answer, plus --noise/--crash/--sample modes. So the handshake, framing
over a live pipe, the timeout, the sampling refusal and process teardown are all
actually exercised.
Verified: 15 protocol + 13 client tests pass; the full CI gate runs 18 extension
suites with 0 failures; requires are child_process + the pure module only (no
vscode); and `ps` is clean after the run — dispose() and reapMcp() genuinely kill
the process group, asserted in-test by polling the pid.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second MCP slice (docs/MCP.md). Stacked on #29 — the diff here is S2 only. Still wired to nothing; S3 is the router line in agent.js.
Pure / IO split
Following the codebase's own pattern (sse.js thin IO, translate.js pure), the parts most likely to be wrong are testable without spawning anything:
mcpProtocol.js (pure) — framing + result flattening. Both are easy to get wrong:
mcpClient.js (IO) — spawn, handshake, request/response, teardown.
Four things it owes the agent
Security choices
Tested against a real subprocess, not mocks
test/fixtures/mock-mcp-server.js speaks just enough protocol to drive the client, with tools that succeed, fail, and never answer, plus --noise / --crash / --sample modes. So the handshake, framing over a live pipe, the timeout, the sampling refusal, and process teardown are genuinely exercised.
Verification
Next
S3 wires it in — TOOLS/TOOLS_TOKENS_EST become per-run, and the router goes immediately before the unknown tool fallthrough at agent.js:442. Then S4 (trust-on-first-use + the approval card), which must not be skipped.
🤖 Generated with Claude Code