modelcontextprotocol / modelcontextprotocol/python-sdk
streamable_http client does not send `Origin` header → rejected with 403 by spec-compliant servers (e.g. go-sdk `CrossOriginProtection`)
Chưa có ai nhận issue này.
- Ngôn ngữ chính
- Python
- Star
- 24.3k
- Fork
- 4k
- Merge trung bình
- 1 ngày 1 giờ
- Pull request đã merge (30 ngày)
- 31
Mô tả
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.
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Hướng nghiên cứu
Bắt đầu trong mcp.client.streamable_http, tại streamablehttp_client và phần thiết lập httpx.AsyncClient; kiểm tra cách các header tùy chỉnh được truyền vào. Tái hiện handshake với Go handler được mô tả, sau đó xác minh rằng Origin mặc định được gửi, các header do bên gọi cung cấp vẫn được tôn trọng và việc khởi tạo phiên thành công.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- python
- Lĩnh vực
- api, backend
- Loại issue
- Lỗi
- Độ khó
- 2/5
- Thời gian dự kiến
- 1-3 giờ
- Mức độ hoạt động
- Ít trao đổi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức phù hợp với người mới
- 75/100