modelcontextprotocol / modelcontextprotocol/python-sdk

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

Đang mở
#1,811 3 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

bug P1 ready for work
Ngôn ngữ chính
Python
Star
24.3k
Fork
4k
Merge trung bình
1 ngày 1 giờ
Pull request đã merge (30 ngày)
31

Mô tả

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`

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu trong src/mcp/client/streamable_http.py, quanh các dòng 433-435, và tái hiện lời gọi bị treo bằng ví dụ Python sử dụng StreamableHttpTransport. Theo dõi cách thời gian chờ đọc SSE đóng kết nối và cách session tiêu thụ các message từ read_stream_writer. Hoàn tất khi lời gọi của client trả về hoặc phát sinh exception sau khi hết thời gian chờ, thay vì bị treo vô thời hạn.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
python
Lĩnh vực
api, networking
Loại issue
Lỗi
Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức độ hoạt động
Đình trệ
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
42/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.