modelcontextprotocol / modelcontextprotocol/python-sdk

client's `read_stream_writer` open after SSE disconnection hanging `.receive()`

未關閉
#1,811 3 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視

還沒有人認領這個 Issue。

bug P1 ready for work
主要語言
Python
星號
24.3k
分支
4k
平均合併
1 天 1 小時
30 天內合併 PR
31

描述

Initial Checks
Description

This issue MAY be related to the following

Experiencing deadlocks on streamable_http transport. In order to reproduce the issue the following can be run.

import asyncio
import logging
import threading
import time

from fastmcp import Client, FastMCP
from fastmcp.client.transports import StreamableHttpTransport

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    handlers=[logging.StreamHandler()],
)

logger = logging.getLogger(__name__)

HOST = "127.0.0.1"
PORT = 8765
SERVER_URL = f"http://{HOST}:{PORT}/mcp"

mcp = FastMCP(name="timeout issue")

SSE_TIMEOUT = 0.1
SLEEP = 60


@mcp.tool
def blocking_call() -> str:
    time.sleep(SLEEP)
    return "42"


def run_server():
    mcp.run(transport="streamable-http", host=HOST, port=PORT)


async def test_blocking_sse():
    transport = StreamableHttpTransport(SERVER_URL, sse_read_timeout=SSE_TIMEOUT)
    async with Client(transport) as client:
        tools = await client.list_tools()
        print(f"available tools: {[t.name for t in tools]}")
        result = await client.call_tool("blocking_call", {})
        print(f"blocking result: {result}")


if __name__ == "__main__":
    server_thread = threading.Thread(target=run_server, daemon=True)
    server_thread.start()

    time.sleep(2)

    try:
        asyncio.run(test_blocking_sse())
    finally:
        logger.info(f"{time.strftime('%Y-%m-%d %H:%M:%S')} [CLIENT] Shutting down...")

Execution context := locally built fastmcp at this commit 790ea92

Observed Behavior: the client hangs forever after the SSE read timeout fires, looking at the logs:

  • tool request is sent, server returns HTTP 200 with [Content-Type: text/event-stream]
  • server starts executing the tool (blocking time.sleep(60))
  • client's SSE read timeout fires (after ~5 seconds with default httpx timeout)
  • client closes the HTTP connection
  • client hangs indefinitely - call_tool() never returns
  • the tool eventually completes on the server side, but when the server tries to send the response back, it gets BrokenResourceError because the HTTP connection was already closed by the client.

I know noting about nothing, so I can imagine the above example is just an issue on my side, maybe:

  • sse timeout should always be greater or equal to the expected timeout of tool calls (now that sse_read_timeout is deprecated it's httpx.Timeout counterpart should be >= tool timeout)
  • long running tool calls MUST send progress updates

but one thing is for sure and that is, the above configuration hangs indefenitely because the session layer never gets to know that the transport layer is dead after the SSE stream is closed

Within my ignorance of many aspects of the implementation, the missing else branch seems to be the root cause.

https://github.com/modelcontextprotocol/python-sdk/blob/a9cc822a1051b1bd2b6b9b57e9e4136406983b61/src/mcp/client/streamable_http.py#L433-L435

and the fix looks something like

if last_event_id is not None:  # pragma: no branch 
    ...
else:
    error_response = JSONRPCError(...)
    await ctx.read_stream_writer.send(SessionMessage(JSONRPCMessage(root=error_response)))

consequently raising McpError

Example Code

Python & MCP Python SDK
Execution context for ease of implementation 
- `mcp==1.24.0`
- example codebase run against locally built `fastmcp` at [790ea92](https://github.com/jlowin/fastmcp/tree/790ea92eb59256da68c83097321ebde8f8819bcf) --> 1.24.0
- Python: `python-3.12.7-macos-aarch64-none/bin/python3.12`

貢獻指南

開啟貢獻指南

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

研究方向

從 src/mcp/client/streamable_http.py 的第 433-435 行附近開始,使用 StreamableHttpTransport 的 Python 範例重現掛起的呼叫。追蹤 SSE 讀取逾時如何關閉連線,以及 session 如何消費 read_stream_writer 訊息。完成的標準是:逾時的用戶端呼叫能夠返回或拋出例外,而不是無限期掛起。

由索引模型根據 Issue 內容生成。

評估

技術堆疊
python
領域
api, networking
Issue 類型
缺陷
難度
3/5
預估耗時
1-2 天
活躍度
停滯
描述清晰度
基本清楚
新手友好度
42/100

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。