Skip to content

Commit fe8afee

Browse files
dsfacciniclaude
andcommitted
Deprecate httpx in favor of httpx2
Mirrors Kludex/starlette@508023b and pydantic/pydantic-ai#5664: prefer `httpx2` at import time and fall back to `httpx` with an `MCPDeprecationWarning` emitted lazily on first use of an HTTP-touching surface (`create_mcp_http_client`, `OAuthClientProvider`, `HttpResource`). The v2-cut PR will drop the fallback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3eb5799 commit fe8afee

11 files changed

Lines changed: 176 additions & 19 deletions

File tree

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,11 @@ filterwarnings = [
206206
"ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning",
207207
# pywin32 internal deprecation warning
208208
"ignore:getargs.*The 'u' format is deprecated:DeprecationWarning",
209+
# `mcp` prefers `httpx2`; HTTP surfaces warn when falling back to `httpx`. `httpx2` is
210+
# not yet on PyPI, so every HTTP-touching test would trip the warning. The dedicated
211+
# test in `tests/shared/test_httpx_shim.py` covers emission. Remove this entry once
212+
# `httpx2` is the default and the fallback is dropped.
213+
"ignore:Using `httpx` with `mcp` is deprecated:mcp.shared._httpx.MCPDeprecationWarning",
209214
]
210215

211216
[tool.markdown.lint]

src/mcp/client/auth/extensions/client_credentials.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
from typing import Any, Literal
1414
from uuid import uuid4
1515

16-
import httpx
1716
import jwt
1817
from pydantic import BaseModel, Field
1918

2019
from mcp.client.auth import OAuthClientProvider, OAuthFlowError, OAuthTokenError, TokenStorage
20+
from mcp.shared._httpx import httpx
2121
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata
2222

2323

src/mcp/client/auth/oauth2.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
from urllib.parse import quote, urlencode, urljoin, urlparse
1616

1717
import anyio
18-
import httpx
1918
from pydantic import BaseModel, Field, ValidationError
2019

