You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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:
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.
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).
Summary
The Python SDK's
streamable_http_clientopens its POST handshake without anOriginheader (and withoutSec-Fetch-Site). The official Go SDK (modelcontextprotocol/go-sdkv1.4.x) wraps every streamable-HTTP handler with Go 1.25's stdlibhttp.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), orOriginheader whose host matches the server'sHost, orCrossOriginProtection.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.1and the standard handler:Client — Python
mcpSDK:Observed:
httpxPOSTs to/mcpwith noOriginheader, noSec-Fetch-*headers.HTTP/1.1 403 Forbiddenimmediately.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.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
Originheader derived from the target URL by default, so a spec-compliant server accepts the handshake.Workarounds (today)
&mcp.StreamableHTTPHandlerOptions{CrossOriginProtection: cop}withcop.AddTrustedOrigin(...), or setGODEBUG=disablecrossoriginprotection=1. Both require code/env changes on every server deployment.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 thehttpx.AsyncClient, derive a defaultOriginheader from the target URL's scheme + netloc and add it to every outgoing request:This makes the Python client's traffic indistinguishable from a same-origin browser request as far as
CrossOriginProtection.Checkis concerned, without weakening any server's CSRF posture. Callers who want a differentOrigin(e.g. multi-tenant proxies) can still override via the existing custom-headers path.Optionally, also set
Sec-Fetch-Site: same-originso the Go middleware short-circuits on the cheaper check.Related
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).http.CrossOriginProtection).