Skip to content

Commit c2aebfb

Browse files
Kludexmaxisbey
authored andcommitted
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 533c6a8 commit c2aebfb

64 files changed

Lines changed: 677 additions & 633 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.

‎docs/installation.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ You don't need to know any of this to use the SDK, but if you're wondering what
3535
* [`anyio`](https://anyio.readthedocs.io/): the async runtime. The whole SDK is written against anyio, so it runs on either `asyncio` or `trio`.
3636
* [`pydantic`](https://docs.pydantic.dev/): what every `mcp_types` model is built on, plus all schema generation and validation.
3737
* [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/): server configuration via `MCP_*` environment variables and `.env` files.
38-
* [`httpx`](https://www.python-httpx.org/) and [`httpx-sse`](https://pypi.org/project/httpx-sse/): the HTTP client behind the Streamable HTTP and SSE *client* transports.
38+
* [`httpx2`](https://pypi.org/project/httpx2/): the HTTP client behind the Streamable HTTP and SSE *client* transports, with server-sent events support built in.
3939
* [`starlette`](https://www.starlette.io/), [`uvicorn`](https://www.uvicorn.org/), [`sse-starlette`](https://pypi.org/project/sse-starlette/), and [`python-multipart`](https://pypi.org/project/python-multipart/): the HTTP *server* transports.
4040
* [`jsonschema`](https://pypi.org/project/jsonschema/): validates a tool's structured output against its declared output schema.
4141
* [`pyjwt[crypto]`](https://pyjwt.readthedocs.io/): OAuth token handling for authorization.

‎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 returns a `CallToolResult` (or an
@@ -56,13 +89,13 @@ async with streamablehttp_client(
5689
**After (v2):**
5790

5891
```python
59-
import httpx
92+
import httpx2
6093
from mcp.client.streamable_http import streamable_http_client
6194

62-
# Configure headers, timeout, and auth on the httpx.AsyncClient
63-
http_client = httpx.AsyncClient(
95+
# Configure headers, timeout, and auth on the httpx2.AsyncClient
96+
http_client = httpx2.AsyncClient(
6497
headers={"Authorization": "Bearer token"},
65-
timeout=httpx.Timeout(30, read=300),
98+
timeout=httpx2.Timeout(30, read=300),
6699
auth=my_auth,
67100
follow_redirects=True,
68101
)
@@ -75,7 +108,7 @@ async with http_client:
75108
...
76109
```
77110

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

80113
### OAuth `callback_handler` returns `AuthorizationCodeResult`
81114

@@ -110,7 +143,7 @@ Forward the `iss` query parameter from the redirect so the validation can run: o
110143

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

113-
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:
146+
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:
114147

115148
**Before (v1):**
116149

@@ -126,23 +159,23 @@ async with streamable_http_client(url) as (read_stream, write_stream, get_sessio
126159
**After (v2):**
127160

128161
```python
129-
import httpx
162+
import httpx2
130163
from mcp.client.streamable_http import streamable_http_client
131164

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

137-
# Option 2: Capture session ID via httpx event hooks if needed
170+
# Option 2: Capture session ID via httpx2 event hooks if needed
138171
captured_session_ids: list[str] = []
139172

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

145-
http_client = httpx.AsyncClient(
178+
http_client = httpx2.AsyncClient(
146179
event_hooks={"response": [capture_session_id]},
147180
follow_redirects=True,
148181
)
@@ -156,7 +189,7 @@ async with http_client:
156189

157190
### `StreamableHTTPTransport` parameters removed
158191

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

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

‎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",

‎examples/servers/simple-pagination/pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ classifiers = [
1414
"Programming Language :: Python :: 3",
1515
"Programming Language :: Python :: 3.10",
1616
]
17-
dependencies = ["anyio>=4.5", "click>=8.2.0", "httpx>=0.27", "mcp"]
17+
dependencies = ["anyio>=4.5", "click>=8.2.0", "httpx2>=2.5.0", "mcp"]
1818

1919
[project.scripts]
2020
mcp-simple-pagination = "mcp_simple_pagination.server:main"

0 commit comments

Comments
 (0)