You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Does same-origin redirect enforcement break MCP servers behind a reverse proxy? #3504
Reading the v2.2.0 release notes: the HTTP client now fails redirects with MCPError unless the target is same-origin. For self-hosted setups this seems like it could bite people running an MCP server behind something like Caddy or nginx — e.g. an HTTP→HTTPS redirect, or a redirect from a bare port to a subpath, which often lands on a different-looking origin even though it's the same server.
The changelog doesn't say whether there's a way to allowlist additional origins for these cases, or whether the guidance is simply "don't let your reverse proxy redirect, terminate TLS and proxy_pass directly." Is there a documented escape hatch, or is this an intentional hard stop meant to force removing redirects from self-hosted deployments entirely? A line in the migration notes about the reverse-proxy case would save people a confusing debugging session when their server that worked in 2.1.x suddenly throws on upgrade.
Good question to surface before people hit it in production. This kind of gate exists because redirect-following with credentials attached is a credential-leak primitive: if the client re-sends the Authorization header to a redirect target, anything that can answer with a 301/302 attracts a bearer token. The strict check is best read as intentional, which means the practical guidance is to remove redirects from the API path rather than allowlist around them.
The trap is that strict origin comparison counts scheme and port, so http to https on the same host is cross-origin even though it looks like one server, and a bare port to a subpath hop is cross-origin too. Both show up constantly in self-hosted setups.
What works in practice:
Configure the client with the final endpoint the proxy serves after all redirects (https://host:port/mcp), so no hop occurs at all.
Terminate TLS at the proxy and proxy_pass the exact path the client uses; keep redirect rules on human-facing routes only. Redirects are for browsers, not API clients.
If both a plain and a TLS endpoint must exist, make the client config match one of them exactly rather than relying on the other to redirect into it.
Check normalization before assuming a mismatch is real: default ports (80/443) and hostname case are easy false positives when reading the error.
If the client does expose an allowlist or opt-out for additional origins, it needs a line in the migration notes, because the failure mode after upgrade is genuinely confusing: a server that worked in 2.1.x throws on connect with no obvious cause. Documenting the reverse-proxy recipe in the transport docs would save a lot of debugging sessions.
We run a connector layer over 36 business APIs (https://corpusiq.io) with the same posture: credentials never ride across an origin change, and the canonical endpoint is defined once at config time so a proxy change cannot silently reroute a token.
Read against main (v2.3.0 plus one commit), src/mcp/shared/_httpx_utils.py. The two cases you list come out differently, and there is no allowlist.
Followed (no change needed):
http→https on the same host with default ports (_within_origin: url.scheme == "http", both ports None, same host, location.scheme == "https").
Same scheme/host/port redirects such as trailing-slash normalisation, as long as the method is kept: 307/308 for a POST, any redirect status for the GET stream. At most client.max_redirects hops.
Not followed:
A redirect to a different host, port or scheme. A bare port to a subpath on the same origin is fine, because a path change doesn't change the origin, but if the proxy sends clients from :8080 to :443, or from http://host:8000 to https://host, that is a different origin and fails. The http→https exception only covers default ports on both sides.
A 301/302/303 on a POST, because httpx2 would turn it into a body-less GET and drop the message. That is the one a proxy rule like return 301 https://... hits when the configured URL was already http://host/mcp on a non-default port.
A Location that carries its own userinfo.
The follow_redirects flag on your own http_client is ignored (the docstring says so: "The client's follow_redirects setting is not consulted"), so there is no client-side escape hatch. The workaround is to give the client the final URL: configure https://host/mcp directly, and have the proxy answer on the path the client uses with proxy_pass instead of a redirect. For nginx, return 308 instead of 301 also lets a same-origin rewrite through; a cross-origin one will still be refused.
On failure the error names the redirect target (query and userinfo stripped), so the message tells you which Location to put in the configured URL. A line in the 2.2 migration notes would help, since the symptom looks like a connection error, not a config problem.
Activity
Ben-Home commentedon Sep 15, 2026
Good question to surface before people hit it in production. This kind of gate exists because redirect-following with credentials attached is a credential-leak primitive: if the client re-sends the Authorization header to a redirect target, anything that can answer with a 301/302 attracts a bearer token. The strict check is best read as intentional, which means the practical guidance is to remove redirects from the API path rather than allowlist around them.
The trap is that strict origin comparison counts scheme and port, so http to https on the same host is cross-origin even though it looks like one server, and a bare port to a subpath hop is cross-origin too. Both show up constantly in self-hosted setups.
What works in practice:
If the client does expose an allowlist or opt-out for additional origins, it needs a line in the migration notes, because the failure mode after upgrade is genuinely confusing: a server that worked in 2.1.x throws on connect with no obvious cause. Documenting the reverse-proxy recipe in the transport docs would save a lot of debugging sessions.
We run a connector layer over 36 business APIs (https://corpusiq.io) with the same posture: credentials never ride across an origin change, and the canonical endpoint is defined once at config time so a proxy change cannot silently reroute a token.
arhancanli commentedon Oct 9, 2026
Read against
main(v2.3.0 plus one commit),src/mcp/shared/_httpx_utils.py. The two cases you list come out differently, and there is no allowlist.Followed (no change needed):
_within_origin:url.scheme == "http", both portsNone, same host,location.scheme == "https").client.max_redirectshops.Not followed:
:8080to:443, or fromhttp://host:8000tohttps://host, that is a different origin and fails. The http→https exception only covers default ports on both sides.return 301 https://...hits when the configured URL was alreadyhttp://host/mcpon a non-default port.Locationthat carries its own userinfo.The
follow_redirectsflag on your ownhttp_clientis ignored (the docstring says so: "The client'sfollow_redirectssetting is not consulted"), so there is no client-side escape hatch. The workaround is to give the client the final URL: configurehttps://host/mcpdirectly, and have the proxy answer on the path the client uses withproxy_passinstead of a redirect. For nginx,return 308instead of301also lets a same-origin rewrite through; a cross-origin one will still be refused.On failure the error names the redirect target (query and userinfo stripped), so the message tells you which
Locationto put in the configured URL. A line in the 2.2 migration notes would help, since the symptom looks like a connection error, not a config problem.