Skip to content

Commit 74fa629

Browse files
Replace httpx_client_factory with httpx_client parameter
Modernize streamable_http_client API by accepting httpx.AsyncClient instances directly instead of factory functions, following industry standards. - New API: httpx_client: httpx.AsyncClient | None parameter - Default client created with recommended timeouts if None - Deprecated wrapper provides backward compatibility - Updated examples to show custom client usage - Add MCP_DEFAULT_TIMEOUT constants to _httpx_utils
1 parent f5c7a5e commit 74fa629

File tree

7 files changed

+206
-137
lines changed

7 files changed

+206
-137
lines changed

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2127,6 +2127,7 @@ from pydantic import AnyUrl
21272127
from mcp import ClientSession
21282128
from mcp.client.auth import OAuthClientProvider, TokenStorage
21292129
from mcp.client.streamable_http import streamable_http_client
2130+
from mcp.shared._httpx_utils import create_mcp_http_client
21302131
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
21312132

21322133

@@ -2180,15 +2181,16 @@ async def main():
21802181
callback_handler=handle_callback,
21812182
)
21822183

2183-
async with streamable_http_client("http://localhost:8001/mcp", auth=oauth_auth) as (read, write, _):
2184-
async with ClientSession(read, write) as session:
2185-
await session.initialize()
2184+
async with create_mcp_http_client(auth=oauth_auth) as custom_client:
2185+
async with streamable_http_client("http://localhost:8001/mcp", httpx_client=custom_client) as (read, write, _):
2186+
async with ClientSession(read, write) as session:
2187+
await session.initialize()
21862188

2187-
tools = await session.list_tools()
2188-
print(f"Available tools: {[tool.name for tool in tools.tools]}")
2189+
tools = await session.list_tools()
2190+
print(f"Available tools: {[tool.name for tool in tools.tools]}")
21892191

2190-
resources = await session.list_resources()
2191-
print(f"Available resources: {[r.uri for r in resources.resources]}")
2192+
resources = await session.list_resources()
2193+
print(f"Available resources: {[r.uri for r in resources.resources]}")
21922194

21932195

21942196
def run():

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

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@
1111
import threading
1212
import time
1313
import webbrowser
14-
from datetime import timedelta
1514
from http.server import BaseHTTPRequestHandler, HTTPServer
1615
from typing import Any
1716
from urllib.parse import parse_qs, urlparse
1817

18+
import httpx
19+
1920
from mcp.client.auth import OAuthClientProvider, TokenStorage
2021
from mcp.client.session import ClientSession
2122
from mcp.client.sse import sse_client
2223
from mcp.client.streamable_http import streamable_http_client
24+
from mcp.shared._httpx_utils import create_mcp_http_client
2325
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
2426

2527

@@ -188,9 +190,7 @@ async def _default_redirect_handler(authorization_url: str) -> None:
188190
# Create OAuth authentication handler using the new interface
189191
oauth_auth = OAuthClientProvider(
190192
server_url=self.server_url.replace("/mcp", ""),
191-
client_metadata=OAuthClientMetadata.model_validate(
192-
client_metadata_dict
193-
),
193+
client_metadata=OAuthClientMetadata.model_validate(client_metadata_dict),
194194
storage=InMemoryTokenStorage(),
195195
redirect_handler=_default_redirect_handler,
196196
callback_handler=callback_handler,
@@ -207,12 +207,12 @@ async def _default_redirect_handler(authorization_url: str) -> None:
207207
await self._run_session(read_stream, write_stream, None)
208208
else:
209209
print("📡 Opening StreamableHTTP transport connection with auth...")
210-
async with streamable_http_client(
211-
url=self.server_url,
212-
auth=oauth_auth,
213-
timeout=timedelta(seconds=60),
214-
) as (read_stream, write_stream, get_session_id):
215-
await self._run_session(read_stream, write_stream, get_session_id)
210+
async with create_mcp_http_client(auth=oauth_auth) as custom_client:
211+
async with streamable_http_client(
212+
url=self.server_url,
213+
httpx_client=custom_client,
214+
) as (read_stream, write_stream, get_session_id):
215+
await self._run_session(read_stream, write_stream, get_session_id)
216216

217217
except Exception as e:
218218
print(f"❌ Failed to connect: {e}")
@@ -322,9 +322,7 @@ async def interactive_loop(self):
322322
await self.call_tool(tool_name, arguments)
323323

324324
else:
325-
print(
326-
"❌ Unknown command. Try 'list', 'call <tool_name>', or 'quit'"
327-
)
325+
print("❌ Unknown command. Try 'list', 'call <tool_name>', or 'quit'")
328326

329327
except KeyboardInterrupt:
330328
print("\n\n👋 Goodbye!")

examples/snippets/clients/oauth_client.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from mcp import ClientSession
1616
from mcp.client.auth import OAuthClientProvider, TokenStorage
1717
from mcp.client.streamable_http import streamable_http_client
18+
from mcp.shared._httpx_utils import create_mcp_http_client
1819
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
1920

2021

@@ -68,15 +69,16 @@ async def main():
6869
callback_handler=handle_callback,
6970
)
7071