2120
from mcp.client.auth.exceptions import OAuthFlowError, OAuthTokenError
@@ -37,6 +36,7 @@
3736
should_use_client_metadata_url,
3837
)
3938
from mcp.client.streamable_http import MCP_PROTOCOL_VERSION
39+
from mcp.shared._httpx import emit_httpx_deprecation_warning, httpx
4040
from mcp.shared.auth import (
4141
OAuthClientInformationFull,
4242
OAuthClientMetadata,
@@ -255,6 +255,8 @@ def __init__(
255255
ValueError: If client_metadata_url is provided but not a valid HTTPS URL
256256
with a non-root pathname.
257257
"""
258+
emit_httpx_deprecation_warning()
259+
258260
# Validate client_metadata_url if provided
259261
if client_metadata_url is not None and not is_valid_client_metadata_url(client_metadata_url):
260262
raise ValueError(

src/mcp/client/auth/utils.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import re
22
from urllib.parse import urljoin, urlparse
33

4-
from httpx import Request, Response
54
from pydantic import AnyUrl, ValidationError
65

76
from mcp.client.auth import OAuthRegistrationError, OAuthTokenError
87
from mcp.client.streamable_http import MCP_PROTOCOL_VERSION
8+
from mcp.shared._httpx import httpx
99
from mcp.shared.auth import (
1010
OAuthClientInformationFull,
1111
OAuthClientMetadata,
@@ -16,7 +16,7 @@
1616
from mcp.types import LATEST_PROTOCOL_VERSION
1717

1818

19-
def extract_field_from_www_auth(response: Response, field_name: str) -> str | None:
19+
def extract_field_from_www_auth(response: httpx.Response, field_name: str) -> str | None:
2020
"""Extract field from WWW-Authenticate header.
2121
2222
Returns:
@@ -37,7 +37,7 @@ def extract_field_from_www_auth(response: Response, field_name: str) -> str | No
3737
return None
3838

3939

40-
def extract_scope_from_www_auth(response: Response) -> str | None:
40+
def extract_scope_from_www_auth(response: httpx.Response) -> str | None:
4141
"""Extract scope parameter from WWW-Authenticate header as per RFC 6750.
4242
4343
Returns:
@@ -46,7 +46,7 @@ def extract_scope_from_www_auth(response: Response) -> str | None:
4646
return extract_field_from_www_auth(response, "scope")
4747

4848

49-
def extract_resource_metadata_from_www_auth(response: Response) -> str | None:
49+
def extract_resource_metadata_from_www_auth(response: httpx.Response) -> str | None:
5050
"""Extract protected resource metadata URL from WWW-Authenticate header as per RFC 9728.
5151
5252
Returns:
@@ -175,7 +175,7 @@ def build_oauth_authorization_server_metadata_discovery_urls(auth_server_url: st
175175

176176

177177
async def handle_protected_resource_response(
178-
response: Response,
178+
response: httpx.Response,
179179
) -> ProtectedResourceMetadata | None:
180180
"""Handle protected resource metadata discovery response.
181181
@@ -198,7 +198,7 @@ async def handle_protected_resource_response(
198198
return None
199199

200200

201-
async def handle_auth_metadata_response(response: Response) -> tuple[bool, OAuthMetadata | None]:
201+
async def handle_auth_metadata_response(response: httpx.Response) -> tuple[bool, OAuthMetadata | None]:
202202
if response.status_code == 200:
203203
try:
204204
content = await response.aread()
@@ -211,13 +211,13 @@ async def handle_auth_metadata_response(response: Response) -> tuple[bool, OAuth
211211
return True, None
212212

213213

214-
def create_oauth_metadata_request(url: str) -> Request:
215-
return Request("GET", url, headers={MCP_PROTOCOL_VERSION: LATEST_PROTOCOL_VERSION})
214+
def create_oauth_metadata_request(url: str) -> httpx.Request:
215+
return httpx.Request("GET", url, headers={MCP_PROTOCOL_VERSION: LATEST_PROTOCOL_VERSION})
216216

217217

218218
def create_client_registration_request(
219219
auth_server_metadata: OAuthMetadata | None, client_metadata: OAuthClientMetadata, auth_base_url: str
220-
) -> Request:
220+
) -> httpx.Request:
221221
"""Build a client registration request."""
222222

223223
if auth_server_metadata and auth_server_metadata.registration_endpoint:
@@ -227,10 +227,10 @@ def create_client_registration_request(
227227

228228
registration_data = client_metadata.model_dump(by_alias=True, mode="json", exclude_none=True)
229229

230-
return Request("POST", registration_url, json=registration_data, headers={"Content-Type": "application/json"})
230+
return httpx.Request("POST", registration_url, json=registration_data, headers={"Content-Type": "application/json"})
231231

232232

233-
async def handle_registration_response(response: Response) -> OAuthClientInformationFull:
233+
async def handle_registration_response(response: httpx.Response) -> OAuthClientInformationFull:
234234
"""Handle registration response."""
235235
if response.status_code not in (200, 201):
236236
await response.aread()
@@ -316,7 +316,7 @@ def create_client_info_from_metadata_url(
316316

317317

318318
async def handle_token_response_scopes(
319-
response: Response,
319+
response: httpx.Response,
320320
) -> OAuthToken:
321321
"""Parse and validate a token response.
322322

src/mcp/client/session_group.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from typing import Any, TypeAlias
1515

1616
import anyio
17-
import httpx
1817
from pydantic import BaseModel, Field
1918
from typing_extensions import Self
2019

@@ -24,6 +23,7 @@
2423
from mcp.client.sse import sse_client
2524
from mcp.client.stdio import StdioServerParameters
2625
from mcp.client.streamable_http import streamable_http_client
26+
from mcp.shared._httpx import httpx
2727
from mcp.shared._httpx_utils import create_mcp_http_client
2828
from mcp.shared.exceptions import MCPError
2929
from mcp.shared.session import ProgressFnT

src/mcp/client/sse.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
from urllib.parse import parse_qs, urljoin, urlparse
66

77
import anyio
8-
import httpx
98
from anyio.abc import TaskStatus
109
from httpx_sse import SSEError, aconnect_sse
1110

1211
from mcp import types
1312
from mcp.shared._context_streams import create_context_streams
13+
from mcp.shared._httpx import httpx
1414
from mcp.shared._httpx_utils import McpHttpClientFactory, create_mcp_http_client
1515
from mcp.shared.message import SessionMessage
1616

src/mcp/client/streamable_http.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
from dataclasses import dataclass
1010

1111
import anyio
12-
import httpx
1312
from anyio.abc import TaskGroup
1413
from httpx_sse import EventSource, ServerSentEvent, aconnect_sse
1514
from pydantic import ValidationError
1615

1716
from mcp.client._transport import TransportStreams
1817
from mcp.shared._context_streams import ContextReceiveStream, ContextSendStream, create_context_streams
18+
from mcp.shared._httpx import httpx
1919
from mcp.shared._httpx_utils import create_mcp_http_client
2020
from mcp.shared.message import ClientMessageMetadata, SessionMessage
2121
from mcp.types import (

src/mcp/server/mcpserver/resources/types.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99

1010
import anyio
1111
import anyio.to_thread
12-
import httpx
1312
import pydantic
1413
import pydantic_core
1514
from pydantic import Field, ValidationInfo, validate_call
1615

1716
from mcp.server.mcpserver.resources.base import Resource
1817
from mcp.shared._callable_inspection import is_async_callable
18+
from mcp.shared._httpx import emit_httpx_deprecation_warning, httpx
1919
from mcp.types import Annotations, Icon
2020

2121

@@ -159,6 +159,7 @@ class HttpResource(Resource):
159159

160160
async def read(self) -> str | bytes:
161161
"""Read the HTTP content."""
162+
emit_httpx_deprecation_warning() # pragma: no cover
162163
async with httpx.AsyncClient() as client: # pragma: no cover
163164
response = await client.get(self.url)
164165
response.raise_for_status()

src/mcp/shared/_httpx.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Compatibility shim: prefer `httpx2`, fall back to `httpx` with a deprecation warning.
2+
3+
Mirrors the pattern from
4+
[Kludex/starlette@508023b](https://github.com/Kludex/starlette/commit/508023b488b649d97c091eb60da1d8ef3636ee06)
5+
and [pydantic/pydantic-ai#5664](https://github.com/pydantic/pydantic-ai/pull/5664).
6+
7+
`httpx2` is not yet on PyPI, so every install today exercises the fallback path. The warning
8+
is emitted lazily on first use (not at module import) to avoid breaking pytest's filter
9+
parser during collection. The MCP v2 cut will drop the fallback and require `httpx2`.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import warnings
15+
from typing import TYPE_CHECKING
16+
17+
__all__ = ["MCPDeprecationWarning", "emit_httpx_deprecation_warning", "httpx"]
18+
19+
20+
class MCPDeprecationWarning(UserWarning):
21+
"""Deprecation warning emitted by the `mcp` package.
22+
23+
Subclasses `UserWarning` (not `DeprecationWarning`) so it is visible by default —
24+
`DeprecationWarning` is silenced at the Python level for non-`__main__` callers.
25+
"""
26+
27+
28+
if TYPE_CHECKING:
29+
import httpx as httpx
30+
31+
_HTTPX_IS_DEPRECATED = False
32+
else:
33+
try:
34+
import httpx2 as httpx
35+
36+
_HTTPX_IS_DEPRECATED = False
37+
except ImportError:
38+
import httpx
39+
40+
_HTTPX_IS_DEPRECATED = True
41+
42+
43+
_warning_emitted = False
44+
45+
46+
def emit_httpx_deprecation_warning() -> None:
47+
"""Emit the `httpx` → `httpx2` deprecation warning at most once per process."""
48+
global _warning_emitted
49+
if _HTTPX_IS_DEPRECATED and not _warning_emitted:
50+
_warning_emitted = True
51+
warnings.warn(
52+
"Using `httpx` with `mcp` is deprecated; install `httpx2` instead.",
53+
MCPDeprecationWarning,
54+
stacklevel=3,
55+
)

src/mcp/shared/_httpx_utils.py

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

33
from typing import Any, Protocol
44

5-
import httpx
5+
from mcp.shared._httpx import emit_httpx_deprecation_warning, httpx
66

77
__all__ = ["create_mcp_http_client", "MCP_DEFAULT_TIMEOUT", "MCP_DEFAULT_SSE_READ_TIMEOUT"]
88

@@ -77,6 +77,8 @@ def create_mcp_http_client(
7777
response = await client.get("/protected-endpoint")
7878
```
7979
"""
80+
emit_httpx_deprecation_warning()
81+
8082
# Set MCP defaults
8183
kwargs: dict[str, Any] = {"follow_redirects": True}
8284

0 commit comments

Comments
 (0)