-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Scope HTTP client redirect following to the request's origin #3075
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,7 +29,7 @@ | |
| --8<-- "docs_src/client_transports/tutorial002.py" | ||
| ``` | ||
|
|
||
| That is the whole production client. `Client` wraps the URL in `streamable_http_client(...)` for you, on top of an `httpx.AsyncClient` configured the way MCP needs: `follow_redirects=True`, a 30-second timeout for connect/write/pool, and a 300-second read timeout because the server may hold a response stream open. | ||
| That is the whole production client. `Client` wraps the URL in `streamable_http_client(...)` for you, on top of an `httpx.AsyncClient` configured the way MCP needs: same-origin redirect handling, a 30-second timeout for connect/write/pool, and a 300-second read timeout because the server may hold a response stream open. | ||
|
Check warning on line 32 in docs/client/transports.md
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 The 'Bring your own Extended reasoning...What the bug is. The specific contradictions. Line 46 tells the reader to "build the Why this matters beyond cosmetics. The stale prose actively steers readers toward constructing a plain Step-by-step proof. (1) A reader lands on the "Bring your own" section needing an Why nothing prevents it. The docs snippets are executed as code (so tutorial003 itself is correct and tested), but prose is not checked against the snippets it surrounds, so the inconsistency ships silently. How to fix. Rewrite the section prose to match the snippet: say to build the client with |
||
|
|
||
| !!! check | ||
| A `Client` you have constructed is **not** connected. Construction only picks the transport; | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -1595,14 +1595,15 @@ async with streamablehttp_client( | |||||
|
|
||||||
| ```python | ||||||
| import httpx | ||||||
| from mcp import create_mcp_http_client | ||||||
| from mcp.client.streamable_http import streamable_http_client | ||||||
|
|
||||||
| # Configure headers, timeout, and auth on the httpx.AsyncClient | ||||||
| http_client = httpx.AsyncClient( | ||||||
| # Configure headers, timeout, and auth on the client. create_mcp_http_client | ||||||
| # applies the SDK defaults (timeouts, same-origin redirect handling). | ||||||
| http_client = create_mcp_http_client( | ||||||
| headers={"Authorization": "Bearer token"}, | ||||||
| timeout=httpx.Timeout(30, read=300), | ||||||
| auth=my_auth, | ||||||
| follow_redirects=True, | ||||||
| ) | ||||||
|
|
||||||
| async with http_client: | ||||||
|
|
@@ -1613,7 +1614,26 @@ async with http_client: | |||||
| ... | ||||||
| ``` | ||||||
|
|
||||||
| v1's internal client set `follow_redirects=True`; set it explicitly when supplying your own `httpx.AsyncClient` to preserve that behavior. | ||||||
| Prefer `create_mcp_http_client` when supplying your own client: a plain `httpx.AsyncClient` does not follow redirects at all, and configuring `follow_redirects=True` yourself sends everything on the client (headers, auth, bodies) to whichever server a redirect names. | ||||||
|
|
||||||
| ### HTTP clients only follow same-origin redirects | ||||||
|
|
||||||
| Clients created by the SDK (including `Client("http://...")`, `sse_client`, and the | ||||||
| default client inside `streamable_http_client`) now vet redirect targets before | ||||||
| following. Redirects that stay on the same origin (scheme, host, and port) and | ||||||
| http-to-https upgrades of the same host still work; any other redirect raises | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The migration note overstates which HTTPS-upgrade redirects remain allowed. The redirect policy described for this change only preserves same-host HTTP→HTTPS upgrades on default ports, so adding that qualifier here would avoid misleading deployments that use custom ports into expecting those redirects to be followed. Prompt for AI agents
Suggested change
|
||||||
| `mcp.RedirectError` naming the target instead of being followed. Everything | ||||||
| configured on a client - headers, auth, request bodies - is sent to whichever | ||||||
| server it talks to, so requests now stay with the server you configured, and a | ||||||
| misconfigured URL fails with a clear error instead of silently bouncing. | ||||||
|
|
||||||
| To migrate: connect to the final URL the error names. If your deployment | ||||||
| genuinely requires following a redirect to a different origin, supply your own | ||||||
| `httpx.AsyncClient(follow_redirects=True)` - after verifying you trust every | ||||||
| destination in the chain. | ||||||
|
|
||||||
| `create_mcp_http_client` (the SDK's client factory) and `RedirectError` are now | ||||||
| exported from the top-level `mcp` package. | ||||||
|
|
||||||
| ### `get_session_id` callback removed from `streamable_http_client` | ||||||
|
|
||||||
|
|
@@ -1638,6 +1658,7 @@ async with streamable_http_client(url) as (read_stream, write_stream, get_sessio | |||||
|
|
||||||
| ```python | ||||||
| import httpx | ||||||
| from mcp import create_mcp_http_client | ||||||
| from mcp.client.streamable_http import streamable_http_client | ||||||
|
|
||||||
| # Option 1: Simply ignore if you don't need the session ID | ||||||
|
|
@@ -1653,10 +1674,8 @@ async def capture_session_id(response: httpx.Response) -> None: | |||||
| if session_id: | ||||||
| captured_session_ids.append(session_id) | ||||||
|
|
||||||
| http_client = httpx.AsyncClient( | ||||||
| event_hooks={"response": [capture_session_id]}, | ||||||
| follow_redirects=True, | ||||||
| ) | ||||||
| http_client = create_mcp_http_client() | ||||||
| http_client.event_hooks["response"].append(capture_session_id) | ||||||
|
|
||||||
| async with http_client: | ||||||
| async with streamable_http_client(url, http_client=http_client) as (read_stream, write_stream): | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,7 @@ | |
| validate_authorization_response_iss, | ||
| validate_metadata_issuer, | ||
| ) | ||
| from mcp.shared._httpx_utils import redirect_error | ||
| from mcp.shared.auth import ( | ||
| AuthorizationCodeResult, | ||
| OAuthClientInformationFull, | ||
|
|
@@ -413,9 +414,11 @@ | |
| token_data, headers = self.context.prepare_token_auth(token_data, headers) | ||
|
|
||
| return httpx.Request("POST", token_url, data=token_data, headers=headers) | ||
|
|
||
| async def _handle_token_response(self, response: httpx.Response) -> None: | ||
| """Handle token exchange response.""" | ||
| if response.has_redirect_location: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Prompt for AI agents |
||
| raise redirect_error(response, context="OAuth token request") | ||
|
Check warning on line 421 in src/mcp/client/auth/oauth2.py
|
||
|
Comment on lines
417
to
+421
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Extended reasoning...What the bug is. This PR standardizes redirect handling across the OAuth response handlers: Why nothing fails at runtime. Grep confirms the provider method has zero callers in What the divergence looks like. Concrete walkthrough of a 307 response through each copy:
Why it matters. Two copies of the same logic now embody two different redirect policies. Any future caller (or a refactor that swaps the utils call back to the method — it was clearly the original implementation the utils function was extracted from) silently reverts to the old behavior. Since every sibling handler in the same class got the check, this copy was plainly missed rather than deliberately excluded. How to fix. Preferably delete the dead method and repoint its three tests at Not a merge blocker: the method is unreachable in the production flow, so merging as-is breaks nothing — this is a code-hygiene/consistency cleanup. |
||
| if response.status_code not in {200, 201}: | ||
| body = await response.aread() | ||
| body_text = body.decode("utf-8") | ||
|
|
@@ -468,6 +471,8 @@ | |
|
|
||
| async def _handle_refresh_response(self, response: httpx.Response) -> bool: | ||
| """Handle token refresh response. Returns True if successful.""" | ||
| if response.has_redirect_location: | ||
| raise redirect_error(response, context="OAuth token refresh request") | ||
| if response.status_code != 200: | ||
| logger.warning(f"Token refresh failed: {response.status_code}") | ||
| self.context.clear_tokens() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: The prose in this section now contradicts the embedded
tutorial003.pysnippet. The snippet was updated from a plainhttpx.AsyncClienttocreate_mcp_http_client(...), but downstream text (around line 46) still tells the reader to "build thehttpx.AsyncClientyourself" and the info box claims "The SDK adds nothing on top and takes nothing away." Both are now false —create_mcp_http_clientinstalls the_raise_on_disallowed_redirectresponse hook and applies MCP default timeouts. This creates conflicting guidance withdocs/migration.md, which explicitly warns against using a plainhttpx.AsyncClient. The surrounding prose (and thehttpx.AsyncClient(auth=OAuthClientProvider(...))example in the info box) should be updated to match the factory-based approach the snippet now demonstrates.Prompt for AI agents
-That is the whole production client.
Clientwraps the URL instreamable_http_client(...)for you, on top of anhttpx.AsyncClientconfigured the way MCP needs:follow_redirects=True, a 30-second timeout for connect/write/pool, and a 300-second read timeout because the server may hold a response stream open.+That is the whole production client.
Clientwraps the URL instreamable_http_client(...)for you, on top of anhttpx.AsyncClientconfigured the way MCP needs: same-origin redirect handling, a 30-second timeout for connect/write/pool, and a 300-second read timeout because the server may hold a response stream open.!!! check
</file context>