Skip to content

Commit b8775a1

Browse files
committed
Convert httpx usage added on main to httpx2
The rebase onto main picked up files added since the swap (stories examples, identity-assertion client/docs, client probe, docs_src tutorials) that still imported httpx. Apply the same httpx -> httpx2 rename to them, update the migration guide's mcp-types and identity-assertion sections to name httpx2, and document the certifi -> truststore TLS verification change.
1 parent dc6b9ea commit b8775a1

32 files changed

Lines changed: 170 additions & 170 deletions

File tree

docs/client/identity-assertion.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Everything below is the second request: the client that sends it and the authori
1919

2020
## The client
2121

22-
**`IdentityAssertionOAuthProvider`** lives in `mcp.client.auth.extensions.identity_assertion`. Like every provider in **[OAuth clients](oauth-clients.md)** it is an `httpx.Auth`: construct one, put it on `auth=`, hand the `httpx.AsyncClient` to the transport.
22+
**`IdentityAssertionOAuthProvider`** lives in `mcp.client.auth.extensions.identity_assertion`. Like every provider in **[OAuth clients](oauth-clients.md)** it is an `httpx2.Auth`: construct one, put it on `auth=`, hand the `httpx2.AsyncClient` to the transport.
2323

2424
```python title="client.py" hl_lines="49-50 53-61"
2525
--8<-- "docs_src/identity_assertion/tutorial001.py"
@@ -139,7 +139,7 @@ And notice what the returned `OAuthToken` does not carry: a refresh token. The I
139139

140140
* [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) lets the enterprise identity provider, not the end user, decide which MCP servers a client may reach. The IdP signs that decision into an **ID-JAG**.
141141
* Obtaining the ID-JAG is an [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token exchange against *your IdP*, and the SDK does not make it. Presenting it to the MCP authorization server is the [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) `jwt-bearer` grant, and the SDK does both sides of that.
142-
* `IdentityAssertionOAuthProvider` is another `httpx.Auth`: a pre-registered confidential client, a pinned `issuer`, and one `assertion_provider(audience, resource)` callback. No browser, no registration, no refresh token.
142+
* `IdentityAssertionOAuthProvider` is another `httpx2.Auth`: a pre-registered confidential client, a pinned `issuer`, and one `assertion_provider(audience, resource)` callback. No browser, no registration, no refresh token.
143143
* The authorization server is never discovered from the resource server. Configure `issuer` to exactly the string its metadata document serves; the comparison is character for character.
144144
* Server side, `identity_assertion_enabled=True` plus `exchange_identity_assertion`. The SDK authenticates the client and gates the grant; validating the ID-JAG is entirely yours, and the issued token is bound to the ID-JAG's `resource`, not the request's.
145145

docs/client/oauth-clients.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Some MCP servers are protected. Send them a request without a token and they answer `401 Unauthorized`.
44

5-
**`OAuthClientProvider`** is how you get the token. It is not an MCP object at all. It is an `httpx.Auth`, the standard httpx hook for "do something to every request". You attach it to an `httpx.AsyncClient`, hand that client to the Streamable HTTP transport, and stop thinking about it.
5+
**`OAuthClientProvider`** is how you get the token. It is not an MCP object at all. It is an `httpx2.Auth`, the standard httpx2 hook for "do something to every request". You attach it to an `httpx2.AsyncClient`, hand that client to the Streamable HTTP transport, and stop thinking about it.
66

77
This page is the client side. Making your own server demand a token is **[Authorization](../run/authorization.md)**.
88

@@ -68,9 +68,9 @@ A real client runs a small local HTTP server on the redirect URI instead of call
6868

6969
### Into the `Client`
7070

71-
Look at `main()`. The provider goes on the **httpx client**, the httpx client goes into `streamable_http_client(url, http_client=...)`, and that transport goes into `Client`.
71+
Look at `main()`. The provider goes on the **httpx2 client**, the httpx2 client goes into `streamable_http_client(url, http_client=...)`, and that transport goes into `Client`.
7272

73-
`streamable_http_client` has no `auth=` keyword. Anything HTTP-level (auth, headers, timeouts, proxies) belongs on the `httpx.AsyncClient` you bring. That layering is **[Client transports](transports.md)**.
73+
`streamable_http_client` has no `auth=` keyword. Anything HTTP-level (auth, headers, timeouts, proxies) belongs on the `httpx2.AsyncClient` you bring. That layering is **[Client transports](transports.md)**.
7474

7575
## What the provider does for you
7676

@@ -103,7 +103,7 @@ The URL must be HTTPS with a non-root path; anything else is a `ValueError` at c
103103

104104
A nightly job, a CI step, another service. There is no browser and nobody to click "allow". That is the **client credentials** grant: you already hold a `client_id` and a `client_secret`, and the token endpoint is the whole flow.
105105

106-
`ClientCredentialsOAuthProvider` is the same `httpx.Auth`, minus the human:
106+
`ClientCredentialsOAuthProvider` is the same `httpx2.Auth`, minus the human:
107107

108108
```python title="client.py" hl_lines="4 27-33"
109109
--8<-- "docs_src/oauth_clients/tutorial002.py"
@@ -113,7 +113,7 @@ What changed:
113113

