From f9bde3af0f14e8fa12c485a8f6167fbac64fd7a8 Mon Sep 17 00:00:00 2001 From: Oliver Everett Date: Mon, 3 Aug 2026 10:30:34 +0800 Subject: [PATCH 1/2] Expose the httpx client factory as public API in mcp.client.streamable_http MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Customizing the streamable-HTTP client's headers/auth/timeout requires building an httpx2.AsyncClient, but the standardized factory (create_mcp_http_client), the McpHttpClientFactory protocol, and the default timeout constants only lived in the private mcp.shared._httpx_utils module. Re-export them from the public mcp.client.streamable_http module — where streamable_http_client itself lives — so building a custom client does not require importing a private module. Add a regression test asserting the public names resolve to the same objects as the private ones, and point the migration guide at the public import path. Refs #3238 --- docs/migration.md | 12 ++++++++++++ src/mcp/client/streamable_http.py | 24 +++++++++++++++++++++++- tests/shared/test_httpx_utils.py | 13 +++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/docs/migration.md b/docs/migration.md index b094d79f84..69d1dc8b71 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -2109,6 +2109,18 @@ v1's internal client set `follow_redirects=True`; set it explicitly when supplyi - `httpx_client_factory`: gone with no replacement — call your factory yourself and pass the result as `http_client`. - `terminate_on_close`: unchanged (default `True`). +If you'd rather not hand-build the `httpx2.AsyncClient`, the standardized factory and its defaults are re-exported from the public `mcp.client.streamable_http` module, so you don't need to touch the private `mcp.shared._httpx_utils`: + +```python +from mcp.client.streamable_http import create_mcp_http_client, streamable_http_client + +async with create_mcp_http_client(headers={"Authorization": "Bearer token"}) as http_client: + async with streamable_http_client(url="http://localhost:8000/mcp", http_client=http_client) as (read_stream, write_stream): + ... +``` + +`create_mcp_http_client(headers=..., timeout=..., auth=...)` applies the MCP defaults (`follow_redirects=True` and `Timeout(30, read=300)`) for you; the `McpHttpClientFactory` protocol and the `MCP_DEFAULT_TIMEOUT` / `MCP_DEFAULT_SSE_READ_TIMEOUT` constants are available from the same module. + Client-side stream resumption is also unchanged: the transport reconnects a dropped GET stream with `Last-Event-ID` on its own, and `session.send_request(..., metadata=ClientMessageMetadata(resumption_token=..., on_resumption_token_update=...))` (from `mcp.shared.message`) works as in v1. ### `get_session_id` callback removed from `streamable_http_client` diff --git a/src/mcp/client/streamable_http.py b/src/mcp/client/streamable_http.py index 226b0fecf9..ad4d43fc2d 100644 --- a/src/mcp/client/streamable_http.py +++ b/src/mcp/client/streamable_http.py @@ -33,11 +33,33 @@ from mcp.client._transport import TransportStreams from mcp.shared._compat import resync_tracer from mcp.shared._context_streams import ContextReceiveStream, ContextSendStream, create_context_streams -from mcp.shared._httpx_utils import create_mcp_http_client +from mcp.shared._httpx_utils import ( + MCP_DEFAULT_SSE_READ_TIMEOUT, + MCP_DEFAULT_TIMEOUT, + McpHttpClientFactory, + create_mcp_http_client, +) from mcp.shared.inbound import MCP_PROTOCOL_VERSION_HEADER from mcp.shared.jsonrpc_dispatcher import cancelled_request_id_from_params from mcp.shared.message import ClientMessageMetadata, SessionMessage +__all__ = [ + "LAST_EVENT_ID", + "MAX_RECONNECTION_ATTEMPTS", + "MCP_DEFAULT_SSE_READ_TIMEOUT", + "MCP_DEFAULT_TIMEOUT", + "MCP_SESSION_ID", + "McpHttpClientFactory", + "RequestContext", + "ResumptionError", + "StreamReader", + "StreamWriter", + "StreamableHTTPError", + "StreamableHTTPTransport", + "create_mcp_http_client", + "streamable_http_client", +] + logger = logging.getLogger(__name__) diff --git a/tests/shared/test_httpx_utils.py b/tests/shared/test_httpx_utils.py index a94d7c9299..4120fdc306 100644 --- a/tests/shared/test_httpx_utils.py +++ b/tests/shared/test_httpx_utils.py @@ -22,3 +22,16 @@ def test_custom_parameters(): assert client.headers["Authorization"] == "Bearer token" assert client.timeout.connect == 60.0 + + +def test_public_reexport_from_streamable_http(): + """The factory, protocol, and timeout defaults are importable from the + public ``mcp.client.streamable_http`` module, not only the private + ``mcp.shared._httpx_utils``.""" + import mcp.shared._httpx_utils as private_module + from mcp.client import streamable_http + + assert streamable_http.create_mcp_http_client is private_module.create_mcp_http_client + assert streamable_http.McpHttpClientFactory is private_module.McpHttpClientFactory + assert streamable_http.MCP_DEFAULT_TIMEOUT == private_module.MCP_DEFAULT_TIMEOUT + assert streamable_http.MCP_DEFAULT_SSE_READ_TIMEOUT == private_module.MCP_DEFAULT_SSE_READ_TIMEOUT From 5a4c1be5a52016b7204ebd22d30dd7cc40ace23a Mon Sep 17 00:00:00 2001 From: Oliver Everett Date: Mon, 3 Aug 2026 10:42:18 +0800 Subject: [PATCH 2/2] Avoid restricting the star-import surface; re-export via aliases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review feedback: the module-level __all__ added previously would have replaced Python's implicit star-import behavior and dropped previously exported names (SessionMessageOrError, DEFAULT_RECONNECTION_DELAY_MS, etc.) for any `from mcp.client.streamable_http import *` consumer. Drop __all__ and mark the re-exports with redundant aliases instead, which is additive only — the star-import surface is unchanged apart from the newly exported names. Refs #3238 --- src/mcp/client/streamable_http.py | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/src/mcp/client/streamable_http.py b/src/mcp/client/streamable_http.py index ad4d43fc2d..438af18310 100644 --- a/src/mcp/client/streamable_http.py +++ b/src/mcp/client/streamable_http.py @@ -33,33 +33,14 @@ from mcp.client._transport import TransportStreams from mcp.shared._compat import resync_tracer from mcp.shared._context_streams import ContextReceiveStream, ContextSendStream, create_context_streams -from mcp.shared._httpx_utils import ( - MCP_DEFAULT_SSE_READ_TIMEOUT, - MCP_DEFAULT_TIMEOUT, - McpHttpClientFactory, - create_mcp_http_client, -) +from mcp.shared._httpx_utils import MCP_DEFAULT_SSE_READ_TIMEOUT as MCP_DEFAULT_SSE_READ_TIMEOUT +from mcp.shared._httpx_utils import MCP_DEFAULT_TIMEOUT as MCP_DEFAULT_TIMEOUT +from mcp.shared._httpx_utils import McpHttpClientFactory as McpHttpClientFactory +from mcp.shared._httpx_utils import create_mcp_http_client from mcp.shared.inbound import MCP_PROTOCOL_VERSION_HEADER from mcp.shared.jsonrpc_dispatcher import cancelled_request_id_from_params from mcp.shared.message import ClientMessageMetadata, SessionMessage -__all__ = [ - "LAST_EVENT_ID", - "MAX_RECONNECTION_ATTEMPTS", - "MCP_DEFAULT_SSE_READ_TIMEOUT", - "MCP_DEFAULT_TIMEOUT", - "MCP_SESSION_ID", - "McpHttpClientFactory", - "RequestContext", - "ResumptionError", - "StreamReader", - "StreamWriter", - "StreamableHTTPError", - "StreamableHTTPTransport", - "create_mcp_http_client", - "streamable_http_client", -] - logger = logging.getLogger(__name__)