modelcontextprotocol / modelcontextprotocol/python-sdk

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

Open
#2,678 8 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug P2 ready for work
Dominant language
Python
Stars
24.3k
Forks
4k
Avg merge
1d 1h
Merged PRs (30d)
31

Description

1. Initial Checks

  • I have searched the issue tracker for similar bugs (closest existing: #1933, but that's about premature close of real stdio during shutdown — different failure layer)
  • I am using a supported MCP SDK version (pinned mcp>=1.27,<2)

2. Description

Triage note: checked jlowin/fastmcp (now PrefectHQ fork) — FastMCP's server-side stdio delegates to mcp.server.stdio via a transport mixin, so the bug belongs here in the SDK rather than in the FastMCP wrapper.

When driving a FastMCP stdio server with a file-redirected stdin (e.g. python -m my_server < payload.jsonl > response.jsonl), in-flight tool-call responses can be dropped if their response writer hasn't been scheduled when stdin EOF arrives.

The stdio read loop appears to treat stdin EOF as an immediate-shutdown signal, cancelling pending writer tasks before they flush their JSON-RPC responses to stdout. The failure is silent — no traceback, no log line, the response is simply absent from stdout.

Expected: All responses for processed requests appear on stdout before the server exits.
Actual: Responses for the last-issued requests can be missing entirely.

3. Example Code (minimal reproducible)

# server.py
from mcp.server.fastmcp import FastMCP
import asyncio

mcp = FastMCP("repro")

@mcp.tool()
async def slow_echo(text: str) -> str:
    await asyncio.sleep(0.05)  # guaranteed yield point so writer scheduling is observable
    return text

if __name__ == "__main__":
    mcp.run(transport="stdio")
# payload.jsonl
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"repro","version":"0.1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"slow_echo","arguments":{"text":"first"}}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"slow_echo","arguments":{"text":"second"}}}
python server.py < payload.jsonl > response.jsonl
# Observed: response.jsonl contains id=0 + id=1 results, id=2 is absent.

The race is timing-sensitive; if id=2 surfaces on the first run, increase the asyncio.sleep delay or run repeatedly. We saw it deterministically in production where the tool body did a real HTTP call (~50-200ms).

Diagnostic fingerprint: the difference between this transport race and a quality bug in the tool itself is absence-of-response vs response-with-empty-result. If you ever see id=N missing entirely (not {"id":N,"result":[]}), suspect this race.

4. Python and MCP Python SDK

  • Python: 3.12 (python:3.12-slim Docker base)
  • MCP SDK: 1.27.x (mcp>=1.27,<2)
  • OS: Linux (Debian Bookworm; reproduced on Synology DSM 7.2 host)
  • Transport: stdio via mcp.run(transport="stdio")

Workaround we shipped (in case it helps the fix design): wrote a Python driver that owns both pipes via subprocess.Popen(stdin=PIPE, stdout=PIPE) and refuses to close stdin until the response for the last-issued id is observed on stdout. ~140 lines stdlib-only.

Suggested fix: before exiting on stdin EOF, await any pending writer tasks (with a small timeout) so their JSON-RPC responses reach stdout before the process terminates.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the mcp.server.stdio path used by mcp.run(transport="stdio") and reproduce the issue with the provided server.py, payload.jsonl, and shell redirection command. Trace stdin EOF handling and pending writer tasks; done means every processed request, including the final tool call, produces a JSON-RPC response on stdout before exit.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.