modelcontextprotocol / modelcontextprotocol/python-sdk

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

Ouverte Adaptée aux débutants
#2,727 3 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

enhancement needs decision P3
Langage dominant
Python
Étoiles
24.3k
Forks
4k
Merge moyen
1 j 1 h
PR mergées (30 j)
31

Description

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 Origin header whose host matches the server's Host, 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:

  1. httpx POSTs to /mcp with no Origin header, no Sec-Fetch-* headers.
  2. Go server returns HTTP/1.1 403 Forbidden immediately.
  3. Python client post_writer swallows the non-2xx (see #2110), and session.initialize() hangs forever on the read stream.
  4. Eventually the caller (e.g. a FastAPI startup hook with a wait_for timeout) cancels, which surfaces as RuntimeError: Attempted to exit cancel scope in a different task than it was entered in because the streamable_http_client task 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} with cop.AddTrustedOrigin(...), or set GODEBUG=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 of streamable_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 via mcp >= 1.x (tested under Locus's locus.integrations.fastmcp.MCPClient wrapper, which is a thin pass-through to streamablehttp_client).
  • modelcontextprotocol/go-sdk@v1.4.1 (server).
  • Go 1.25 (stdlib http.CrossOriginProtection).
  • Python 3.13, anyio 4.x.

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Piste de recherche

Commencez dans mcp.client.streamable_http, au niveau de streamablehttp_client et de la configuration de httpx.AsyncClient ; examinez comment les en-têtes personnalisés sont transmis. Reproduisez le handshake avec le handler Go décrit, puis vérifiez que l’Origin par défaut est envoyé, que les en-têtes fournis par l’appelant restent respectés et que l’initialisation de la session réussit.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
python
Domaine
api, backend
Type d'issue
Bug
Difficulté
2/5
Temps estimé
1-3 heures
Activité
Calme
Clarté
Clairement spécifiée
Accessibilité débutants
75/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.