Title: Scope HTTP client redirect following to the request's origin by maxisbey · Pull Request #3075 · modelcontextprotocol/python-sdk · GitHub
Open Graph Title: Scope HTTP client redirect following to the request's origin by maxisbey · Pull Request #3075 · modelcontextprotocol/python-sdk
X Title: Scope HTTP client redirect following to the request's origin by maxisbey · Pull Request #3075 · modelcontextprotocol/python-sdk
Description: Clients created by the SDK's httpx factory always enabled follow_redirects=True, so everything configured on a client — headers, auth, request bodies — was re-sent to whatever host a Location header named, and a misconfigured URL failed with an unhelpful Unexpected content type: error. This PR scopes redirect following to the request's origin and makes every refused redirect fail loudly with the target URL and the remedy. Motivation and Context follow_redirects=True was added for operational convenience: trailing-slash 307s from Starlette-mounted servers (#105, #732) and hosted-server redirects (#283). The dominant case — the same-origin slash fix — keeps working exactly as before; the server-side cause was also fixed long ago in #1115. What the blanket default additionally did was silently re-send client configuration to arbitrary redirect targets and silently mask misconfigured URLs. httpx's own default is follow_redirects=False for the same reason (encode/httpx#2533 keeps its cross-origin strip list deliberately minimal and leaves per-application policy to the application). Clients built by create_mcp_http_client now: follow redirects within the same origin (scheme, host, port) and same-host http→https upgrades on default ports — trailing-slash normalization and TLS-upgrade redirects keep working, verified end-to-end against real Starlette apps; raise the new RedirectError for any other redirect target, before the next request is sent, with a message naming the resolved target and how to proceed (connect to the final URL, or supply your own pre-configured httpx.AsyncClient after verifying the destination). Transports resolve a refused redirect in-band — a request gets a JSON-RPC error rather than a hung caller, notifications are delivered to the session's message handler, and the standalone GET stream stops retrying an endpoint that redirects on every attempt. Caller-supplied clients that follow no redirects (httpx's default) get the same clear error on POST, GET-stream, and SSE-connect paths. OAuth discovery, registration, token, refresh, and the identity-assertion token exchange fail loudly on redirect responses instead of silently trying the next URL or abandoning the discovery chain. create_mcp_http_client and RedirectError are now exported from the top-level mcp package (a deliberate API addition): the docs previously told users to build their own httpx.AsyncClient for headers/auth, which quietly lost the SDK's defaults. Docs and examples now configure clients through the factory; the general-purpose simple-tool fetch example gets a browser-like client of its own, since it fetches arbitrary web pages rather than a configured MCP endpoint. How Has This Been Tested? Real-server verification: booted MCPServer.streamable_http_app() under uvicorn and drove it with the real client. A trailing-slash /mcp/ URL initializes and completes a full session lifecycle (every request rides a genuine Starlette 307); a redirect to a different origin resolves initialization with a clear error naming the target; a typo'd path still yields the usual not-found error. New unit and transport tests (all proven failing on the base revision): same-origin and https-upgrade redirects followed with method/body/headers intact; other-host, other-port, https→http, and empty-host absolute-form Locations refused before a second request is sent; unparsable Location defers to httpx's own error; per-request and client-level headers behave identically; request/notification/resumption/GET-stream/SSE paths each surface the in-band error; OAuth handlers raise on redirect responses; a caller-supplied follow_redirects=True client is untouched (opt-in preserved). A parametrized test asserts the target-resolution helper matches response.next_request.url from httpx itself across relative, protocol-relative, absolute-form-without-host, and cross-origin shapes, so an httpx behavior change surfaces as a test failure rather than a silent policy divergence. Full suite: 5458 passed, 100% coverage including strict-no-cover, pyright and ruff clean. Breaking Changes Yes, deliberate and documented in docs/migration.md: a redirect to a different origin now raises mcp.RedirectError instead of being followed. Same-origin redirects and same-host https upgrades are unaffected. Deployments that genuinely need cross-origin redirect following can pass their own httpx.AsyncClient(follow_redirects=True) to streamable_http_client / sse_client. Types of changes Bug fix (non-breaking change which fixes an issue) New feature (non-breaking change which adds functionality) Breaking change (fix or feature that would cause existing functionality to change) Documentation update Checklist I have read the MCP Documentation My code follows the repository's style guidelines New and existing tests pass locally I have added appropriate error handling I have added or updated documentation as needed Additional context Four # pragma: no branch / # pragma: no cover comments are added in tests: three for the coverage.py nested-async with phantom-arc misreport documented in AGENTS.md, one marking an unreachable context-manager body in a connect-failure test. Known, accepted scope edges: resumption/reconnection GETs with a caller-supplied non-following client surface httpx's generic status error rather than the typed one (the factory's clients get the typed error everywhere); the origin comparison is deliberately conservative and fails loud on exotic-but-equivalent forms (IPv6 long-form vs. shorthand, trailing-dot hosts, non-default-port https upgrades) — supply your own client for those chains. The RedirectPolicy proposal in #2180 addressed the scheme-downgrade slice of the same default; with redirect targets now vetted at the factory for every consumer, that policy knob is superseded by this change. AI Disclaimer
Open Graph Description: Clients created by the SDK's httpx factory always enabled follow_redirects=True, so everything configured on a client — headers, auth, request bodies — was re-sent to whatever host a Location h...
X Description: Clients created by the SDK's httpx factory always enabled follow_redirects=True, so everything configured on a client — headers, auth, request bodies — was re-sent to whatever host a Locati...
Opengraph URL: https://github.com/modelcontextprotocol/python-sdk/pull/3075
X: @github
Domain: github.com
| route-pattern | /:user_id/:repository/pull/:id/files(.:format) |
| route-controller | pull_requests |
| route-action | files |
| fetch-nonce | v2:e31046a3-c64f-63d5-b8b0-6847077605a0 |
| current-catalog-service-hash | ae870bc5e265a340912cde392f23dad3671a0a881730ffdadd82f2f57d81641b |
| request-id | D8E2:26CE68:C67967:10C82B4:6A5B9A55 |
| html-safe-nonce | d5a8473201d4341e07dff2014b8d4d07b557eaecf414386e2295cec292139bc1 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJEOEUyOjI2Q0U2ODpDNjc5Njc6MTBDODJCNDo2QTVCOUE1NSIsInZpc2l0b3JfaWQiOiI0NzU3NTEwNjc4Mjg1ODgzOTg5IiwicmVnaW9uX2VkZ2UiOiJpYWQiLCJyZWdpb25fcmVuZGVyIjoiaWFkIn0= |
| visitor-hmac | 1d379707ae2ba3b973fd657467c54b46a9a25ac3eaf18d28fa16505e3d19ab72 |
| hovercard-subject-tag | pull_request:4013648428 |
| github-keyboard-shortcuts | repository,pull-request-list,pull-request-conversation,pull-request-files-changed,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/modelcontextprotocol/python-sdk/pull/3075/files |
| twitter:image | https://avatars.githubusercontent.com/u/224885523?s=400&v=4 |
| twitter:card | summary_large_image |
| og:image | https://avatars.githubusercontent.com/u/224885523?s=400&v=4 |
| og:image:alt | Clients created by the SDK's httpx factory always enabled follow_redirects=True, so everything configured on a client — headers, auth, request bodies — was re-sent to whatever host a Location h... |
| og:site_name | GitHub |
| og:type | object |
| hostname | github.com |
| expected-hostname | github.com |
| None | 5290d7e14309ad1e76106a9c4237bd1041517e83ea182c8ab756752cb0c6940b |
| turbo-cache-control | no-preview |
| diff-view | unified |
| go-import | github.com/modelcontextprotocol/python-sdk git https://github.com/modelcontextprotocol/python-sdk.git |
| octolytics-dimension-user_id | 182288589 |
| octolytics-dimension-user_login | modelcontextprotocol |
| octolytics-dimension-repository_id | 862584018 |
| octolytics-dimension-repository_nwo | modelcontextprotocol/python-sdk |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 862584018 |
| octolytics-dimension-repository_network_root_nwo | modelcontextprotocol/python-sdk |
| turbo-body-classes | logged-out env-production page-responsive full-width |
| disable-turbo | true |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 9c975978430e9ad293956f2bbdaf153b1bd84a99 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width