Repository navigation
streamable_http client does not send Origin header → rejected with 403 by spec-compliant servers (e.g. go-sdk CrossOriginProtection) #2727
Description
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 completesObserved:
httpxPOSTs to/mcpwith noOriginheader, noSec-Fetch-*headers.- Go server returns
HTTP/1.1 403 Forbiddenimmediately. - Python client
post_writerswallows the non-2xx (see HTTP transport swallows non-2xx status codes causing client to hang #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
- HTTP transport swallows non-2xx status codes causing client to hang #2110 — explains why the resulting 403 manifests as a client hang instead of a clean exception.
- Guide: Resolving "421 Invalid Host Header" (DNS Rebinding Protection) #1798 / Add support for DNS rebinding protections #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.
Activity
norika1207-lab commentedon Jun 2, 2026
tarunag10 commentedon Jun 2, 2026
Opened draft PR #2759 with a minimal default
Originheader derived from the streamable HTTP target URL while preserving caller-provided overrides: #2759XidaoApi commentedon Jun 3, 2026
Interop note from an OpenAI-compatible gateway/operator perspective: deriving a default
Originfrom the target URL looks like the right baseline here, but I would avoid sendingSec-Fetch-Site: same-originfrom non-browser clients. Some infrastructure treats theSec-Fetch-*family as browser provenance signals, so synthesizing them in server-to-server traffic can create confusing logs or future policy edge cases.The lower-risk default seems to be:
Origin: <scheme>://<host[:port]>when the caller did not provide oneinitialize()pending foreverThat last part matters operationally. In gateway setups the current behavior looks like a transient transport stall, when the real problem is a deterministic cross-origin rejection at the server boundary. Even just surfacing the status code + response body on the first failed handshake would make this class of mismatch much easier to debug.
So the change here should probably be:
Originby default, explicit override path, and a separate fix to convert non-2xx handshake responses into immediate client exceptions instead of hanging reads.fix(client): send same-origin Origin header from streamable HTTP client
fix(client): send same-origin Origin header from streamable HTTP client
feat(client): send a same-origin Origin header by default (streamable…
fix(client): send same-origin Origin header from streamable HTTP client
fix(client): send same-origin Origin header from streamable HTTP client
fix(client): send same-origin Origin header from streamable HTTP client
fix(client): send same-origin Origin header from streamable HTTP client