Title: streamable_http client does not send `Origin` header → rejected with 403 by spec-compliant servers (e.g. go-sdk `CrossOriginProtection`) · Issue #2727 · modelcontextprotocol/python-sdk · GitHub
Open Graph Title: streamable_http client does not send `Origin` header → rejected with 403 by spec-compliant servers (e.g. go-sdk `CrossOriginProtection`) · Issue #2727 · modelcontextprotocol/python-sdk
X Title: streamable_http client does not send `Origin` header → rejected with 403 by spec-compliant servers (e.g. go-sdk `CrossOriginProtection`) · Issue #2727 · modelcontextprotocol/python-sdk
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 s...
Open Graph 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 ...
X 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 ev...
Opengraph URL: https://github.com/modelcontextprotocol/python-sdk/issues/2727
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"streamable_http client does not send `Origin` header → rejected with 403 by spec-compliant servers (e.g. go-sdk `CrossOriginProtection`)","articleBody":"## Summary\n\nThe 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:\n\n- `Sec-Fetch-Site: same-origin | same-site | none` (browser-only), or\n- An `Origin` header whose host matches the server's `Host`, or\n- An origin explicitly listed via `CrossOriginProtection.AddTrustedOrigin`.\n\nSince 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.\n\nSo 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.\n\n## Reproduction\n\n**Server** — a Go MCP server built with `modelcontextprotocol/go-sdk@v1.4.1` and the standard handler:\n\n```go\nhandler := mcp.NewStreamableHTTPHandler(\n func(_ *http.Request) *mcp.Server { return srv },\n nil, // default CrossOriginProtection: deny non-same-origin\n)\nhttp.Handle(\"/mcp\", handler)\n```\n\n**Client** — Python `mcp` SDK:\n\n```python\nfrom mcp.client.streamable_http import streamablehttp_client\nfrom mcp.client.session import ClientSession\n\nasync with streamablehttp_client(\"http://my-go-server:8081/mcp\") as (read, write, _):\n async with ClientSession(read, write) as session:\n await session.initialize() # never completes\n```\n\n**Observed:**\n\n1. `httpx` POSTs to `/mcp` with no `Origin` header, no `Sec-Fetch-*` headers.\n2. Go server returns `HTTP/1.1 403 Forbidden` immediately.\n3. Python client `post_writer` swallows the non-2xx (see #2110), and `session.initialize()` hangs forever on the read stream.\n4. 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.\n\n**Expected:** the Python client should send an `Origin` header derived from the target URL by default, so a spec-compliant server accepts the handshake.\n\n## Workarounds (today)\n\n- **Server side, Go**: pass `\u0026mcp.StreamableHTTPHandlerOptions{CrossOriginProtection: cop}` with `cop.AddTrustedOrigin(...)`, or set `GODEBUG=disablecrossoriginprotection=1`. Both require code/env changes on every server deployment.\n- **Client side, Python**: monkey-patch the httpx client to inject `Origin`. Brittle; depends on internals of `streamable_http`.\n\nNeither is satisfactory if both reference SDKs are supposed to interoperate out of the box.\n\n## Suggested fix\n\nIn `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:\n\n```python\nparsed = urlparse(self.url)\ndefault_origin = f\"{parsed.scheme}://{parsed.netloc}\"\nheaders.setdefault(\"Origin\", default_origin)\n```\n\nThis 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.\n\nOptionally, also set `Sec-Fetch-Site: same-origin` so the Go middleware short-circuits on the cheaper check.\n\n## Related\n\n- #2110 — explains why the resulting 403 manifests as a client hang instead of a clean exception.\n- #1798 / #861 — the Python *server* added equivalent DNS-rebinding protection; the *client* never picked up the matching header behavior.\n\n## Environment\n\n- `mcp` (Python SDK): latest installed via `mcp \u003e= 1.x` (tested under Locus's `locus.integrations.fastmcp.MCPClient` wrapper, which is a thin pass-through to `streamablehttp_client`).\n- `modelcontextprotocol/go-sdk@v1.4.1` (server).\n- Go 1.25 (stdlib `http.CrossOriginProtection`).\n- Python 3.13, anyio 4.x.","author":{"url":"https://github.com/fede-kamel","@type":"Person","name":"fede-kamel"},"datePublished":"2026-05-30T02:44:31.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":3},"url":"https://github.com/2727/python-sdk/issues/2727"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:94012779-bced-6dbc-643e-e9e74cefbfe5 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | BFEE:A4CAA:18F88C1:23BB59B:6A5A39E4 |
| html-safe-nonce | 79c801643db1c0d3d98255db62b4e520ef8a890ded387e75b12f972f7fec7d09 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJCRkVFOkE0Q0FBOjE4Rjg4QzE6MjNCQjU5Qjo2QTVBMzlFNCIsInZpc2l0b3JfaWQiOiI0OTMzMzQzODM0NzcwMzg5NDc3IiwicmVnaW9uX2VkZ2UiOiJpYWQiLCJyZWdpb25fcmVuZGVyIjoiaWFkIn0= |
| visitor-hmac | e6635108f5dd5b3cede31f68f3f386cca2fdd85544abeeea7a6e8aa195a138d4 |
| hovercard-subject-tag | issue:4552936463 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/modelcontextprotocol/python-sdk/2727/issue_layout |
| twitter:image | https://opengraph.githubassets.com/36ef8e4f5eabedfb8a739d0790717e36a37423e12c7ac77fd6d88a3b0ecb053b/modelcontextprotocol/python-sdk/issues/2727 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/36ef8e4f5eabedfb8a739d0790717e36a37423e12c7ac77fd6d88a3b0ecb053b/modelcontextprotocol/python-sdk/issues/2727 |
| og:image:alt | 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 ... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | fede-kamel |
| hostname | github.com |
| expected-hostname | github.com |
| None | 19c67ad7579d2401ff06e88a32ef45460fbb40fab3191c7f7582d5e166f326fc |
| turbo-cache-control | no-preview |
| go-import | github.com/modelcontextprotocol/python-sdk git https://github.com/modelcontextprotocol/python-sdk.git |
| octolytics-dimension-user_id | 182288589 |
| octolytics-dimension-user_login | modelcontextprotocol |
| octolytics-dimension-repository_id | 862584018 |
| octolytics-dimension-repository_nwo | modelcontextprotocol/python-sdk |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 862584018 |
| octolytics-dimension-repository_network_root_nwo | modelcontextprotocol/python-sdk |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | e618485bff22dab2ef1ffb4e34e4e6626688df69 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width