You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The stdio loop hung every streaming MCP client because `IO.binread(io, 4096)`
ran inline in the GenServer and only returned at 4096 bytes or EOF. Real
clients (Claude Code, Claude Desktop, Cursor, Cline, Inspector) write one
~250-byte `initialize` line and wait for a reply, so the read never returned
and worker `:async_reply` messages couldn't be drained from the mailbox.
The temp-file integration suite missed it because file-backed pipes always
EOF.
Switch to line-mode reads done in a dedicated `spawn_monitor`'d reader
process that forwards each frame as a `{:stdin_line, ...}` message; the
GenServer is never IO-blocked and async replies interleave naturally.
Treat EOF-during-`exit`-drain as a no-op so the grace timer can finish
delivering in-flight replies (the old temp-file `tools/call` test passed by
timing luck once reads were serial). Cancel the grace timer on clean drain
to avoid an orphan `:exit_grace_elapsed` message. Add a `terminate/2` that
kills the orphaned reader on test-mode stop.
New regression test (`streaming_stdio_test.exs`) drives the release as an
Erlang `Port` with a persistent stdin and exercises both `initialize` and
`tools/call (+ 1 2 3)`. Without the fix, this hangs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: Plans/ptc-runner-mcp-server.md
+3-1Lines changed: 3 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -367,8 +367,10 @@ program completes; ordering is not preserved.
367
367
368
368
| Event | Server behavior |
369
369
|---|---|
370
-
| stdin EOF | Cancel all in-flight sandboxes; emit no further responses; exit 0. |
370
+
| stdin EOF (no `exit` drain in progress) | Cancel all in-flight sandboxes; emit no further responses; exit 0. |
371
+
| stdin EOF (during `exit` drain) | Defer to row 2 — the grace-period drain finishes first. File-backed clients (and `Port`-driven test runners) hit EOF the moment after writing the `exit` frame; tearing down here would race the in-flight reply. |
371
372
|`shutdown` request (if sent) | Reply `null`; transition to drain; on subsequent `exit` notification, exit 0. |
373
+
|`exit` notification (workers in flight) | Set `exit_pending`; schedule a 2 s grace timer; once `in_flight` drains, reply-then-stop and cancel the timer. If grace elapses, force-kill workers and stop. |
372
374
|`notifications/cancelled` for in-flight ID | Kill the sandbox process; emit no response for that ID. |
373
375
|`notifications/cancelled` for unknown/already-completed ID | Ignore silently. |
374
376
| Unhandled BEAM crash in request handler | Log to stderr; emit `-32603 Internal error` referencing the request ID; continue serving. |
0 commit comments