modelcontextprotocol / modelcontextprotocol/python-sdk

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

Aberta Para iniciantes
#2,727 3 comentários 0 reações 0 responsáveis Ver no GitHub

Ninguém assumiu esta issue ainda.

enhancement needs decision P3
Linguagem predominante
Python
Estrelas
24.3k
Forks
4k
Merge médio
1d 1h
PRs com merge (30d)
31

Descrição

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.

Guia de contribuição

Abrir o guia de contribuição

Primeiros passos

  1. Leia a issue inteira e depois o guia de contribuição do projeto.
  2. Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
  3. Faça um fork do repositório e trabalhe em uma branch.
  4. Abra um pull request que referencie o número da issue.

Direção de pesquisa

Comece em mcp.client.streamable_http, em streamablehttp_client e na configuração de httpx.AsyncClient; inspecione como os headers personalizados são passados. Reproduza o handshake com o handler Go descrito e, em seguida, verifique se o Origin padrão é enviado, se os headers fornecidos pelo chamador continuam sendo respeitados e se a inicialização da sessão é bem-sucedida.

Escrita pelo modelo de indexação a partir do texto da issue.

Avaliação

Stack de tecnologia
python
Domínio
api, backend
Tipo de issue
Bug
Dificuldade
2/5
Tempo estimado
1-3 horas
Status de atividade
Pouca atividade
Clareza
Claramente especificada
Facilidade para iniciantes
75/100

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.