Skip to content

Commit 86e257f

Browse files
committed
Replace httpx and httpx-sse with httpx2
httpx2 (2.5.0) is the next-generation httpx fork with server-sent events support built in, so the separate httpx-sse dependency is no longer needed. - Swap the httpx/httpx-sse dependencies for httpx2>=2.5.0 in the SDK and the example projects. - Rewrite the SSE transports against httpx2's API: aconnect_sse(...) -> client.stream(...)/client.sse(...) wrapped in EventSource, and iterate the EventSource directly instead of .aiter_sse(). - Document the swap as a v2 breaking change in docs/migration.md and update docs/installation.md, README.v2.md, and the example sources. Verified: ruff, pyright, and the full test suite pass at 100% coverage.
1 parent a527142 commit 86e257f

65 files changed

Lines changed: 674 additions & 631 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.v2.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2319,7 +2319,7 @@ cd to the `examples/snippets` directory and run:
23192319
import asyncio
23202320
from urllib.parse import parse_qs, urlparse
23212321

2322-
import httpx
2322+
import httpx2
23232323
from pydantic import AnyUrl
23242324

23252325
from mcp import ClientSession
@@ -2382,7 +2382,7 @@ async def main():
23822382
callback_handler=handle_callback,
23832383
)
23842384

2385-
async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client:
2385+
async with httpx2.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client:
23862386
async with streamable_http_client("http://localhost:8001/mcp", http_client=custom_client) as (read, write):
23872387
async with ClientSession(read, write) as session:
23882388
await session.initialize()