71-
async with streamable_http_client("http://localhost:8001/mcp", auth=oauth_auth) as (read, write, _):
72-
async with ClientSession(read, write) as session:
73-
await session.initialize()
72+
async with create_mcp_http_client(auth=oauth_auth) as custom_client:
73+
async with streamable_http_client("http://localhost:8001/mcp", httpx_client=custom_client) as (read, write, _):
74+
async with ClientSession(read, write) as session:
75+
await session.initialize()
7476

75-
tools = await session.list_tools()
76-
print(f"Available tools: {[tool.name for tool in tools.tools]}")
77+
tools = await session.list_tools()
78+
print(f"Available tools: {[tool.name for tool in tools.tools]}")
7779

78-
resources = await session.list_resources()
79-
print(f"Available resources: {[r.uri for r in resources.resources]}")
80+
resources = await session.list_resources()
81+
print(f"Available resources: {[r.uri for r in resources.resources]}")
8082

8183

8284
def run():

src/mcp/client/session_group.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from typing import Any, TypeAlias
1717

1818
import anyio
19+
import httpx
1920
from pydantic import BaseModel
2021
from typing_extensions import Self
2122

@@ -24,6 +25,7 @@
2425
from mcp.client.sse import sse_client
2526
from mcp.client.stdio import StdioServerParameters
2627
from mcp.client.streamable_http import streamable_http_client
28+
from mcp.shared._httpx_utils import create_mcp_http_client
2729
from mcp.shared.exceptions import McpError
2830

2931

@@ -250,11 +252,18 @@ async def _establish_session(
250252
)
251253
read, write = await session_stack.enter_async_context(client)
252254
else:
255+
httpx_client = create_mcp_http_client(
256+
headers=server_params.headers,
257+
timeout=httpx.Timeout(
258+
server_params.timeout.total_seconds(),
259+
read=server_params.sse_read_timeout.total_seconds(),
260+
),
261+
)
262+
await session_stack.enter_async_context(httpx_client)
263+
253264
client = streamable_http_client(
254265
url=server_params.url,
255-
headers=server_params.headers,
256-
timeout=server_params.timeout,
257-
sse_read_timeout=server_params.sse_read_timeout,
266+
httpx_client=httpx_client,
258267
terminate_on_close=server_params.terminate_on_close,
259268
)
260269
read, write, _ = await session_stack.enter_async_context(client)

src/mcp/client/streamable_http.py

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
and session management.
77
"""
88

9+
import contextlib
910
import logging
1011
from collections.abc import AsyncGenerator, Awaitable, Callable
1112
from contextlib import asynccontextmanager
@@ -19,7 +20,12 @@
1920
from httpx_sse import EventSource, ServerSentEvent, aconnect_sse
2021
from typing_extensions import deprecated
2122

22-
from mcp.shared._httpx_utils import McpHttpClientFactory, create_mcp_http_client
23+
from mcp.shared._httpx_utils import (
24+
MCP_DEFAULT_SSE_READ_TIMEOUT,
25+
MCP_DEFAULT_TIMEOUT,
26+
McpHttpClientFactory,
27+
create_mcp_http_client,
28+
)
2329
from mcp.shared.message import ClientMessageMetadata, SessionMessage
2430
from mcp.types import (
2531
ErrorData,
@@ -102,9 +108,9 @@ def __init__(
102108
self.session_id = None
103109
self.protocol_version = None
104110
self.request_headers = {
111+
**self.headers,
105112
ACCEPT: f"{JSON}, {SSE}",
106113
CONTENT_TYPE: JSON,
107-
**self.headers,
108114
}
109115

110116
def _prepare_request_headers(self, base_headers: dict[str, str]) -> dict[str, str]:
@@ -445,12 +451,9 @@ def get_session_id(self) -> str | None:
445451
@asynccontextmanager
446452
async def streamable_http_client(
447453
url: str,
448-
headers: dict[str, str] | None = None,
449-
timeout: float | timedelta = 30,
450-
sse_read_timeout: float | timedelta = 60 * 5,
454+
*,
455+
httpx_client: httpx.AsyncClient | None = None,
451456
terminate_on_close: bool = True,
452-
httpx_client_factory: McpHttpClientFactory = create_mcp_http_client,
453-
auth: httpx.Auth | None = None,
454457
) -> AsyncGenerator[
455458
tuple[
456459
MemoryObjectReceiveStream[SessionMessage | Exception],
@@ -462,30 +465,57 @@ async def streamable_http_client(
462465
"""
463466
Client transport for StreamableHTTP.
464467
465-
`sse_read_timeout` determines how long (in seconds) the client will wait for a new
466-
event before disconnecting. All other HTTP operations are controlled by `timeout`.
468+
Args:
469+
url: The MCP server endpoint URL.
470+
httpx_client: Optional pre-configured httpx.AsyncClient. If None, a default
471+
client with recommended MCP timeouts will be created. To configure headers,
472+
authentication, or other HTTP settings, create an httpx.AsyncClient and pass it here.
473+
terminate_on_close: If True, send a DELETE request to terminate the session
474+
when the context exits.
467475
468476
Yields:
469477
Tuple containing:
470478
- read_stream: Stream for reading messages from the server
471479
- write_stream: Stream for sending messages to the server
472480
- get_session_id_callback: Function to retrieve the current session ID
473-
"""
474-
transport = StreamableHTTPTransport(url, headers, timeout, sse_read_timeout, auth)
475481
482+
Example:
483+
See examples/snippets/clients/ for usage patterns.
484+
"""
476485
read_stream_writer, read_stream = anyio.create_memory_object_stream[SessionMessage | Exception](0)
477486
write_stream, write_stream_reader = anyio.create_memory_object_stream[SessionMessage](0)
478487

