modelcontextprotocol / modelcontextprotocol/python-sdk
streamable_http client does not send `Origin` header → rejected with 403 by spec-compliant servers (e.g. go-sdk `CrossOriginProtection`)
まだ誰も着手していません。
- 主要言語
- Python
- スター
- 24.3k
- フォーク
- 4k
- 平均マージ
- 1日 1時間
- マージ済み PR(30日)
- 31
説明
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 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
Originheader whose host matches the server'sHost, 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:
handler := mcp.NewStreamableHTTPHandler(
func(_ *http.Request) *mcp.Server { return srv },
nil, // default CrossOriginProtection: deny non-same-origin
)
http.Handle("/mcp", handler)
Client — Python mcp SDK:
from mcp.client.streamable_http import streamablehttp_client
from mcp.client.session import ClientSession
async with streamablehttp_client("http://my-go-server:8081/mcp") as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize() # never completes
Observed:
httpxPOSTs to/mcpwith noOriginheader, noSec-Fetch-*headers.- Go server returns
HTTP/1.1 403 Forbiddenimmediately. - Python client
post_writerswallows the non-2xx (see #2110), andsession.initialize()hangs forever on the read stream. - Eventually the caller (e.g. a FastAPI startup hook with a
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 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}withcop.AddTrustedOrigin(...), or setGODEBUG=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 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 the httpx.AsyncClient, derive a default Origin header from the target URL's scheme + netloc and add it to every outgoing request:
parsed = urlparse(self.url)
default_origin = f"{parsed.scheme}://{parsed.netloc}"
headers.setdefault("Origin", default_origin)
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.
Related
- #2110 — explains why the resulting 403 manifests as a client hang instead of a clean exception.
- #1798 / #861 — the Python server added equivalent DNS-rebinding protection; the client never picked up the matching header behavior.
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).- Go 1.25 (stdlib
http.CrossOriginProtection). - Python 3.13, anyio 4.x.
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
調査の方向性
mcp.client.streamable_http の streamablehttp_client と httpx.AsyncClient のセットアップから始め、カスタムヘッダーがどのように渡されるかを調べます。説明されている Go ハンドラーに対して handshake を再現し、その後、デフォルトの Origin が送信されること、呼び出し元が指定したヘッダーが引き続き尊重されること、セッションの初期化が成功することを確認します。
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- python
- 領域
- api, backend
- issue の種類
- バグ
- 難易度
- 2/5
- 見積もり時間
- 1〜3時間
- 活発さ
- 静か
- 明瞭さ
- 明確に書かれている
- 初心者へのやさしさ
- 75/100