fix(cli): stop the MCP stdio bridge serialising requests #92
Reference in New Issue
Block a user
Delete Branch "fix/mcp-bridge-head-of-line"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The bug
The stdio bridge's stdin loop awaited every request before reading the next line, so it handled exactly one at a time. A single slow call stalled every later request — and because those requests were never even sent, nothing could time them out. The client saw silence, not an error.
Evidence
Observed 2026-08-05: two gitea calls through this bridge sat completely mute until Claude Code aborted them at its own 1800s idle limit (
sent no response or progress). The upstream was healthy throughout:tools/callentries — the calls never reached itThey died queued in the bridge. Ruled out along the way: Cloudflare (allows every plausible UA; only
Python-urllibis blocked, which was my probe script), the favourite-index namespace change (legacy flatserver/toolnames still resolve fine), SSE streams staying open (all end in 3–192ms), and mcplocal's own client (already has a 30sAbortSignal.timeout).The fix
JSON-RPC ids exist precisely so responses may return out of order, so nothing here needed a queue. Requests now dispatch concurrently and are tracked in a set; stdin close awaits them before the session DELETE, otherwise a concurrent call races teardown and dies with a 404.
We still serialise until the session id exists — it arrives on the first response, and firing later requests without it would open a second upstream session. A client sends
initializefirst and waits anyway, so this costs one round trip, not throughput.Also makes the per-request timeout configurable via
MCPCTL_MCP_TIMEOUT_MS(default unchanged at 30s) and names it in the error, so a project with genuinely long tool calls can raise it rather than hitting a hardcoded wall.Tests
Two regression tests, and I verified the first one fails on the old serial code by reverting the change — a regression test that doesn't catch the bug is worthless:
Full CLI suite green: 515 tests / 42 files. Lint error count unchanged from HEAD (7 pre-existing).
🤖 Generated with Claude Code