modelcontextprotocol / modelcontextprotocol/python-sdk

streamable_http client does not send `Origin` header → rejected with 403 by spec-compliant servers (e.g. go-sdk `CrossOriginProtection`)

Aperta Adatta ai principianti
#2,727 3 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

enhancement needs decision P3
Lingua principale
Python
Stelle
24.3k
Fork
4k
Merge medio
1g 1h
PR unite (30g)
31

Descrizione

Summary

The Python SDK's streamable_http_client opens its POST handshake without an Origin header (and without Sec-Fetch-Site). The official Go SDK (modelcontextprotocol/go-sdk v1.4.x) wraps every streamable-HTTP handler with Go 1.25's stdlib http.CrossOriginProtection, which enforces the spec's anti-DNS-rebinding rule and denies any state-changing request that cannot prove same-origin via one of:

  • Sec-Fetch-Site: same-origin | same-site | none (browser-only), or
  • An Origin header whose host matches the server's Host, or
  • An origin explicitly listed via CrossOriginProtection.AddTrustedOrigin.

Since the Python client sends none of those, a perfectly legitimate server-to-server connection from the official Python client to the official Go server is indistinguishable from a CSRF attempt → HTTP 403 Forbidden on the very first POST.

So the two reference SDKs from the same org are out of sync by one spec revision: the Go server enforces the new rule; the Python client doesn't yet send the headers that satisfy it.

Reproduction

Server — a Go MCP server built with modelcontextprotocol/go-sdk@v1.4.1 and the standard handler:

handler := mcp.NewStreamableHTTPHandler(
    func(_ *http.Request) *mcp.Server { return srv },
    nil, // default CrossOriginProtection: deny non-same-origin
)
http.Handle("/mcp", handler)

Client — Python mcp SDK:

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

async with streamablehttp_client("http://my-go-server:8081/mcp") as (read, write, _):
    async with ClientSession(read, write) as session:
        await session.initialize()   # never completes

Observed:

  1. httpx POSTs to /mcp with no Origin header, no Sec-Fetch-* headers.
  2. Go server returns HTTP/1.1 403 Forbidden immediately.
  3. Python client post_writer swallows the non-2xx (see #2110), and session.initialize() hangs forever on the read stream.
  4. Eventually the caller (e.g. a FastAPI startup hook with a wait_for timeout) cancels, which surfaces as RuntimeError: Attempted to exit cancel scope in a different task than it was entered in because the streamable_http_client task group was entered in one task and is being torn down in another.

Expected: the Python client should send an Origin header derived from the target URL by default, so a spec-compliant server accepts the handshake.

Workarounds (today)

  • Server side, Go: pass &mcp.StreamableHTTPHandlerOptions{CrossOriginProtection: cop} with cop.AddTrustedOrigin(...), or set GODEBUG=disablecrossoriginprotection=1. Both require code/env changes on every server deployment.
  • Client side, Python: monkey-patch the httpx client to inject Origin. Brittle; depends on internals of streamable_http.

Neither is satisfactory if both reference SDKs are supposed to interoperate out of the box.

Suggested fix

In mcp.client.streamable_http, when opening the httpx.AsyncClient, derive a default Origin header from the target URL's scheme + netloc and add it to every outgoing request:

parsed = urlparse(self.url)
default_origin = f"{parsed.scheme}://{parsed.netloc}"
headers.setdefault("Origin", default_origin)

This makes the Python client's traffic indistinguishable from a same-origin browser request as far as CrossOriginProtection.Check is concerned, without weakening any server's CSRF posture. Callers who want a different Origin (e.g. multi-tenant proxies) can still override via the existing custom-headers path.

Optionally, also set Sec-Fetch-Site: same-origin so the Go middleware short-circuits on the cheaper check.

Related

  • #2110 — explains why the resulting 403 manifests as a client hang instead of a clean exception.
  • #1798 / #861 — the Python server added equivalent DNS-rebinding protection; the client never picked up the matching header behavior.

Environment

  • mcp (Python SDK): latest installed via mcp >= 1.x (tested under Locus's locus.integrations.fastmcp.MCPClient wrapper, which is a thin pass-through to streamablehttp_client).
  • modelcontextprotocol/go-sdk@v1.4.1 (server).
  • Go 1.25 (stdlib http.CrossOriginProtection).
  • Python 3.13, anyio 4.x.

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.

Direzione di ricerca

Inizia in mcp.client.streamable_http, all’interno di streamablehttp_client e della configurazione di httpx.AsyncClient; esamina come vengono passati gli header personalizzati. Riproduci l’handshake con l’handler Go descritto, quindi verifica che venga inviato l’Origin predefinito, che gli header forniti dal chiamante continuino a essere rispettati e che l’inizializzazione della sessione vada a buon fine.

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

Valutazione

Stack tecnologico
python
Ambito
api, backend
Tipo di issue
Bug
Difficoltà
2/5
Tempo stimato
1-3 ore
Stato di attività
Tranquilla
Chiarezza
Specificata chiaramente
Idoneità per principianti
75/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.