modelcontextprotocol / modelcontextprotocol/python-sdk

StreamableHTTP: a client disconnect while reading the POST body is logged as an exception and answered 500

オープン 初心者向け
#3,469 コメント 2 件 リアクション 0 件 担当者 0 名 GitHub で見る

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

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

説明

Initial Checks
Release line

2.x (current stable), and the same on 1.28.1

Description

StreamableHTTPServerTransport._handle_post_request reads the request body inside its blanket except Exception. When the client goes away before the body arrives, await request.body() raises starlette.requests.ClientDisconnect, the transport logs it with logger.exception("Error handling POST request") and answers 500. That is a normal network event on a public MCP endpoint (mobile clients, proxies with short timeouts, health probes), and it lands as an unhandled server error with a stack trace in every log aggregator. #2064 / #2257 handled the disconnect in _handle_message; this is the earlier point, in streamable_http.py (the body = await request.body() line and the except Exception as err below it). The SSE transport has no handling for it either.

Example Code
import asyncio
import importlib.metadata as md
import logging

from mcp.server.streamable_http import StreamableHTTPServerTransport

records = []


class Capture(logging.Handler):
    def emit(self, record):
        records.append(record)


logging.getLogger("mcp").addHandler(Capture())
logging.getLogger("mcp").setLevel(logging.DEBUG)


async def main():
    transport = StreamableHTTPServerTransport(mcp_session_id=None)
    scope = {
        "type": "http", "method": "POST", "path": "/mcp", "query_string": b"", "headers": [
            (b"content-type", b"application/json"),
            (b"accept", b"application/json, text/event-stream"),
            (b"content-length", b"58"),
        ],
    }

    async def receive():  # the client went away before sending its body
        return {"type": "http.disconnect"}

    sent = []

    async def send(message):
        sent.append(message)

    async with transport.connect():
        try:
            await asyncio.wait_for(transport.handle_request(scope, receive, send), timeout=5)
        except asyncio.TimeoutError:
            print("handle_request did not return within 5s")
    status = next((m["status"] for m in sent if m["type"] == "http.response.start"), None)
    print(f"mcp {md.version('mcp')}: response status = {status}")
    for r in records:
        if r.levelno >= logging.ERROR:
            print(f"  logged {r.levelname} by {r.name}: {r.getMessage()} | exc: {type(r.exc_info[1]).__name__ if r.exc_info else None}")


try:
    asyncio.run(asyncio.wait_for(main(), timeout=15))
except asyncio.TimeoutError:
    print("main did not finish within 15s")

Output on 1.28.1 and 2.1.1:

mcp 2.1.1: response status = 500
  logged ERROR by mcp.server.streamable_http: Error handling POST request | exc: ClientDisconnect

(The script caps handle_request at 5 s because the stub send / receive is not a real ASGI server; the 500 and the log line are sent before that.)

Expected: ClientDisconnect caught before the generic handler, logged at debug or info, and no response attempted, since there is nobody to answer.

Python & MCP Python SDK

Python 3.12.13, mcp 1.28.1 and 2.1.1, starlette 1.3.1, macOS

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

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

はじめの一歩

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

調査の方向性

mcp/server/streamable_http.py の StreamableHTTPServerTransport._handle_post_request から始め、特に body = await request.body() の行と、その後に続く汎用の例外ハンドラーを確認します。#2064 と #2257 で参照されている、_handle_message における既存の切断処理と比較してください。ClientDisconnect がエラーのスタックトレースや 500 レスポンスを発生させずに処理され、回帰テストのカバレッジでその動作が検証されれば完了です。

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

評価

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

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

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