modelcontextprotocol / modelcontextprotocol/python-sdk

Streamable HTTP transport drops requests immediately after `initialize`

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

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

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

説明

Initial Checks
Description

Solution: https://github.com/modelcontextprotocol/python-sdk/pull/1674

Title: Streamable HTTP transport drops requests immediately after initialize

Summary
When using a server over Streamable HTTP, the first session.list_tools() (and sometimes a few follow-up requests) can intermittently return an empty tool list right after session.initialize() succeeds. This happens even though the server has already provided tool metadata during initialization.

Steps to Reproduce

  1. Start examples/servers/simple-streamablehttp.
  2. Run a client that calls session.initialize() and immediately follows with session.list_tools().
  3. Repeat quickly; roughly 1 in 5 iterations returns an empty list.

Expected Behavior
Once initialize completes, the client transport should be ready to send subsequent JSON-RPC requests and receive their responses reliably.

Actual Behavior
There is a race inside streamable_http.streamablehttp_client: the post_writer task is started with tg.start_soon, so the caller can enqueue requests before the writer task has finished subscribing to the in-memory stream. Because the stream buffer size is 0, those requests can be dropped, leading to empty responses or timeouts.

Proposed Fix
Start post_writer with tg.start(...), mirroring the SSE transport. This blocks until the writer task signals readiness via TaskStatus.started, guaranteeing that the zero-buffer stream is ready before yielding to the caller. Two new stress tests (test_streamablehttp_no_race_condition_on_consecutive_requests and test_streamablehttp_rapid_request_sequence) cover the regression.

Impact
Streamable HTTP transports become much more reliable in real deployments that issue back-to-back requests (initialize → list_tools, quick polling, etc.), preventing confusing empty tool listings and retry storms.

Example Code
# reproduce_streamablehttp_race.py
import anyio

from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamablehttp_client


SERVER_URL = "http://127.0.0.1:8000/mcp"
ITERATIONS = 100


async def main() -> None:
    failures = 0

    for i in range(ITERATIONS):
        async with streamablehttp_client(SERVER_URL) as (read_stream, write_stream, _):
            async with ClientSession(read_stream, write_stream) as session:
                result = await session.initialize()
                tools = await session.list_tools()
                if not tools.tools:
                    failures += 1
                    print(f"[{i}] list_tools returned EMPTY! session.initialize -> list_tools race hit.")
                else:
                    print(f"[{i}] ok: {len(tools.tools)} tools")

    if failures:
        raise SystemExit(f"Race reproduced: {failures}/{ITERATIONS} iterations failed")

    print("No failures observed (try increasing ITERATIONS or lower server CPU).")


if __name__ == "__main__":
    anyio.run(main)
Python & MCP Python SDK
latest

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

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

はじめの一歩

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

調査の方向性

streamable_http.streamablehttp_client エントリポイントから開始し、そのタスクの起動を SSE トランスポートと比較します。指定されたストレステスト、test_streamablehttp_no_race_condition_on_consecutive_requests と test_streamablehttp_rapid_request_sequence を実行し、initialize の直後に list_tools を実行すると期待されるツールが確実に返されることを確認します。

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

評価

技術スタック
python
領域
backend-api-design, testing-qa
issue の種類
バグ
難易度
2/5
見積もり時間
1〜3時間
活発さ
停滞
明瞭さ
明確に書かれている
初心者へのやさしさ
25/100

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

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