Skip to content

Commit 2c901e8

Browse files
committed
Port the httpx2 migration notes into the restructured docs
The migration guide and client docs were reorganized on main after the swap branched: re-add the httpx2 section under Packaging with rows in the dependency table, convert the After-v2 examples that arrived with the reorganization (Before-v1 blocks keep httpx), and surface the system-trust-store TLS note on the transports page. No-Verification-Needed: documentation-only change
1 parent 422d7b0 commit 2c901e8

2 files changed

Lines changed: 72 additions & 22 deletions

File tree

docs/client/transports.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Pass a URL string and you get **Streamable HTTP**, the transport you deploy behi
2929
--8<-- "docs_src/client_transports/tutorial002.py"
3030
```
3131

32-
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.
32+
That is the whole production client. `Client` wraps the URL in `streamable_http_client(...)` for you, on top of an `httpx2.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.
3333

3434
!!! check
3535
A `Client` you have constructed is **not** connected. Construction only picks the transport;
@@ -41,19 +41,25 @@ That is the whole production client. `Client` wraps the URL in `streamable_http_
4141

4242
Nothing was resolved, fetched or spawned when you wrote `Client("http://...")`. That line is free.
4343

44-
### Bring your own `httpx.AsyncClient`
44+
### Bring your own `httpx2.AsyncClient`
4545

46-
The moment you need an `Authorization` header, a cookie, a proxy, mTLS, or a different timeout, build the `httpx.AsyncClient` yourself and hand it to `streamable_http_client`:
46+
The moment you need an `Authorization` header, a cookie, a proxy, mTLS, or a different timeout, build the `httpx2.AsyncClient` yourself and hand it to `streamable_http_client`:
4747

4848
```python title="client.py" hl_lines="8-14"
4949
--8<-- "docs_src/client_transports/tutorial003.py"
5050
```
5151

5252
Two things to notice:
5353

54-
* You own the `httpx.AsyncClient`, so **you** enter and exit it. The SDK never closes a client it didn't create.
54+
* You own the `httpx2.AsyncClient`, so **you** enter and exit it. The SDK never closes a client it didn't create.
5555
* `streamable_http_client(url, http_client=...)` returns a transport, and `Client(transport)` accepts it like anything else.
5656

57+
One TLS note: `httpx2` verifies certificates against the operating system trust store (via
58+
[`truststore`](https://pypi.org/project/truststore/)), not a bundled CA list. In an environment with
59+
no usable system CA store - some minimal containers - pass an explicit `verify=ssl_context` to your
60+
`httpx2.AsyncClient` (background in
61+
[`httpx` and `httpx-sse` replaced by `httpx2`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)).
62+
5763
!!! warning
5864
`streamable_http_client` used to take `headers=` and `timeout=` directly. It does not any more:
5965
its only parameters are `url`, `http_client` and `terminate_on_close`. Reach for `headers=` out
@@ -63,12 +69,13 @@ Two things to notice:
6369
TypeError: streamable_http_client() got an unexpected keyword argument 'headers'
6470
```
6571

66-
Everything HTTP-shaped now lives on the one `httpx.AsyncClient` you pass in.
72+
Everything HTTP-shaped now lives on the one `httpx2.AsyncClient` you pass in.
6773

6874
!!! info
69-
If you know `httpx`, you already know how to do auth, proxies, event hooks, retries and connection
70-
limits here. The SDK adds nothing on top and takes nothing away. It is also where OAuth plugs in:
71-
`httpx.AsyncClient(auth=OAuthClientProvider(...))`. That whole flow is **[OAuth clients](oauth-clients.md)**.
75+
`httpx2` keeps the familiar `httpx` API, so if you know `httpx` you already know how to do auth,
76+
proxies, event hooks, retries and connection limits here. The SDK adds nothing on top and takes
77+
nothing away. It is also where OAuth plugs in:
78+
`httpx2.AsyncClient(auth=OAuthClientProvider(...))`. That whole flow is **[OAuth clients](oauth-clients.md)**.
7279

7380
## stdio
7481

@@ -106,7 +113,7 @@ A **transport** is any async context manager that yields a `(read, write)` pair
106113

107114
* `Client(mcp)` (the server object) connects in memory. Use it for tests and for embedding.
108115
* `Client("http://.../mcp")` (a URL) connects over Streamable HTTP, the production transport.
109-
* Headers, auth, proxies and timeouts belong on an `httpx.AsyncClient` you pass to `streamable_http_client(url, http_client=...)`. There is no `headers=` keyword.
116+
* Headers, auth, proxies and timeouts belong on an `httpx2.AsyncClient` you pass to `streamable_http_client(url, http_client=...)`. There is no `headers=` keyword.
110117
* stdio is `Client(stdio_client(StdioServerParameters(...)))`, never the parameters object alone.
111118
* The subprocess gets an allow-listed environment, not yours; `env=` adds to it.
112119
* A transport is anything you can `async with x as (read, write)`. `Client` hands anything that isn't a server object or a URL straight to that protocol.

