Skip to content

fix(stdio): drain pending responses before closing read stream on EOF (#2678)#2821

Open
friendlygeorge wants to merge 2 commits into
modelcontextprotocol:mainfrom
friendlygeorge:fix/2678-stdio-eof-drain
Open

fix(stdio): drain pending responses before closing read stream on EOF (#2678)#2821
friendlygeorge wants to merge 2 commits into
modelcontextprotocol:mainfrom
friendlygeorge:fix/2678-stdio-eof-drain

Conversation

@friendlygeorge

Copy link
Copy Markdown

Summary

Fixes #2678 — stdio transport drops in-flight server responses when the server process exits.

Problem

When a stdio MCP server exits, the client's closes on EOF before the server's pending responses are fully drained. This causes the client to lose responses that were already sent but not yet consumed.

Root Cause

calls on the stream immediately when EOF is detected, which signals the read loop to stop — even if there are buffered responses still in flight.

Fix

Replace the context manager with explicit in a block, allowing all pending responses to drain before the stream is closed.

  • : Remove context manager on , call in
  • : Extract helper, ensure process cleanup happens after stream drain

All 10 existing stdio tests pass. Added test confirming pending responses are delivered before EOF.

Closes modelcontextprotocol#2678. When stdin hits EOF, the previous code closed
read_stream_writer inside the `async with` block, which cascaded to
close write_stream_reader before the server's pending responses could
drain through stdout_writer.

The fix removes the `async with read_stream_writer` wrapper from
stdin_reader and instead calls `aclose()` in the `finally` block.
This ensures:
1. All stdin lines are read and forwarded to the server
2. The read stream is closed promptly on EOF (signaling the server)
3. Buffered responses in write_stream_reader drain through stdout_writer
   before the task group exits

All existing tests pass. Added regression test verifying responses are
not dropped when stdin closes immediately after a request.
friendlygeorge pushed a commit to friendlygeorge/python-sdk that referenced this pull request Jun 8, 2026
Both close() and __del__ are no-op overrides that prevent TextIOWrapper
from closing real stdio handles. They are not directly tested in this PR
(the test is on modelcontextprotocol#2821) and __del__ relies on GC timing which is
unreliable in tests.
- Collapse multi-line io.BytesIO() to single line (ruff format)
- Add blank line after module docstring (ruff format)
- Remove unused imports: time, anyio (ruff check --fix)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FastMCP/stdio: in-flight tool responses dropped on stdin EOF when input is bash-redirected from a file

1 participant