114114
* No `OAuthClientMetadata`, no handlers. You pass `client_id` and `client_secret`; the provider builds a minimal `client_credentials` registration around them and skips dynamic registration entirely.
115115
* `scopes` is a space-separated string, the OAuth wire format.
116-
* Everything downstream is identical: the same `TokenStorage`, the same `httpx.AsyncClient(auth=...)`, the same `streamable_http_client`.
116+
* Everything downstream is identical: the same `TokenStorage`, the same `httpx2.AsyncClient(auth=...)`, the same `streamable_http_client`.
117117

118118
By default the secret travels as HTTP Basic auth on the token request (`client_secret_basic`). Pass `token_endpoint_auth_method="client_secret_post"` to put it in the form body instead. Some authorization servers only accept one of the two.
119119

@@ -133,11 +133,11 @@ There is one more no-human situation: the client belongs to an enterprise whose
133133

134134
When the OAuth flow goes wrong, the provider raises an `OAuthFlowError` from `mcp.client.auth`. It has two subclasses. `OAuthRegistrationError` means the authorization server refused to register you. `OAuthTokenError` means the token endpoint said no. One `except OAuthFlowError:` covers discovery, registration, authorization, and exchange.
135135

136-
Not everything is a flow error. The network can still fail; those are ordinary `httpx` exceptions and pass through untouched.
136+
Not everything is a flow error. The network can still fail; those are ordinary `httpx2` exceptions and pass through untouched.
137137

138138
## Recap
139139

140-
* `OAuthClientProvider` is an `httpx.Auth`. Put it on an `httpx.AsyncClient`, pass that to `streamable_http_client(url, http_client=...)`, and `Client` never knows OAuth happened.
140+
* `OAuthClientProvider` is an `httpx2.Auth`. Put it on an `httpx2.AsyncClient`, pass that to `streamable_http_client(url, http_client=...)`, and `Client` never knows OAuth happened.
141141
* You supply four things: the server URL, an `OAuthClientMetadata`, a `TokenStorage`, and the redirect/callback handler pair.
142142
* `TokenStorage` is a `Protocol`: four async methods, no base class. Persist `client_info` as well as the tokens.
143143
* Discovery, registration (dynamic, or via a **Client ID Metadata Document**), PKCE, the `state` and `iss` checks, and token refresh are the provider's job, not yours.

docs_src/client_transports/tutorial003.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import httpx
1+
import httpx2
22

33
from mcp import Client
44
from mcp.client.streamable_http import streamable_http_client
55

66

77
async def main() -> None:
8-
async with httpx.AsyncClient(
8+
async with httpx2.AsyncClient(
99
headers={"Authorization": "Bearer ..."},
10-
timeout=httpx.Timeout(30.0, read=300.0),
10+
timeout=httpx2.Timeout(30.0, read=300.0),
1111
follow_redirects=True,
1212
) as http_client:
1313
transport = streamable_http_client("http://localhost:8000/mcp", http_client=http_client)

docs_src/identity_assertion/tutorial001.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import time
22
import uuid
33

4-
import httpx
4+
import httpx2
55
import jwt
66

77
from mcp import Client
@@ -62,7 +62,7 @@ async def fetch_id_jag(audience: str, resource: str) -> str:
6262

6363

6464
async def main() -> None:
65-
async with httpx.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
65+
async with httpx2.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
6666
transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client)
6767
async with Client(transport) as client:
6868
result = await client.list_tools()

docs_src/oauth_clients/tutorial001.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from urllib.parse import parse_qs, urlparse
22

3-
import httpx
3+
import httpx2
44
from pydantic import AnyUrl
55

66
from mcp import Client
@@ -55,7 +55,7 @@ async def wait_for_callback() -> AuthorizationCodeResult:
5555

5656

5757
async def main() -> None:
58-
async with httpx.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
58+
async with httpx2.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
5959
transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client)
6060
async with Client(transport) as client:
6161
result = await client.list_tools()

docs_src/oauth_clients/tutorial002.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import httpx
1+
import httpx2
22

33
from mcp import Client
44
from mcp.client.auth.extensions.client_credentials import ClientCredentialsOAuthProvider
@@ -34,7 +34,7 @@ async def set_client_info(self, client_info: OAuthClientInformationFull) -> None
3434

3535

3636
async def main() -> None:
37-
async with httpx.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
37+
async with httpx2.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
3838
transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client)
3939
async with Client(transport) as client:
4040
result = await client.list_tools()

examples/snippets/clients/identity_assertion_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import asyncio
1818

19-
import httpx
19+
import httpx2
2020

