66and session management.
77"""
88
9+ import contextlib
910import logging
1011from collections .abc import AsyncGenerator , Awaitable , Callable
1112from contextlib import asynccontextmanager
1920from httpx_sse import EventSource , ServerSentEvent , aconnect_sse
2021from 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+ )
2329from mcp .shared .message import ClientMessageMetadata , SessionMessage
2430from mcp .types import (
2531 ErrorData ,
@@ -106,9 +112,9 @@ def __init__(
106112 self .session_id = None
107113 self .protocol_version = None
108114 self .request_headers = {
115+ ** self .headers ,
109116 ACCEPT : f"{ JSON } , { SSE } " ,
110117 CONTENT_TYPE : JSON ,
111- ** self .headers ,
112118 }
113119
114120 def _prepare_request_headers (self , base_headers : dict [str , str ]) -> dict [str , str ]:
@@ -563,12 +569,9 @@ def get_session_id(self) -> str | None:
563569@asynccontextmanager
564570async def streamable_http_client (
565571 url : str ,
566- headers : dict [str , str ] | None = None ,
567- timeout : float | timedelta = 30 ,
568- sse_read_timeout : float | timedelta = 60 * 5 ,
572+ * ,
573+ httpx_client : httpx .AsyncClient | None = None ,
569574 terminate_on_close : bool = True ,
570- httpx_client_factory : McpHttpClientFactory = create_mcp_http_client ,
571- auth : httpx .Auth | None = None ,
572575) -> AsyncGenerator [
573576 tuple [
574577 MemoryObjectReceiveStream [SessionMessage | Exception ],
@@ -580,30 +583,57 @@ async def streamable_http_client(
580583 """
581584 Client transport for StreamableHTTP.
582585
583- `sse_read_timeout` determines how long (in seconds) the client will wait for a new
584- event before disconnecting. All other HTTP operations are controlled by `timeout`.
586+ Args:
587+ url: The MCP server endpoint URL.
588+ httpx_client: Optional pre-configured httpx.AsyncClient. If None, a default
589+ client with recommended MCP timeouts will be created. To configure headers,
590+ authentication, or other HTTP settings, create an httpx.AsyncClient and pass it here.
591+ terminate_on_close: If True, send a DELETE request to terminate the session
592+ when the context exits.
585593
586594 Yields:
587595 Tuple containing:
588596 - read_stream: Stream for reading messages from the server
589597 - write_stream: Stream for sending messages to the server
590598 - get_session_id_callback: Function to retrieve the current session ID
591- """
592- transport = StreamableHTTPTransport (url , headers , timeout , sse_read_timeout , auth )
593599
600+ Example:
601+ See examples/snippets/clients/ for usage patterns.
602+ """
594603 read_stream_writer , read_stream = anyio .create_memory_object_stream [SessionMessage | Exception ](0 )
595604 write_stream , write_stream_reader = anyio .create_memory_object_stream [SessionMessage ](0 )
596605
606+ # Determine if we need to create and manage the client
607+ client_provided = httpx_client is not None
608+ client = httpx_client
609+
610+ if client is None :
611+ # Create default client with recommended MCP timeouts
612+ client = create_mcp_http_client ()
613+
614+ # Extract configuration from the client to pass to transport
615+ headers_dict = dict (client .headers ) if client .headers else None
616+ timeout = client .timeout .connect if (client .timeout and client .timeout .connect is not None ) else MCP_DEFAULT_TIMEOUT
617+ sse_read_timeout = (
618+ client .timeout .read if (client .timeout and client .timeout .read is not None ) else MCP_DEFAULT_SSE_READ_TIMEOUT
619+ )
620+ auth = client .auth
621+
622+ # Create transport with extracted configuration
623+ transport = StreamableHTTPTransport (url , headers_dict , timeout , sse_read_timeout , auth )
624+
625+ # Sync client headers with transport's merged headers (includes MCP protocol requirements)
626+ client .headers .update (transport .request_headers )
627+
597628 async with anyio .create_task_group () as tg :
598629 try :
599630 logger .debug (f"Connecting to StreamableHTTP endpoint: { url } " )
600631
601- async with httpx_client_factory (
602- headers = transport .request_headers ,
603- timeout = httpx .Timeout (transport .timeout , read = transport .sse_read_timeout ),
604- auth = transport .auth ,
605- ) as client :
606- # Define callbacks that need access to tg
632+ async with contextlib .AsyncExitStack () as stack :
633+ # Only manage client lifecycle if we created it
634+ if not client_provided :
635+ await stack .enter_async_context (client )
636+
607637 def start_get_stream () -> None :
608638 tg .start_soon (transport .handle_get_stream , client , read_stream_writer )
609639
@@ -650,7 +680,24 @@ async def streamablehttp_client(
650680 ],
651681 None ,
652682]:
653- async with streamable_http_client (
654- url , headers , timeout , sse_read_timeout , terminate_on_close , httpx_client_factory , auth
655- ) as streams :
656- yield streams
683+ # Convert timeout parameters
684+ timeout_seconds = timeout .total_seconds () if isinstance (timeout , timedelta ) else timeout
685+ sse_read_timeout_seconds = (
686+ sse_read_timeout .total_seconds () if isinstance (sse_read_timeout , timedelta ) else sse_read_timeout
687+ )
688+
689+ # Create httpx client using the factory with old-style parameters
690+ client = httpx_client_factory (
691+ headers = headers ,
692+ timeout = httpx .Timeout (timeout_seconds , read = sse_read_timeout_seconds ),
693+ auth = auth ,
694+ )
695+
696+ # Manage client lifecycle since we created it
697+ async with client :
698+ async with streamable_http_client (
699+ url ,
700+ httpx_client = client ,
701+ terminate_on_close = terminate_on_close ,
702+ ) as streams :
703+ yield streams
0 commit comments