Skip to content

fix(client): send same-origin Origin header from streamable HTTP client#2729

Open
Bartok9 wants to merge 1 commit into
modelcontextprotocol:mainfrom
Bartok9:fix/2727-streamable-origin-header
Open

fix(client): send same-origin Origin header from streamable HTTP client#2729
Bartok9 wants to merge 1 commit into
modelcontextprotocol:mainfrom
Bartok9:fix/2727-streamable-origin-header

Conversation

@Bartok9
Copy link
Copy Markdown

@Bartok9 Bartok9 commented May 30, 2026

Summary

  • The streamable HTTP client now sends a same-origin Origin header by default.
  • This lets the official Python client interoperate with spec-compliant servers (e.g. the Go SDK's http.CrossOriginProtection) that otherwise reject the handshake with 403.

Motivation

Closes #2727.

The Python streamablehttp_client opened its POST handshake without an Origin header. The official Go SDK (modelcontextprotocol/go-sdk v1.4.x) wraps every streamable-HTTP handler with Go 1.25's http.CrossOriginProtection, which denies any state-changing request that cannot prove same-origin via Sec-Fetch-Site, a matching Origin, or an allow-listed origin. A legitimate server-to-server connection from the Python client therefore looks like a CSRF attempt → HTTP 403 Forbidden on the first POST, and the client (per #2110) swallows the non-2xx and hangs forever on session.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 the Origin header on every request. The derivation:

  • Uses urllib.parse.urlsplit on the configured URL.
  • Returns 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 underlying httpx.AsyncClient default headers.

Verification

  • uv run pytest tests/shared/test_streamable_http.py63 passed (includes the existing test_streamable_http_client_mcp_headers_override_defaults / custom-header tests, unchanged).
  • New tests:
    • test_prepare_headers_includes_same_originhttp://my-go-server:8081/mcpOrigin: http://my-go-server:8081; https://…/path?x=1https://example.com.
    • test_prepare_headers_omits_origin_for_invalid_url — no Origin added for a URL lacking scheme/host.
  • uv run ruff check / ruff format --check — clean.

Diff: 2 files, +41/-0.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

streamable_http client does not send Origin header → rejected with 403 by spec-compliant servers (e.g. go-sdk CrossOriginProtection)

1 participant