modelcontextprotocol / modelcontextprotocol/python-sdk

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

オープン
#2,678 コメント 8 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

bug P2 ready for work
主要言語
Python
スター
24.3k
フォーク
4k
平均マージ
1日 1時間
マージ済み PR(30日)
31

説明

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.

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

mcp.run(transport="stdio") が使用する mcp.server.stdio パスから始め、提供されている server.py、payload.jsonl、およびシェルのリダイレクトコマンドで問題を再現します。stdin の EOF 処理と保留中の writer タスクを追跡します。完了の条件は、最後のツール呼び出しを含む処理済みのすべてのリクエストが、終了前に stdout へ JSON-RPC レスポンスを出力することです。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
python
領域
api, backend
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
活発
明瞭さ
おおむね明確
初心者へのやさしさ
55/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。