{"repo":"NousResearch/hermes-agent","public_id":"17003","display_label":"MCP keepalive bugs, reconnect loops, stale connections","summary":"These issues and fixes address multiple related problems in MCP keepalive logic: inefficient list_tools-based probing, infinite reconnect loops triggered by empty exceptions, and stale HTTP connections after idle periods. Several fixes attempt to address overlapping aspects of the same fragile keepalive mechanism.","subsystem":"mcp","counts":{"members":10,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":3,"merged_prs":2,"attempt_prs":5},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":20209},"resolution":{"state":"partially_resolved","by":"pr-20209,pr-68658"},"freshness":{"state":"verified","last_verified_at":"2026-07-21T19:42:34+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.17155555555555554},"fix_edges":[{"issue_number":17003,"pr_number":20209,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":62212,"pr_number":68658,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":65673,"pr_number":68658,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":66092,"pr_number":68658,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":17003,"title":"MCP HTTP connections go stale after extended idle periods","url":null,"state":"closed"},{"kind":"issue","number":62212,"title":"MCP stdio keepalive probe triggers infinite reconnect loop on empty exceptions","url":null,"state":"closed"},{"kind":"issue","number":65787,"title":"[Bug]: MCP keepalive uses list_tools() (O(tool-count)) — guaranteed timeout + reconnect loop on large servers","url":null,"state":"closed"},{"kind":"pr","number":17016,"title":"fix: MCP circuit breaker recovery and HTTP keepalive (#16788, #17003)","url":null,"state":"closed","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":20209,"title":"fix(mcp): add periodic keepalive to _wait_for_lifecycle_event (salvage #17016)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tools/mcp_tool.py"],"fix_hunks":[{"file":"tools/mcp_tool.py","hunks":["@@ -1038,14 +1038,43 @@ async def _wait_for_lifecycle_event(self) -> str:\n                         with a fresh signal.\n \n         Shutdown takes precedence if both events are set simultaneously.\n+\n+        Periodically sends a lightweight keepalive (``list_tools``) to\n+        prevent TCP connections from going stale during long idle\n+        periods (#17003).  If the keepalive fails, triggers a reconnect.\n         \"\"\"\n+        # Keepalive interval in seconds.  Must be shorter than typical\n+        # LB / NAT idle-timeout (commonly 300-600s).\n+        _KEEPALIVE_INTERVAL = 180  # 3 minutes\n+\n         shutdown_task = asyncio.create_task(self._shutdown_event.wait())\n         reconnect_task = asyncio.create_task(self._reconnect_event.wait())\n         try:\n-            await asyncio.wait(\n-                {shutdown_task, reconnect_task},\n-                return_when=asyncio.FIRST_COMPLETED,\n-            )\n+            while True:\n+                done, _pending = await asyncio.wait(\n+                    {shutdown_task, reconnect_task},\n+                    timeout=_KEEPALIVE_INTERVAL,\n+                    return_when=asyncio.FIRST_COMPLETED,\n+                )\n+                if done:\n+                    break\n+\n+                # Timeout — no lifecycle event fired.  Send a keepalive\n+                # to exercise the connection and detect stale sockets.\n+                if self.session:\n+                    try:\n+                        await asyncio.wait_for(\n+                            self.session.list_tools(),\n+                            timeout=30.0,\n+                        )\n+                    except Exception as exc:\n+                        logger.warning(\n+                            \"MCP server '%s' keepalive failed, \"\n+                            \"triggering reconnect: %s\",\n+                            self.name, exc,\n+                        )\n+                        self._reconnect_event.set()\n+                        break\n         finally:\n             for t in (shutdown_task, reconnect_task):\n                 if not t.done():\n"]}]},{"kind":"pr","number":62220,"title":"fix(mcp): treat empty keepalive exceptions as ping unsupported to prevent infinite reconnect loops","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#68658","number":68658},{"kind":"pr","value":"#62212","number":62212},{"kind":"pr","value":"#68658","number":68658}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":62506,"title":"fix(mcp): prevent infinite keepalive reconnect loop on empty exceptions (#62212)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#62515","number":62515}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":62515,"title":"fix(mcp): prevent infinite keepalive reconnect loop on empty exceptions (#62212)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#64838","number":64838}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":65813,"title":"fix(mcp): increase list_tools keepalive timeout to 60s and improve error logging","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#65787","number":65787},{"kind":"pr","value":"#65795","number":65795},{"kind":"pr","value":"#65795","number":65795}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":68658,"title":"fix(mcp): budget-bounded reconnects, exception-group unwrapping, and per-transition logging","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/tools/test_mcp_failure_classification.py","tests/tools/test_mcp_rapid_drop_budget.py","tests/tools/test_mcp_reconnect_log_hygiene.py","tests/tools/test_mcp_transport_group_reconnect.py","tools/mcp_tool.py"]}],"trust":{"default_tier":"verified","includes_suggested":false}}