From 2efbb423fde7d4377e8a315178589e03b18f738c Mon Sep 17 00:00:00 2001 From: Christian Sidak Date: Sun, 5 Apr 2026 21:14:51 -0700 Subject: [PATCH 1/2] Fix infinite retry loop in StreamableHTTP _handle_reconnection When a reconnection succeeds at the HTTP level but the SSE stream ends without delivering a complete response, _handle_reconnection recursed with attempt=0, resetting the counter and making MAX_RECONNECTION_ATTEMPTS ineffective. This caused the client to retry forever if the server repeatedly accepted connections but dropped streams. Pass attempt + 1 instead of 0 so total reconnection attempts are properly tracked across recursions. Fixes #2393 --- src/mcp/client/streamable_http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mcp/client/streamable_http.py b/src/mcp/client/streamable_http.py index 9a119c633..7d4dde1d0 100644 --- a/src/mcp/client/streamable_http.py +++ b/src/mcp/client/streamable_http.py @@ -421,9 +421,9 @@ async def _handle_reconnection( await event_source.response.aclose() return - # Stream ended again without response - reconnect again (reset attempt counter) + # Stream ended again without response - reconnect again logger.info("SSE stream disconnected, reconnecting...") - await self._handle_reconnection(ctx, reconnect_last_event_id, reconnect_retry_ms, 0) + await self._handle_reconnection(ctx, reconnect_last_event_id, reconnect_retry_ms, attempt + 1) except Exception as e: # pragma: no cover logger.debug(f"Reconnection failed: {e}") # Try to reconnect again if we still have an event ID From a4cb11bfc7805bbd9be682df493d01010cb93bf4 Mon Sep 17 00:00:00 2001 From: Christian Sidak Date: Thu, 9 Apr 2026 19:58:39 -0700 Subject: [PATCH 2/2] Reset reconnection attempt counter when server makes progress When the server sends new events before closing the stream (indicated by a changed last_event_id), the reconnection is intentional and the attempt counter should reset to 0. Only increment the counter when no new events were received, which indicates an actual failure to make progress. This prevents legitimate server-initiated reconnection patterns from being penalized by the retry limit while still protecting against infinite loops when the server returns empty responses. Github-Issue: #2397 --- src/mcp/client/streamable_http.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/mcp/client/streamable_http.py b/src/mcp/client/streamable_http.py index 7d4dde1d0..6e57317ca 100644 --- a/src/mcp/client/streamable_http.py +++ b/src/mcp/client/streamable_http.py @@ -423,7 +423,10 @@ async def _handle_reconnection( # Stream ended again without response - reconnect again logger.info("SSE stream disconnected, reconnecting...") - await self._handle_reconnection(ctx, reconnect_last_event_id, reconnect_retry_ms, attempt + 1) + # Reset attempt counter if server made progress (sent new events), + # otherwise count as a failed attempt + next_attempt = 0 if reconnect_last_event_id != last_event_id else attempt + 1 + await self._handle_reconnection(ctx, reconnect_last_event_id, reconnect_retry_ms, next_attempt) except Exception as e: # pragma: no cover logger.debug(f"Reconnection failed: {e}") # Try to reconnect again if we still have an event ID