modelcontextprotocol / modelcontextprotocol/python-sdk
streamable_http client does not send `Origin` header → rejected with 403 by spec-compliant servers (e.g. go-sdk `CrossOriginProtection`)
還沒有人認領這個 Issue。
- 主要語言
- Python
- 星號
- 24.3k
- 分支
- 4k
- 平均合併
- 1 天 1 小時
- 30 天內合併 PR
- 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 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
研究方向
從 mcp.client.streamable_http 中的 streamablehttp_client 和 httpx.AsyncClient 設定開始;檢查自訂 header 的傳遞方式。針對所描述的 Go handler 重現 handshake,然後驗證預設的 Origin 會被傳送、呼叫方提供的 header 仍會獲得保留,以及工作階段初始化成功。
由索引模型根據 Issue 內容生成。
評估
- 技術堆疊
- python
- 領域
- api, backend
- Issue 類型
- 缺陷
- 難度
- 2/5
- 預估耗時
- 1-3 小時
- 活躍度
- 冷清
- 描述清晰度
- 描述清楚
- 新手友好度
- 75/100