488+
# Determine if we need to create and manage the client
489+
client_provided = httpx_client is not None
490+
client = httpx_client
491+
492+
if client is None:
493+
# Create default client with recommended MCP timeouts
494+
client = create_mcp_http_client()
495+
496+
# Extract configuration from the client to pass to transport
497+
headers_dict = dict(client.headers) if client.headers else None
498+
timeout = client.timeout.connect if (client.timeout and client.timeout.connect is not None) else MCP_DEFAULT_TIMEOUT
499+
sse_read_timeout = (
500+
client.timeout.read if (client.timeout and client.timeout.read is not None) else MCP_DEFAULT_SSE_READ_TIMEOUT
501+
)
502+
auth = client.auth
503+
504+
# Create transport with extracted configuration
505+
transport = StreamableHTTPTransport(url, headers_dict, timeout, sse_read_timeout, auth)
506+
507+
# Sync client headers with transport's merged headers (includes MCP protocol requirements)
508+
client.headers.update(transport.request_headers)
509+
479510
async with anyio.create_task_group() as tg:
480511
try:
481512
logger.debug(f"Connecting to StreamableHTTP endpoint: {url}")
482513

483-
async with httpx_client_factory(
484-
headers=transport.request_headers,
485-
timeout=httpx.Timeout(transport.timeout, read=transport.sse_read_timeout),
486-
auth=transport.auth,
487-
) as client:
488-
# Define callbacks that need access to tg
514+
async with contextlib.AsyncExitStack() as stack:
515+
# Only manage client lifecycle if we created it
516+
if not client_provided:
517+
await stack.enter_async_context(client)
518+
489519
def start_get_stream() -> None:
490520
tg.start_soon(transport.handle_get_stream, client, read_stream_writer)
491521

@@ -532,7 +562,24 @@ async def streamablehttp_client(
532562
],
533563
None,
534564
]:
535-
async with streamable_http_client(
536-
url, headers, timeout, sse_read_timeout, terminate_on_close, httpx_client_factory, auth
537-
) as streams:
538-
yield streams
565+
# Convert timeout parameters
566+
timeout_seconds = timeout.total_seconds() if isinstance(timeout, timedelta) else timeout
567+
sse_read_timeout_seconds = (
568+
sse_read_timeout.total_seconds() if isinstance(sse_read_timeout, timedelta) else sse_read_timeout
569+
)
570+
571+
# Create httpx client using the factory with old-style parameters
572+
client = httpx_client_factory(
573+
headers=headers,
574+
timeout=httpx.Timeout(timeout_seconds, read=sse_read_timeout_seconds),
575+
auth=auth,
576+
)
577+
578+
# Manage client lifecycle since we created it
579+
async with client:
580+
async with streamable_http_client(
581+
url,
582+
httpx_client=client,
583+
terminate_on_close=terminate_on_close,
584+
) as streams:
585+
yield streams

src/mcp/shared/_httpx_utils.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44

55
import httpx
66

7-
__all__ = ["create_mcp_http_client"]
7+
__all__ = ["create_mcp_http_client", "MCP_DEFAULT_TIMEOUT", "MCP_DEFAULT_SSE_READ_TIMEOUT"]
8+
9+
# Default MCP timeout configuration
10+
MCP_DEFAULT_TIMEOUT = 30.0 # General operations (seconds)
11+
MCP_DEFAULT_SSE_READ_TIMEOUT = 300.0 # SSE streams - 5 minutes (seconds)
812

913

1014
class McpHttpClientFactory(Protocol):
@@ -68,7 +72,7 @@ def create_mcp_http_client(
6872

6973
# Handle timeout
7074
if timeout is None:
71-
kwargs["timeout"] = httpx.Timeout(30.0)
75+
kwargs["timeout"] = httpx.Timeout(MCP_DEFAULT_TIMEOUT, read=MCP_DEFAULT_SSE_READ_TIMEOUT)
7276
else:
7377
kwargs["timeout"] = timeout
7478

0 commit comments

Comments
 (0)