docs/installation.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ The Python SDK is available on PyPI as [`mcp`](https://pypi.org/project/mcp/) so
1515

1616
The following dependencies are automatically installed:
1717

18-
- [`httpx`](https://pypi.org/project/httpx/): HTTP client to handle HTTP Streamable and SSE transports.
19-
- [`httpx-sse`](https://pypi.org/project/httpx-sse/): HTTP client to handle SSE transport.
18+
- [`httpx2`](https://pypi.org/project/httpx2/): HTTP client to handle HTTP Streamable and SSE transports.
2019
- [`pydantic`](https://pypi.org/project/pydantic/): Types, JSON schema generation, data validation, and [more](https://docs.pydantic.dev/latest/).
2120
- [`starlette`](https://pypi.org/project/starlette/): Web framework used to build the HTTP transport endpoints.
2221
- [`python-multipart`](https://pypi.org/project/python-multipart/): Handle HTTP body parsing.

docs/migration.md

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,39 @@ Version 2 of the MCP Python SDK introduces several breaking changes to improve t
88

99
## Breaking Changes
1010

11+
### `httpx` replaced by `httpx2`
12+
13+
The SDK now depends on [`httpx2`](https://pypi.org/project/httpx2/) instead of
14+
`httpx` and `httpx-sse`. `httpx2` is the next-generation HTTP client (a fork of
15+
`httpx`) with server-sent events support built in, so the separate `httpx-sse`
16+
dependency is gone.
17+
18+
The public API surface is unchanged in shape - `streamable_http_client` and
19+
`sse_client` still accept the same arguments - but the client type they expect
20+
is now `httpx2.AsyncClient`. If you construct your own client to pass as
21+
`http_client` (or build an `httpx.Auth` subclass for `auth`), import from
22+
`httpx2`:
23+
24+
**Before (v1):**
25+
26+
```python
27+
import httpx
28+
29+
http_client = httpx.AsyncClient(follow_redirects=True)
30+
```
31+
32+
**After (v2):**
33+
34+
```python
35+
import httpx2
36+
37+
http_client = httpx2.AsyncClient(follow_redirects=True)
38+
```
39+
40+
`httpx2` is API-compatible with `httpx`, so usually only the import name
41+
changes. To consume SSE directly, use `httpx2.EventSource` (or
42+
`AsyncClient.sse()`) instead of the `httpx-sse` helpers.
43+
1144
### `MCPServer.call_tool()` returns `CallToolResult`
1245

1346
`MCPServer.call_tool()` now always returns a `CallToolResult`. It previously
@@ -55,13 +88,13 @@ async with streamablehttp_client(
5588
**After (v2):**
5689

5790
```python
58-
import httpx
91+
import httpx2
5992
from mcp.client.streamable_http import streamable_http_client
6093

61-
# Configure headers, timeout, and auth on the httpx.AsyncClient
62-
http_client = httpx.AsyncClient(
94+
# Configure headers, timeout, and auth on the httpx2.AsyncClient
95+
http_client = httpx2.AsyncClient(
6396
headers={"Authorization": "Bearer token"},
64-
timeout=httpx.Timeout(30, read=300),
97+
timeout=httpx2.Timeout(30, read=300),
6598
auth=my_auth,
6699
follow_redirects=True,
67100
)
@@ -74,7 +107,7 @@ async with http_client:
74107
...
75108
```
76109

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

79112
### OAuth `callback_handler` returns `AuthorizationCodeResult`
80113

@@ -109,7 +142,7 @@ Forward the `iss` query parameter from the redirect so the validation can run: o
109142

110143
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.
111144

112-
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:
145+
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:
113146

114147
**Before (v1):**
115148

@@ -125,23 +158,23 @@ async with streamable_http_client(url) as (read_stream, write_stream, get_sessio
125158
**After (v2):**
126159

127160
```python
128-
import httpx
161+
import httpx2
129162
from mcp.client.streamable_http import streamable_http_client
130163

131164
# Option 1: Simply ignore if you don't need the session ID
132165
async with streamable_http_client(url) as (read_stream, write_stream):
133166
async with ClientSession(read_stream, write_stream) as session:
134167
await session.initialize()
135168

136-
# Option 2: Capture session ID via httpx event hooks if needed
169+
# Option 2: Capture session ID via httpx2 event hooks if needed
137170
captured_session_ids: list[str] = []
138171

139-
async def capture_session_id(response: httpx.Response) -> None:
172+
async def capture_session_id(response: httpx2.Response) -> None:
140173
session_id = response.headers.get("mcp-session-id")
141174
if session_id:
142175
captured_session_ids.append(session_id)
143176

144-
http_client = httpx.AsyncClient(
177+
http_client = httpx2.AsyncClient(
145178
event_hooks={"response": [capture_session_id]},
146179
follow_redirects=True,
147180
)
@@ -155,7 +188,7 @@ async with http_client:
155188

156189
### `StreamableHTTPTransport` parameters removed
157190

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

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

examples/clients/simple-auth-client/mcp_simple_auth_client/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from typing import Any
1818
from urllib.parse import parse_qs, urlparse
1919

20-
import httpx
20+
import httpx2
2121
from mcp.client._transport import ReadStream, WriteStream
2222
from mcp.client.auth import AuthorizationCodeResult, OAuthClientProvider, TokenStorage
2323
from mcp.client.session import ClientSession
@@ -233,7 +233,7 @@ async def _default_redirect_handler(authorization_url: str) -> None:
233233
await self._run_session(read_stream, write_stream)
234234
else:
235235
print("📡 Opening StreamableHTTP transport connection with auth...")
236-
async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client:
236+
async with httpx2.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client:
237237
async with streamable_http_client(url=self.server_url, http_client=custom_client) as (
238238
read_stream,
239239
write_stream,

examples/clients/simple-chatbot/mcp_simple_chatbot/main.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from contextlib import AsyncExitStack
99
from typing import Any
1010

11-
import httpx
11+
import httpx2
1212
from dotenv import load_dotenv
1313
from mcp import ClientSession, StdioServerParameters
1414
from mcp.client.stdio import stdio_client
@@ -230,7 +230,7 @@ def get_response(self, messages: list[dict[str, str]]) -> str:
230230
The LLM's response as a string.
231231
232232
Raises:
233-
httpx.RequestError: If the request to the LLM fails.
233+
httpx2.RequestError: If the request to the LLM fails.
234234
"""
235235
url = "https://api.groq.com/openai/v1/chat/completions"
236236

@@ -249,17 +249,17 @@ def get_response(self, messages: list[dict[str, str]]) -> str:
249249
}
250250

251251
try:
252-
with httpx.Client() as client:
252+
with httpx2.Client() as client:
253253
response = client.post(url, headers=headers, json=payload)
254254
response.raise_for_status()
255255
data = response.json()
256256
return data["choices"][0]["message"]["content"]
257257

258-
except httpx.RequestError as e:
258+
except httpx2.RequestError as e:
259259
error_message = f"Error getting LLM response: {str(e)}"
260260
logging.error(error_message)
261261

262-
if isinstance(e, httpx.HTTPStatusError):
262+
if isinstance(e, httpx2.HTTPStatusError):
263263
status_code = e.response.status_code
264264
logging.error(f"Status code: {status_code}")
265265
logging.error(f"Response details: {e.response.text}")

examples/clients/sse-polling-client/mcp_sse_polling_client/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def main(url: str, items: int, checkpoint_every: int, log_level: str) -> None:
9292
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
9393
)
9494
# Suppress noisy HTTP client logging
95-
logging.getLogger("httpx").setLevel(logging.WARNING)
95+
logging.getLogger("httpx2").setLevel(logging.WARNING)
9696
logging.getLogger("httpcore").setLevel(logging.WARNING)
9797

9898
asyncio.run(run_demo(url, items, checkpoint_every))

examples/mcpserver/text_me.py

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

2020
from typing import Annotated
2121

22-
import httpx
22+
import httpx2
2323
from pydantic import BeforeValidator
2424
from pydantic_settings import BaseSettings, SettingsConfigDict
2525

@@ -44,7 +44,7 @@ class SurgeSettings(BaseSettings):
4444
@mcp.tool(name="textme", description="Send a text message to me")
4545
def text_me(text_content: str) -> str:
4646
"""Send a text message to a phone number via https://surgemsg.com/"""
47-
with httpx.Client() as client:
47+
with httpx2.Client() as client:
4848
response = client.post(
4949
"https://api.surgemsg.com/messages",
5050
headers={

examples/servers/everything-server/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ requires-python = ">=3.10"
77
authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
88
keywords = ["mcp", "llm", "automation", "conformance", "testing"]
99
license = { text = "MIT" }
10-
dependencies = ["anyio>=4.5", "click>=8.2.0", "httpx>=0.27", "mcp", "starlette", "uvicorn"]
10+
dependencies = ["anyio>=4.5", "click>=8.2.0", "httpx2>=2.5.0", "mcp", "starlette", "uvicorn"]
1111

1212
[project.scripts]
1313
mcp-everything-server = "mcp_everything_server.server:main"

examples/servers/simple-auth/mcp_simple_auth/token_verifier.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,18 @@ def __init__(
3333

3434
async def verify_token(self, token: str) -> AccessToken | None:
3535
"""Verify token via introspection endpoint."""
36-
import httpx
36+
import httpx2
3737

3838
# Validate URL to prevent SSRF attacks
3939
if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
4040
logger.warning(f"Rejecting introspection endpoint with unsafe scheme: {self.introspection_endpoint}")
4141
return None
4242

4343
# Configure secure HTTP client
44-
timeout = httpx.Timeout(10.0, connect=5.0)
45-
limits = httpx.Limits(max_connections=10, max_keepalive_connections=5)
44+
timeout = httpx2.Timeout(10.0, connect=5.0)
45+
limits = httpx2.Limits(max_connections=10, max_keepalive_connections=5)
4646

47-
async with httpx.AsyncClient(
47+
async with httpx2.AsyncClient(
4848
timeout=timeout,
4949
limits=limits,
5050
verify=True, # Enforce SSL verification

examples/servers/simple-auth/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ license = { text = "MIT" }
99
dependencies = [
1010
"anyio>=4.5",
1111
"click>=8.2.0",
12-
"httpx>=0.27",
12+
"httpx2>=2.5.0",
1313
"mcp",
1414
"pydantic>=2.0",
1515
"pydantic-settings>=2.5.2",

0 commit comments

Comments
 (0)