@@ -58,25 +58,10 @@ class ResumptionError(StreamableHTTPError):
5858 """Raised when resumption request is invalid."""
5959
6060
61- @dataclass
62- class StreamableHTTPReconnectionOptions :
63- """Configuration options for reconnection behavior of StreamableHTTPTransport.
64-
65- Attributes:
66- initial_reconnection_delay: Initial backoff time in seconds. Default is 1.0.
67- max_reconnection_delay: Maximum backoff time in seconds. Default is 30.0.
68- reconnection_delay_grow_factor: Factor by which delay increases. Default is 1.5.
69- max_retries: Maximum reconnection attempts. Default is 2.
70- """
71-
72- initial_reconnection_delay : float = 1.0
73- max_reconnection_delay : float = 30.0
74- reconnection_delay_grow_factor : float = 1.5
75- max_retries : int = 2
76-
77- def __post_init__ (self ) -> None :
78- if self .initial_reconnection_delay > self .max_reconnection_delay :
79- raise ValueError ("initial_reconnection_delay cannot exceed max_reconnection_delay" )
61+ # Default reconnection settings per SEP-1699
62+ # The server controls timing via SSE retry field; this is just a fallback
63+ DEFAULT_RECONNECTION_DELAY = 1.0 # seconds, used when server doesn't provide retry
64+ DEFAULT_MAX_RETRIES = 2
8065
8166
8267@dataclass
@@ -102,7 +87,7 @@ def __init__(
10287 timeout : float | timedelta = 30 ,
10388 sse_read_timeout : float | timedelta = 60 * 5 ,
10489 auth : httpx .Auth | None = None ,
105- reconnection_options : StreamableHTTPReconnectionOptions | None = None ,
90+ max_retries : int = DEFAULT_MAX_RETRIES ,
10691 ) -> None :
10792 """Initialize the StreamableHTTP transport."""
10893 self .url = url
@@ -114,7 +99,7 @@ def __init__(
11499 self .auth = auth
115100 self .session_id = None
116101 self .protocol_version = None
117- self .reconnection_options = reconnection_options or StreamableHTTPReconnectionOptions ()
102+ self .max_retries = max_retries
118103 self ._server_retry_seconds : float | None = None # Server-provided retry delay
119104 self .request_headers = {
120105 ACCEPT : f"{ JSON } , { SSE } " ,
@@ -166,23 +151,15 @@ def _maybe_extract_protocol_version_from_message(
166151 ) # pragma: no cover
167152 logger .warning (f"Raw result: { message .root .result } " )
168153
169- def _get_next_reconnection_delay (self , attempt : int ) -> float :
170- """Calculate the next reconnection delay using exponential backoff .
154+ def _get_reconnection_delay (self ) -> float :
155+ """Get the reconnection delay per SEP-1699 .
171156
172- Args:
173- attempt: Current reconnection attempt count
174-
175- Returns:
176- Time to wait in seconds before next reconnection attempt
157+ Returns the server-provided retry value if available (MUST per spec),
158+ otherwise falls back to a simple default.
177159 """
178- # Use server-provided retry value if available
179160 if self ._server_retry_seconds is not None :
180161 return self ._server_retry_seconds
181-
182- # Fall back to exponential backoff
183- opts = self .reconnection_options
184- delay = opts .initial_reconnection_delay * (opts .reconnection_delay_grow_factor ** attempt )
185- return min (delay , opts .max_reconnection_delay )
162+ return DEFAULT_RECONNECTION_DELAY
186163
187164 async def _handle_sse_event (
188165 self ,
@@ -427,17 +404,15 @@ async def _attempt_sse_reconnection( # pragma: no cover
427404 Called when SSE stream ends without receiving a response/error,
428405 but we have a priming event indicating resumability.
429406 """
430- max_retries = self .reconnection_options .max_retries
431-
432- if attempt >= max_retries :
433- error_msg = f"Max reconnection attempts ({ max_retries } ) exceeded"
407+ if attempt >= self .max_retries :
408+ error_msg = f"Max reconnection attempts ({ self .max_retries } ) exceeded"
434409 logger .error (error_msg )
435410 await ctx .read_stream_writer .send (StreamableHTTPError (error_msg ))
436411 return
437412
438- # Calculate delay (uses server retry if available, else exponential backoff)
439- delay = self ._get_next_reconnection_delay ( attempt )
440- logger .info (f"SSE stream closed, reconnecting in { delay :.1f} s (attempt { attempt + 1 } /{ max_retries } )" )
413+ # Use server-provided retry interval (MUST per spec) or fallback
414+ delay = self ._get_reconnection_delay ( )
415+ logger .info (f"SSE stream closed, reconnecting in { delay :.1f} s (attempt { attempt + 1 } /{ self . max_retries } )" )
441416
442417 await anyio .sleep (delay )
443418
@@ -635,7 +610,7 @@ async def streamablehttp_client(
635610 terminate_on_close : bool = True ,
636611 httpx_client_factory : McpHttpClientFactory = create_mcp_http_client ,
637612 auth : httpx .Auth | None = None ,
638- reconnection_options : StreamableHTTPReconnectionOptions | None = None ,
613+ max_retries : int = DEFAULT_MAX_RETRIES ,
639614) -> AsyncGenerator [
640615 tuple [
641616 MemoryObjectReceiveStream [SessionMessage | Exception ],
@@ -649,8 +624,10 @@ async def streamablehttp_client(
649624
650625 `sse_read_timeout` determines how long (in seconds) the client will wait for a new
651626 event before disconnecting. All other HTTP operations are controlled by `timeout`.
627+ `max_retries` controls how many times the client will attempt to reconnect when
628+ the server closes an SSE stream mid-operation (SEP-1699 polling).
652629 """
653- transport = StreamableHTTPTransport (url , headers , timeout , sse_read_timeout , auth , reconnection_options )
630+ transport = StreamableHTTPTransport (url , headers , timeout , sse_read_timeout , auth , max_retries )
654631
655632 read_stream_writer , read_stream = anyio .create_memory_object_stream [SessionMessage | Exception ](0 )
656633 write_stream , write_stream_reader = anyio .create_memory_object_stream [SessionMessage ](0 )
0 commit comments