modelcontextprotocol / modelcontextprotocol/python-sdk

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

Offen
#2,678 8 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

bug P2 ready for work
Vorherrschende Sprache
Python
Sterne
24.3k
Forks
4k
Ø Merge
1 T. 1 Std.
Gemergte PRs (30 T.)
31

Beschreibung

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.

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Beginne mit dem von mcp.run(transport="stdio") verwendeten mcp.server.stdio-Pfad und reproduziere das Problem mit der bereitgestellten server.py, payload.jsonl und dem Shell-Umleitungsbefehl. Verfolge die Behandlung von stdin-EOF und ausstehenden Writer-Tasks; abgeschlossen ist die Aufgabe, wenn jede verarbeitete Anfrage, einschließlich des letzten Tool-Aufrufs, vor dem Beenden eine JSON-RPC-Antwort auf stdout erzeugt.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
python
Bereich
api, backend
Issue-Typ
Bug
Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Aktivitätsstatus
Aktiv
Klarheit
Größtenteils klar
Anfängerfreundlichkeit
55/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.