modelcontextprotocol / modelcontextprotocol/python-sdk
streamable_http client does not send `Origin` header → rejected with 403 by spec-compliant servers (e.g. go-sdk `CrossOriginProtection`)
Nessuno ha ancora preso questa issue.
- 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
Originheader whose host matches the server'sHost, 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:
httpxPOSTs to/mcpwith noOriginheader, noSec-Fetch-*headers.- Go server returns
HTTP/1.1 403 Forbiddenimmediately. - Python client
post_writerswallows the non-2xx (see #2110), andsession.initialize()hangs forever on the read stream. - Eventually the caller (e.g. a FastAPI startup hook with a
wait_fortimeout) cancels, which surfaces asRuntimeError: Attempted to exit cancel scope in a different task than it was entered inbecause thestreamable_http_clienttask 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}withcop.AddTrustedOrigin(...), or setGODEBUG=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 ofstreamable_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 viamcp >= 1.x(tested under Locus'slocus.integrations.fastmcp.MCPClientwrapper, which is a thin pass-through tostreamablehttp_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
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- 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