streamable HTTP client: the standalone GET stream advertises Accept: application/json but can only read text/event-stream, then gives up silently

Aperta
#3,503 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
3/5
Tempo stimato
1-2 giorni
Idoneità per principianti
73/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
python
Ambito
api, networking

Direzione di ricerca

Inizia in mcp/client/streamable_http.py, in StreamableHTTPTransport._prepare_headers e nei punti di chiamata di handle_get_stream, resumption, reconnection e termination. Esegui la riproduzione fornita del server e del client, quindi verifica che le richieste SSE GET pubblicizzino solo text/event-stream, che le richieste GET e DELETE prive di body omettano Content-Type e che lo stream non fallisca più silenziosamente quando la risposta è application/json.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

v1 v2

AI disclosure: I used Claude Code to read the transport, capture the headers and build the reproduction. I hit the problem myself, ran the repro, and stand behind the report.

Initial Checks
  • 2.2.0 (latest 2.x). The two files involved are byte-identical at upstream/main (9972c21a); v1.x has the same shape.
  • Searched first. Nearby reports cover the server side of the GET (#3102, #2474, PR #3129), Accept quality factors (#3154), and the reconnect loop (#3257, #3356, #2393). None covers the Accept the client sends.
Release line

2.x (current stable)

Description

StreamableHTTPTransport._prepare_headers is written for the POST — accept: application/json, text/event-stream plus content-type: application/json — but all five outbound requests use it:

Call site Request
_handle_post_request POST with a body ok
handle_get_stream standalone SSE GET Accept overridden, Content-Type with no body
_handle_resumption_request resumption SSE GET same
_handle_reconnection reconnect SSE GET same
terminate_session DELETE, no body Content-Type with no body

sse_within_origin sets _SSE_HEADERS = {"Accept": "text/event-stream", ...} and then does merged.update(headers or {}), so the caller's Accept wins. Those GET responses are read only through httpx2.EventSource, which raises SSEError for anything but text/event-stream: the client offers a representation it will then refuse. On the wire:

GET /mcp
  accept: application/json, text/event-stream
  content-type: application/json

The DELETE carries the same content-type with no body.

What it costs. Answer that GET with application/json and the offer is honoured back at the client:

httpx2.SSEError: Expected response with content type 'text/event-stream', got 'application/json'.
DEBUG mcp.client.streamable_http GET stream max reconnection attempts (2) exceeded

handle_get_stream swallows it in its broad except Exception and returns. Client-to-server requests keep working — the repro still prints its tools/call result — so the session looks healthy while notifications, sampling, elicitation and roots are gone for its lifetime, with only a DEBUG line.

Expected. The SSE GETs advertise only text/event-stream, and the bodyless GET and DELETE carry no Content-Type.

Scope. Under the 2025-11-25 spec the client is within its rights (it "MUST include an Accept header, listing text/event-stream as a supported content type"; listing more is not forbidden) and the server is not (it "MUST either return Content-Type: text/event-stream [...] or else return HTTP 405 Method Not Allowed"). A compliant server never hits this, and the repro's server is deliberately non-compliant. What still looks wrong is offering what the client cannot read, and losing the channel silently when a server takes the offer up; the stray Content-Type is wrong regardless. mcp/client/sse.py is unaffected — it passes no headers.

The repro uses mode="legacy" because notifications/initialized is sent only from ClientSession.initialize(); the modern path adopts a DiscoverResult, so start_get_stream never fires there.

Example Code
# server.py — the official server, answering the GET with the content type the client accepts.
import uvicorn
from mcp.server.mcpserver import MCPServer

server = MCPServer(name="probe", version="0.1.0")


@server.tool()
def echo(text: str) -> str:
    return text


class TakeTheClientAtItsWord:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        if scope["type"] == "http" and scope["method"] == "GET":
            body = b'{"jsonrpc":"2.0"}'
            await send({
                "type": "http.response.start",
                "status": 200,
                "headers": [(b"content-type", b"application/json"), (b"content-length", str(len(body)).encode())],
            })
            await send({"type": "http.response.body", "body": body})
            return
        await self.app(scope, receive, send)


uvicorn.run(TakeTheClientAtItsWord(server.streamable_http_app()), host="127.0.0.1", port=8000)
# client.py
import asyncio
import logging

from mcp import Client

logging.basicConfig(level=logging.DEBUG, format="%(levelname)s %(name)s %(message)s")


async def main() -> None:
    async with Client("http://127.0.0.1:8000/mcp", mode="legacy") as client:
        print("tools/call still works:", (await client.call_tool("echo", {"text": "hi"})).content[0].text)
        await asyncio.sleep(3)


asyncio.run(main())

Drop the GET branch from the wrapper and log scope["headers"] to see the outgoing headers instead of the failure.

Python & MCP Python SDK
Python 3.14.3 (CPython, macOS arm64)
mcp 2.2.0, httpx2 2.12.0, httpcore2 2.12.0, anyio 4.15.1, uvicorn 0.52.4
Lingua principale
Python
Stelle
24.3k
Fork
4k
Merge medio
1g 1h
PR unite (30g)
31

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di modelcontextprotocol/python-sdk

Tutte le issue di modelcontextprotocol/python-sdk

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.