fix(client): send same-origin Origin header from streamable HTTP client#2729
fix(client): send same-origin Origin header from streamable HTTP client#2729Bartok9 wants to merge 1 commit into
Conversation
|
CI note: the single red cell (
A rerun of the failed job should clear it (I don't have rerun permission on the fork PR). Happy to rebase if main has moved. |
Closes modelcontextprotocol#2727 The streamable HTTP client opened its POST handshake without an Origin header, so spec-compliant servers that enforce anti-DNS-rebinding / CSRF protection (e.g. the Go SDK's http.CrossOriginProtection) reject the very first request with 403 Forbidden, and the client then hangs on the read stream. _prepare_headers now derives a same-origin value (scheme://host[:port]) from the target URL and sends it as the Origin header. URLs without a scheme or host add no header. Callers needing a different Origin can set one on the underlying httpx client's default headers.
c9bf54b to
f24afa8
Compare
|
CI note — rebased onto current |
Summary
Originheader by default.http.CrossOriginProtection) that otherwise reject the handshake with403.Motivation
Closes #2727.
The Python
streamablehttp_clientopened its POST handshake without anOriginheader. The official Go SDK (modelcontextprotocol/go-sdkv1.4.x) wraps every streamable-HTTP handler with Go 1.25'shttp.CrossOriginProtection, which denies any state-changing request that cannot prove same-origin viaSec-Fetch-Site, a matchingOrigin, or an allow-listed origin. A legitimate server-to-server connection from the Python client therefore looks like a CSRF attempt →HTTP 403 Forbiddenon the first POST, and the client (per #2110) swallows the non-2xx and hangs forever onsession.initialize().The two reference SDKs from the same org were out of sync by one spec revision: the Go server enforces the rule; the Python client never sent the header that satisfies it.
Fix
StreamableHTTPTransport._prepare_headers()now derives a same-origin value (scheme://host[:port]) from the target URL and sends it as theOriginheader on every request. The derivation:urllib.parse.urlspliton the configured URL.None(adds no header) when the URL has no scheme or host, so malformed/relative URLs are unaffected.Callers needing a different
Origin(e.g. multi-tenant proxies) can still set one on the underlyinghttpx.AsyncClientdefault headers.Verification
uv run pytest tests/shared/test_streamable_http.py— 63 passed (includes the existingtest_streamable_http_client_mcp_headers_override_defaults/ custom-header tests, unchanged).test_prepare_headers_includes_same_origin—http://my-go-server:8081/mcp→Origin: http://my-go-server:8081;https://…/path?x=1→https://example.com.test_prepare_headers_omits_origin_for_invalid_url— noOriginadded for a URL lacking scheme/host.uv run ruff check/ruff format --check— clean.Diff: 2 files, +41/-0.