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
so it never follows HTTP redirects. The MCP spec explicitly permits servers to respond to transport requests with 301/302/307/308 (load balancing, canonical URL / trailing-slash normalization — see e.g. https://github.com/IBM/mcp-context-forge#4441 for a real 307 on /mcp/). Against any redirecting server, every request degrades to:
The truncation is deliberate (there is a unit test asserting no custom-header leak to a redirect target — sensible given reqwest's default policy would replay custom headers), but the consequence is that a spec-compliant redirecting MCP server is unsupported out of the box.
Repro (no build needed)
Point any rmcp StreamableHttpClientTransport (default client) at a server that answers POST /mcp with 307 Location: <same-origin>/v2/mcp. Every post_message returns UnexpectedServerResponse("HTTP 307: ...") instead of issuing a follow-up request to the new Location.
Follow redirects safely rather than never: only follow cross-host/cross-lo*...* leftovers to be trimmed — or, at minimum, make redirect policy configurable on StreamableHttpClientTransportConfig while keeping the default strict (no-follow) for backwards compatibility. An explicit redirect policy override (as an associated constructor option) would let security-conscious users opt in to same-origin redirect following.
Environment
rmcp via modelcontextprotocol/rust-sdk (checked out 2026-08-22, rmcp crate 3.x)
I'd like to work on this. I plan to add a focused local same-origin 307 regression test and implement a safe redirect policy that preserves the existing cross-host and custom-header protections. Please let me know if this direction matches the project's expectations.
Friendly bump — checking in on this conformance report. Quick recap: the default StreamableHttpClientTransport never follows spec-permitted 301/302/307/308 redirects (redirect::Policy::none() at streamable_http_client.rs:381-387), so redirecting MCP servers hard-fail with Err(UnexpectedServerResponse("HTTP 307: ...")) — a spec-compliant server is unusable out of the box. @dawNotPoi has kindly offered to implement a safe redirect policy (same-origin + host guard); is that direction acceptable to the maintainers, or would you prefer a configurable policy option? Happy to help review/verify. PoC (scenario B prints the 307 error): https://github.com/trickyfalcon/cve-hunt/blob/main/poc-goose-rust/
Friendly bump (final scheduled check-in, day 21) — still no maintainer direction on @dawNotPoi's offer to implement a safe same-origin redirect policy. Recap: the default StreamableHttpClientTransport uses redirect::Policy::none() (streamable_http_client.rs:381-387), so spec-permitted 301/302/307/308 redirects hard-fail with Err(UnexpectedServerResponse(\"HTTP 307: ...\")) — a redirecting spec-compliant MCP server is unusable out of the box. Happy to help review/verify the regression test and policy once a PR lands. Is a transport-level fix planned? PoC (scenario B prints the 307 error): https://github.com/trickyfalcon/cve-hunt/blob/main/poc-goose-rust/
Hi @dawNotPoi, are you still planning to work on this? I'd be glad to review a PR, even a draft. If you've moved on to other things, would you mind if I picked it up?
One thing that might be worth covering: on 301/302/303, reqwest turns a POST into a GET with no body, so following those would drop the JSON-RPC message. 307/308 keep the body.
Either path is fine from my side — @dawNotPoi if you're still on it, otherwise @gocamille please take it; even a draft PR is enough to get review moving.
On the semantics: agreed, and it's exactly why the repro uses 307 (scenario B in https://github.com/trickyfalcon/cve-hunt/blob/main/poc-goose-rust/) — 307/308 preserve method+body so the JSON-RPC call survives the hop, while a naive follow of 301/302/303 rewrites POST→GET and silently drops the message. So the shape isn't just 'follow the spec codes': same-origin auto-follow for body-preserving 307/308, with 301/302/303 either surfaced to the caller or handled with a deliberate, documented rewrite — otherwise the hard failure just becomes silent message loss. Happy to verify whatever lands against the PoC (scenario B prints the 307 error).
Opened a draft in #1367 so there's something concrete to review. Thanks @trickyfalcon for the report and the 301/302/303 point; you're credited in the PR and commit.
@dawNotPoi, if you have work in progress, glad to fold it in or close mine.
@trickyfalcon, a run against your PoC (scenario B) would be welcome.
@DaleSeo, this relaxes the Policy::none() from #936, so it needs your sign-off on the approach: the default client follows only same-origin 307/308 (up to 10 hops), and 301/302/303 and cross-origin redirects still come back as errors. Does that direction work for you?
Ran the verification @gocamille asked for — PoC (scenario B) and the PR's own tests, both against current main (5d9935d) with the #1367 patch applied.
PoC run (self-contained harness: "public" endpoint on the LAN IP 307s every POST to a loopback victim that logs what it receives):
[B] rust-sdk default (Policy::none()): status=307, Location surfaced to the caller, victim untouched → the UnexpectedServerResponse("HTTP 307 …") hard-failure this issue reports, reproduced on latest main.
[A] goose-style client (no policy override), for contrast: redirect followed into the loopback service with user-agent: goose and the custom auth header forwarded — the other half of the same story (#11500).
[C] PR-equivalent guard: public→loopback hop stops, victim delta 0; [D] internal→internal hop still follows.
Cross-check vs the PR's policy (same_origin_redirect_policy): origin equality on the first previous() entry, 307/308-only, hop cap — all match the behavior I proposed in the thread. One deliberate divergence, flagged for completeness: at the cap the PR returns attempt.error(...) (reqwest Error::Redirect, surfacing via StreamableHttpError::Client) where my sketch used stop(); the loop test asserting MAX_REDIRECTS + 1 hits is consistent with the error path, so it's coherent — just note the cap is a hard error, not a silent stop.
PR test suite (cargo test -p rmcp --lib --features client,transport-streamable-http-client-reqwest):
patch applied: the 6 new tests pass — same-origin 307/308 followed with method/body/session/Authorization/x-api-key intact, 301/302/303 surfaced as UnexpectedServerResponse with no follow, cross-origin not followed, loop capped — and the full lib run is 293 passed / 0 failed.
same tree with the patch stashed: 1 preexisting redirect test, so the +6 delta is exactly this PR's coverage, including the rewritten L593 same-host expectation.
Happy with the approach; the @DaleSeo sign-off on relaxing #936 is the remaining gate. For the eventual release notes / advisory:
I'm good with this direction. #936 was about not sending custom headers or the session ID to a different host, and a same-origin-only policy preserves that guarantee. Limiting redirects to 307 and 308 also avoids the 301, 302, and 303 POST-to-GET rewrite, which would silently drop the JSON-RPC message. So that seems like the right call. Leaving caller-supplied clients alone sounds right to me too.
@gocamille, feel free to mark it ready for review. It would be nice to add a short note to the default_http_client docs about the redirect behavior. Updating the stored URI for 308 can be a follow-up.
@trickyfalcon, about credit, this is a conformance fix, not a security vulnerability, so there won't be an advisory.
Activity
dawNotPoi commentedon Aug 23, 2026
I'd like to work on this. I plan to add a focused local same-origin 307 regression test and implement a safe redirect policy that preserves the existing cross-host and custom-header protections. Please let me know if this direction matches the project's expectations.
trickyfalcon commentedon Sep 7, 2026
Friendly bump — checking in on this conformance report. Quick recap: the default
StreamableHttpClientTransportnever follows spec-permitted 301/302/307/308 redirects (redirect::Policy::none()atstreamable_http_client.rs:381-387), so redirecting MCP servers hard-fail withErr(UnexpectedServerResponse("HTTP 307: ..."))— a spec-compliant server is unusable out of the box. @dawNotPoi has kindly offered to implement a safe redirect policy (same-origin + host guard); is that direction acceptable to the maintainers, or would you prefer a configurable policy option? Happy to help review/verify. PoC (scenario B prints the 307 error): https://github.com/trickyfalcon/cve-hunt/blob/main/poc-goose-rust/Happy to be credited in the advisory/release notes as: Mo (@trickyfalcon, https://trickyfalcon.com)
trickyfalcon commentedon Sep 12, 2026
Friendly bump (final scheduled check-in, day 21) — still no maintainer direction on @dawNotPoi's offer to implement a safe same-origin redirect policy. Recap: the default
StreamableHttpClientTransportusesredirect::Policy::none()(streamable_http_client.rs:381-387), so spec-permitted 301/302/307/308 redirects hard-fail withErr(UnexpectedServerResponse(\"HTTP 307: ...\"))— a redirecting spec-compliant MCP server is unusable out of the box. Happy to help review/verify the regression test and policy once a PR lands. Is a transport-level fix planned? PoC (scenario B prints the 307 error): https://github.com/trickyfalcon/cve-hunt/blob/main/poc-goose-rust/Happy to be credited in the advisory/release notes as: Mo (@trickyfalcon, https://trickyfalcon.com)
gocamille commentedon Oct 14, 2026
Hi @dawNotPoi, are you still planning to work on this? I'd be glad to review a PR, even a draft. If you've moved on to other things, would you mind if I picked it up?
One thing that might be worth covering: on 301/302/303, reqwest turns a POST into a GET with no body, so following those would drop the JSON-RPC message. 307/308 keep the body.
trickyfalcon commentedon Oct 15, 2026
Either path is fine from my side — @dawNotPoi if you're still on it, otherwise @gocamille please take it; even a draft PR is enough to get review moving.
On the semantics: agreed, and it's exactly why the repro uses 307 (scenario B in https://github.com/trickyfalcon/cve-hunt/blob/main/poc-goose-rust/) — 307/308 preserve method+body so the JSON-RPC call survives the hop, while a naive follow of 301/302/303 rewrites POST→GET and silently drops the message. So the shape isn't just 'follow the spec codes': same-origin auto-follow for body-preserving 307/308, with 301/302/303 either surfaced to the caller or handled with a deliberate, documented rewrite — otherwise the hard failure just becomes silent message loss. Happy to verify whatever lands against the PoC (scenario B prints the 307 error).
gocamille commentedon Oct 15, 2026
Opened a draft in #1367 so there's something concrete to review. Thanks @trickyfalcon for the report and the 301/302/303 point; you're credited in the PR and commit.
@dawNotPoi, if you have work in progress, glad to fold it in or close mine.
@trickyfalcon, a run against your PoC (scenario B) would be welcome.
@DaleSeo, this relaxes the
Policy::none()from #936, so it needs your sign-off on the approach: the default client follows only same-origin 307/308 (up to 10 hops), and 301/302/303 and cross-origin redirects still come back as errors. Does that direction work for you?trickyfalcon commentedon Oct 16, 2026
Ran the verification @gocamille asked for — PoC (scenario B) and the PR's own tests, both against current main (5d9935d) with the #1367 patch applied.
PoC run (self-contained harness: "public" endpoint on the LAN IP 307s every POST to a loopback victim that logs what it receives):
[B]rust-sdk default (Policy::none()):status=307,Locationsurfaced to the caller, victim untouched → theUnexpectedServerResponse("HTTP 307 …")hard-failure this issue reports, reproduced on latest main.[A]goose-style client (no policy override), for contrast: redirect followed into the loopback service withuser-agent: gooseand the custom auth header forwarded — the other half of the same story (#11500).[C]PR-equivalent guard: public→loopback hop stops, victim delta 0;[D]internal→internal hop still follows.Cross-check vs the PR's policy (
same_origin_redirect_policy): origin equality on the firstprevious()entry, 307/308-only, hop cap — all match the behavior I proposed in the thread. One deliberate divergence, flagged for completeness: at the cap the PR returnsattempt.error(...)(reqwestError::Redirect, surfacing viaStreamableHttpError::Client) where my sketch usedstop(); the loop test assertingMAX_REDIRECTS + 1hits is consistent with the error path, so it's coherent — just note the cap is a hard error, not a silent stop.PR test suite (
cargo test -p rmcp --lib --features client,transport-streamable-http-client-reqwest):x-api-keyintact, 301/302/303 surfaced asUnexpectedServerResponsewith no follow, cross-origin not followed, loop capped — and the full lib run is 293 passed / 0 failed.Happy with the approach; the @DaleSeo sign-off on relaxing #936 is the remaining gate. For the eventual release notes / advisory:
Happy to be credited in the advisory/release notes as: Mo (@trickyfalcon, https://trickyfalcon.com)
DaleSeo commentedon Oct 16, 2026
I'm good with this direction. #936 was about not sending custom headers or the session ID to a different host, and a same-origin-only policy preserves that guarantee. Limiting redirects to 307 and 308 also avoids the 301, 302, and 303 POST-to-GET rewrite, which would silently drop the JSON-RPC message. So that seems like the right call. Leaving caller-supplied clients alone sounds right to me too.
@gocamille, feel free to mark it ready for review. It would be nice to add a short note to the
default_http_clientdocs about the redirect behavior. Updating the stored URI for 308 can be a follow-up.@trickyfalcon, about credit, this is a conformance fix, not a security vulnerability, so there won't be an advisory.
gocamille commentedon Oct 16, 2026
Thanks @DaleSeo! #1367 is now ready for review, with the doc note added. The 308 stored-URI update is tracked in #1373.