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. |
c9bf54b to
f24afa8
Compare
|
CI note — rebased onto current |
f24afa8 to
f690f39
Compare
f690f39 to
e45220d
Compare
5a585eb to
35dee20
Compare
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.
35dee20 to
a6f301c
Compare
|
Thanks for the PR, and sorry it sat here without a proper review. We're closing most of the open PR backlog. v2 is out and changed a lot of the SDK, so many older PRs no longer apply as written, and we're a small team that realistically doesn't have the capacity to work through the rest. If this still matters to you on v2, the most useful thing you can do is open an issue (or comment on the existing one) with your use case and a repro. Hearing why it matters to you is what we use to decide what to prioritise. |
|
Thanks for the note, and no worries on the wait. Understood that v2 moved and the backlog is being closed rather than reviewed in place. I won't reopen this PR. The original case (streamable HTTP client omitting Origin, so spec-compliant servers reject the handshake with 403) is already tracked on #2727. If that still reproduces on v2 I'll add a short repro there; if v2 already sends Origin, I'll leave it. |
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.