cd /news/developer-tools/the-mcp-server-wedged-the-ide-on-a-l… · home topics developer-tools article
[ARTICLE · art-45269] src=loomcycle.dev ↗ pub= topic=developer-tools verified=true sentiment=↓ negative

The MCP server wedged the IDE on a list — head-of-line blocking, and why killing the process was the only release (RFC O/P/R)

A head-of-line blocking bug in Loomcycle's MCP server caused an IDE to hang indefinitely when a slow tool call blocked all subsequent frames on a single goroutine. The issue was amplified by cross-runtime interruption wake delays and provider outages, requiring a process kill to recover. Three coordinated fixes in v0.23.0—concurrent dispatch, per-call timeouts, and a thin-client topology—eliminate the failure class at multiple levels.

read3 min views12 publishedJun 6, 2026

Day two of the operator-via-MCP series. Mid-experiment, the operator's IDE (a fresh Claude Code session driving loomcycle through the plugin's stdio MCP server) hung on a list_runs tool confirmation. The user approved the call. Nothing happened. The only way out was to kill the loomcycle mcp process from another terminal. Source-reading v0.22.0's internal/api/mcp/server.go found a single load-bearing footnote (lines 102-115): "Frames are dispatched SEQUENTIALLY on a single goroutine. Concurrent tools/call is a v0.9.x optimisation — not implemented." The comment was honest; it was also wrong about the priority. Combined with an unbounded spawn_run handler (no per-call timeout), one slow run blocked every subsequent frame in the OS pipe behind it — even a cheap list_runs, even a cancel_run. Classic head-of-line blocking on a single-consumer stream. The list was the victim, not the cause. Three amplifiers turned "slow" into "wedged for an hour": F15 (cross-runtime interruption wake) made an interruption-held run block to the 1-hour interruption.default_timeout_ms; provider outages (the day this happened coincided with an Opus 4.7/4.8 incident on Anthropic's side) stalled spawn_run on retries; F16 accumulated SSE clients + heartbeating runs added resource pressure on top. Why killing the process was the only release: ctx cancellation is polled only between frames, so cancelling the parent context doesn't preempt an in-flight handler. A cancel_run frame would itself be HOL-blocked behind the occupier. SIGTERM on the loomcycle mcp PID ends the process; stdin closes; the wedged call dies with it; the IDE sees the MCP transport drop and reconnects. Three coordinated PRs in v0.23.0 close the failure class at three levels: RFC O (#377) makes stdio dispatch concurrent on bounded goroutines (the writeMu for stdout framing already existed, only used by the notification path; now it serializes every response). Independent tool calls run concurrently; initialize/initialized stay sequential. The HOL class is gone. RFC P (#380) wraps spawn_run in a transport-level context.WithTimeout, configurable per operator. A provider stall or unkillable run no longer keeps the handler alive forever. Composed with RFC O, the bound is hard. RFC R (#381) ships the thin-client topology: loomcycle mcp --upstream launches a stdio MCP server that proxies to a single full runtime over HTTP/gRPC. Every tool call lands on the one runtime that owns the in-process bus; the cross-process bus-Notify problem dissolves (interruption_resolve from a thin client hits the same in-process bus the blocking ask waits on). F15 stops being reachable in normal use. Breaking change in the same release (#383): loomcycle mcp --no-http is removed — the pattern that needed it (two full runtimes side-by-side with HTTP suppressed on one) is the pattern that caused F15. A real fix for the original code path — durable cross-runtime wake via the _system/interrupts/resolved channel — shipped two days later (#400) for multi-host HA topologies that genuinely need multiple full runtimes. The thin-client pattern stays the recommended default. None of the three PRs would have been sufficient alone: concurrent dispatch without timeout still allows a goroutine slot to be tied up indefinitely; timeout without concurrent dispatch still produces "list hung for 5 minutes" before the timeout fires; thin-client without the other two still HOL-blocks a single transport on a single runtime. The three together remove the failure class at three different levels. The engineering lesson: a "we'll fix it later" comment on a load-bearing concurrency property is a P1 bug, not a roadmap item. The runtime documentation said the limit; the operators' code didn't read the documentation. The first real workload to touch the limit looked like complete system failure.

── more in #developer-tools 4 stories · sorted by recency
── more on @loomcycle 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/the-mcp-server-wedge…] indexed:0 read:3min 2026-06-06 ·