docs/migration.md

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ v2 raises the minimum versions of several shared dependencies and adds new requi
6767
| pywin32 (Windows) | `>=310` (Python <3.14) | `>=311` | floor raised on Python <3.14 |
6868
| opentelemetry-api | not a dependency | `>=1.28.0` | new required dependency |
6969
| mcp-types | not a dependency | `==<exact mcp version>` | new, exact-pinned |
70+
| httpx | `>=0.27.1,<1.0.0` | removed | see [`httpx` and `httpx-sse` replaced by `httpx2`](#httpx-and-httpx-sse-replaced-by-httpx2) |
71+
| httpx-sse | `>=0.4` | removed | see [`httpx` and `httpx-sse` replaced by `httpx2`](#httpx-and-httpx-sse-replaced-by-httpx2) |
72+
| httpx2 | not a dependency | `>=2.5.0` | new required dependency |
7073
| `ws` extra | `websockets>=15.0.1` | removed | see [WebSocket transport removed](#websocket-transport-removed) |
7174

7275
**Before (v1):**
@@ -89,6 +92,46 @@ dependencies = [
8992

9093
Relax or bump any conflicting pins when upgrading. sse-starlette jumps two majors, so a project that imports `sse_starlette` itself must also work through that library's own breaking changes to co-install with mcp v2. `opentelemetry-api` is a new hard dependency because every outbound request now carries a `_meta` envelope used for OpenTelemetry trace propagation; see [Every outbound request now carries a `_meta` envelope](#every-outbound-request-now-carries-a-_meta-envelope-opentelemetry-is-on-by-default). `mcp-types` is exact-pinned to the SDK version; nothing in a v1 tree can conflict with it, but do not pin `mcp-types` independently of `mcp`.
9194

95+
### `httpx` and `httpx-sse` replaced by `httpx2`
96+
97+
The SDK now depends on [`httpx2`](https://pypi.org/project/httpx2/) instead of
98+
`httpx` and `httpx-sse`. `httpx2` is the next-generation HTTP client (a fork of
99+
`httpx`) with server-sent events support built in, so the separate `httpx-sse`
100+
dependency is gone.
101+
102+
The swap itself does not change any SDK signatures - `streamable_http_client`
103+
and `sse_client` accept the same arguments as elsewhere in v2 - but the client
104+
type they expect is now `httpx2.AsyncClient`. If you construct your own client to pass as
105+
`http_client` (or build an `httpx2.Auth` subclass for `auth`), import from
106+
`httpx2`:
107+
108+
**Before (v1):**
109+
110+
```python
111+
import httpx
112+
113+
http_client = httpx.AsyncClient(follow_redirects=True)
114+
```
115+
116+
**After (v2):**
117+
118+
```python
119+
import httpx2
120+
121+
http_client = httpx2.AsyncClient(follow_redirects=True)
122+
```
123+
124+
`httpx2` is API-compatible with `httpx`, so usually only the import name
125+
changes. To consume SSE directly, use `httpx2.EventSource` (or
126+
`AsyncClient.sse()`) instead of the `httpx-sse` helpers.
127+
128+
TLS verification also changes: `httpx` validated certificates against the
129+
bundled `certifi` CA list, while `httpx2` validates against the operating
130+
system trust store via [`truststore`](https://pypi.org/project/truststore/).
131+
If your environment has no usable system CA store (some minimal containers),
132+
or you relied on certifi's bundle specifically, pass an explicit
133+
`verify=ssl_context` to your `httpx2.AsyncClient`.
134+
92135
### `mcp dev` and `mcp install` pin the spawned environment to your SDK version
93136

94137
Both commands run your server through a fresh `uv run --with ...` environment. In v1 the
@@ -111,7 +154,7 @@ unchanged. Only the `mcp.types` submodule and `mcp.shared.version` were removed.
111154
package's API reference is at [`mcp_types`](api/mcp_types/index.md).
112155

113156
**Why:** keeping the wire types in their own package lets tooling and lightweight clients
114-
depend on the protocol schema without pulling in `httpx`, `starlette`, `uvicorn`, and the
157+
depend on the protocol schema without pulling in `httpx2`, `starlette`, `uvicorn`, and the
115158
rest of the server/transport stack.
116159

117160
**Before (v1):**
@@ -1594,13 +1637,13 @@ async with streamablehttp_client(
15941637
**After (v2):**
15951638

15961639
```python
1597-
import httpx
1640+
import httpx2
15981641
from mcp.client.streamable_http import streamable_http_client
15991642

1600-
# Configure headers, timeout, and auth on the httpx.AsyncClient
1601-
http_client = httpx.AsyncClient(
1643+
# Configure headers, timeout, and auth on the httpx2.AsyncClient
1644+
http_client = httpx2.AsyncClient(
16021645
headers={"Authorization": "Bearer token"},
1603-
timeout=httpx.Timeout(30, read=300),
1646+
timeout=httpx2.Timeout(30, read=300),
16041647
auth=my_auth,
16051648
follow_redirects=True,
16061649
)
@@ -1613,15 +1656,15 @@ async with http_client:
16131656
...
16141657
```
16151658

1616-
v1's internal client set `follow_redirects=True`; set it explicitly when supplying your own `httpx.AsyncClient` to preserve that behavior.
1659+
v1's internal client set `follow_redirects=True`; set it explicitly when supplying your own `httpx2.AsyncClient` to preserve that behavior.
16171660

16181661
### `get_session_id` callback removed from `streamable_http_client`
16191662

16201663
The `get_session_id` callback (third element of the returned tuple) has been removed from `streamable_http_client`. The function now returns a 2-tuple `(read_stream, write_stream)` instead of a 3-tuple.
16211664

16221665
The `GetSessionIdCallback` type alias is gone as well, so `from mcp.client.streamable_http import GetSessionIdCallback` now raises `ImportError`. Drop the annotation, or inline `Callable[[], str | None]` if your own wrapper code still needs the type.
16231666

1624-
If you need to capture the session ID (e.g., for session resumption testing), you can use httpx event hooks to capture it from the response headers:
1667+
If you need to capture the session ID (e.g., for session resumption testing), you can use httpx2 event hooks to capture it from the response headers:
16251668

16261669
**Before (v1):**
16271670

@@ -1637,23 +1680,23 @@ async with streamable_http_client(url) as (read_stream, write_stream, get_sessio
16371680
**After (v2):**
16381681

16391682
```python
1640-
import httpx
1683+
import httpx2
16411684
from mcp.client.streamable_http import streamable_http_client
16421685

16431686
# Option 1: Simply ignore if you don't need the session ID
16441687
async with streamable_http_client(url) as (read_stream, write_stream):
16451688
async with ClientSession(read_stream, write_stream) as session:
16461689
await session.initialize()
16471690

1648-
# Option 2: Capture session ID via httpx event hooks if needed
1691+
# Option 2: Capture session ID via httpx2 event hooks if needed
16491692
captured_session_ids: list[str] = []
16501693

1651-
async def capture_session_id(response: httpx.Response) -> None:
1694+
async def capture_session_id(response: httpx2.Response) -> None:
16521695
session_id = response.headers.get("mcp-session-id")
16531696
if session_id:
16541697
captured_session_ids.append(session_id)
16551698

1656-
http_client = httpx.AsyncClient(
1699+
http_client = httpx2.AsyncClient(
16571700
event_hooks={"response": [capture_session_id]},
16581701
follow_redirects=True,
16591702
)
@@ -1667,7 +1710,7 @@ async with http_client:
16671710

16681711
### `StreamableHTTPTransport` parameters removed
16691712

1670-
The `headers`, `timeout`, `sse_read_timeout`, and `auth` parameters have been removed from `StreamableHTTPTransport`. Configure these on the `httpx.AsyncClient` instead (see example above).
1713+
The `headers`, `timeout`, `sse_read_timeout`, and `auth` parameters have been removed from `StreamableHTTPTransport`. Configure these on the `httpx2.AsyncClient` instead (see example above).
16711714

16721715
Note: `sse_client` retains its `headers`, `timeout`, `sse_read_timeout`, and `auth` parameters — only the streamable HTTP transport changed.
16731716

@@ -1730,7 +1773,7 @@ async with streamable_http_client(url) as (read, write):
17301773
raise
17311774
```
17321775

1733-
Move HTTP-status failure handling from around the transport context to around the individual calls, catching `MCPError` (see [`McpError` renamed to `MCPError`](#mcperror-renamed-to-mcperror)). Connect-level failures such as `httpx.ConnectError` still escape the transport context as before; keep context-level handling for those only.
1776+
Move HTTP-status failure handling from around the transport context to around the individual calls, catching `MCPError` (see [`McpError` renamed to `MCPError`](#mcperror-renamed-to-mcperror)). Connect-level failures such as `httpx2.ConnectError` still escape the transport context as before; keep context-level handling for those only.
17341777

17351778
### `terminate_windows_process` removed
17361779

0 commit comments

Comments
 (0)