2121
from mcp import ClientSession
2222
from mcp.client.auth.extensions.identity_assertion import IdentityAssertionOAuthProvider
@@ -66,7 +66,7 @@ async def main() -> None:
6666
scope="user",
6767
)
6868

69-
async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as http_client:
69+
async with httpx2.AsyncClient(auth=oauth_auth, follow_redirects=True) as http_client:
7070
async with streamable_http_client("http://localhost:8001/mcp", http_client=http_client) as (read, write):
7171
async with ClientSession(read, write) as session:
7272
await session.initialize()

examples/stories/_harness.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from urllib.parse import urlsplit
1919

2020
import anyio
21-
import httpx
21+
import httpx2
2222
from mcp_types.version import LATEST_MODERN_VERSION
2323

2424
from mcp import StdioServerParameters, stdio_client
@@ -38,8 +38,8 @@
3838
TargetFactory = Callable[[], Target]
3939
"""Yields a FRESH target against the same server/app on every call (``multi_connection`` stories)."""
4040

41-
AuthBuilder = Callable[[httpx.AsyncClient], httpx.Auth]
42-
"""Builds an ``httpx.Auth`` bound to the in-process HTTP client (auth-story harness seam)."""
41+
AuthBuilder = Callable[[httpx2.AsyncClient], httpx2.Auth]
42+
"""Builds an ``httpx2.Auth`` bound to the in-process HTTP client (auth-story harness seam)."""
4343

4444

4545
def argv_after(flag: str, *, default: str | None = None) -> str:
@@ -127,8 +127,8 @@ def _story_cfg(name: str) -> dict[str, Any]:
127127
return manifest["defaults"] | manifest["story"].get(name, {})
128128

129129

130-
def _authed_targets(url: str, http: httpx.AsyncClient) -> TargetFactory:
131-
"""Fresh streamable-HTTP transports over an already-authed ``httpx`` client."""
130+
def _authed_targets(url: str, http: httpx2.AsyncClient) -> TargetFactory:
131+
"""Fresh streamable-HTTP transports over an already-authed ``httpx2`` client."""
132132
return lambda: streamable_http_client(url, http_client=http)
133133

134134

@@ -176,13 +176,13 @@ async def _run() -> None:
176176
if url is None or (build_auth is None and not cfg["needs_http"]):
177177
await main(targets if cfg["multi_connection"] else targets(), mode=mode)
178178
return
179-
# Auth and needs_http stories want the raw httpx client underneath the transport:
180-
# build_auth threads an httpx.Auth onto it (Client(url, auth=...) doesn't exist
179+
# Auth and needs_http stories want the raw httpx2 client underneath the transport:
180+
# build_auth threads an httpx2.Auth onto it (Client(url, auth=...) doesn't exist
181181
# yet), and needs_http stories assert on raw responses, so root the client at the
182182
# server origin and relative paths like "/mcp" resolve.
183183
parts = urlsplit(url)
184184
base = f"{parts.scheme}://{parts.netloc}"
185-
http = await stack.enter_async_context(httpx.AsyncClient(base_url=base))
185+
http = await stack.enter_async_context(httpx2.AsyncClient(base_url=base))
186186
make = targets
187187
if build_auth is not None:
188188
http.auth = build_auth(http)

examples/stories/_hosting.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
AppFactory = Callable[[], Starlette]
2626

2727
NO_DNS_REBIND = TransportSecuritySettings(enable_dns_rebinding_protection=False)
28-
"""Harness servers bind 127.0.0.1 and the in-process httpx client sends no Origin header."""
28+
"""Harness servers bind 127.0.0.1 and the in-process httpx2 client sends no Origin header."""
2929

3030

3131
def argv_after(flag: str, *, default: str | None = None) -> str:

examples/stories/_shared/auth.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import time
1111
from urllib.parse import parse_qs, urlsplit
1212

13-
import httpx
13+
import httpx2
1414
from pydantic import AnyHttpUrl
1515

1616
from mcp.server.auth.provider import (
@@ -49,14 +49,14 @@ async def set_client_info(self, client_info: OAuthClientInformationFull) -> None
4949

5050

5151
class HeadlessOAuth:
52-
"""Completes the authorize redirect in-process via the bound ``httpx`` client."""
52+
"""Completes the authorize redirect in-process via the bound ``httpx2`` client."""
5353

5454
def __init__(self) -> None:
5555
self.authorize_url: str | None = None
56-
self._http: httpx.AsyncClient | None = None
56+
self._http: httpx2.AsyncClient | None = None
5757
self._result = AuthorizationCodeResult(code="", state=None)
5858

59-
def bind(self, http_client: httpx.AsyncClient) -> None:
59+
def bind(self, http_client: httpx2.AsyncClient) -> None:
6060
self._http = http_client
6161

6262
async def redirect_handler(self, authorization_url: str) -> None:

0 commit comments

Comments
 (0)