# NousResearch/hermes-agent · MCP keepalive bugs, reconnect loops, stale connections

## Open now

- Nothing open on GitHub.

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.

## TL;DR

- 10 member(s): 0 open issue(s), 3 closed issue(s), 0 open PR(s), 2 merged PR(s), 5 PR(s) closed without merging.
- A fix was merged to main in PR #20209. Verify it addresses your case.
- **Recommended:** read PR #20209 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-20209,pr-68658`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.17`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #17003: MCP HTTP connections go stale after extended idle periods
- #62212: MCP stdio keepalive probe triggers infinite reconnect loop on empty exceptions
- #65787: \[Bug\]: MCP keepalive uses list_tools() (O(tool-count)) — guaranteed timeout + reconnect loop on large servers

## Prior & competing PRs

- **#17016: fix: MCP circuit breaker recovery and HTTP keepalive (#16788, #17003)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#20209: fix(mcp): add periodic keepalive to _wait_for_lifecycle_event (salvage #17016)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `tools/mcp_tool.py`
  - fix hunks:
  - `tools/mcp_tool.py`

    ```diff
    @@ -1038,14 +1038,43 @@ async def _wait_for_lifecycle_event(self) -> str:
                             with a fresh signal.
     
             Shutdown takes precedence if both events are set simultaneously.
    +
    +        Periodically sends a lightweight keepalive (``list_tools``) to
    +        prevent TCP connections from going stale during long idle
    +        periods (#17003).  If the keepalive fails, triggers a reconnect.
             """
    +        # Keepalive interval in seconds.  Must be shorter than typical
    +        # LB / NAT idle-timeout (commonly 300-600s).
    +        _KEEPALIVE_INTERVAL = 180  # 3 minutes
    +
             shutdown_task = asyncio.create_task(self._shutdown_event.wait())
             reconnect_task = asyncio.create_task(self._reconnect_event.wait())
             try:
    -            await asyncio.wait(
    -                {shutdown_task, reconnect_task},
    -                return_when=asyncio.FIRST_COMPLETED,
    -            )
    +            while True:
    +                done, _pending = await asyncio.wait(
    +                    {shutdown_task, reconnect_task},
    +                    timeout=_KEEPALIVE_INTERVAL,
    +                    return_when=asyncio.FIRST_COMPLETED,
    +                )
    +                if done:
    +                    break
    +
    +                # Timeout — no lifecycle event fired.  Send a keepalive
    +                # to exercise the connection and detect stale sockets.
    +                if self.session:
    +                    try:
    +                        await asyncio.wait_for(
    +                            self.session.list_tools(),
    +                            timeout=30.0,
    +                        )
    +                    except Exception as exc:
    +                        logger.warning(
    +                            "MCP server '%s' keepalive failed, "
    +                            "triggering reconnect: %s",
    +                            self.name, exc,
    +                        )
    +                        self._reconnect_event.set()
    +                        break
             finally:
                 for t in (shutdown_task, reconnect_task):
                     if not t.done():
    ```
- **#62506: fix(mcp): prevent infinite keepalive reconnect loop on empty exceptions (#62212)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#62515`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#62515: fix(mcp): prevent infinite keepalive reconnect loop on empty exceptions (#62212)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#64838`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#68658: fix(mcp): budget-bounded reconnects, exception-group unwrapping, and per-transition logging**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `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`
- **#62220: fix(mcp): treat empty keepalive exceptions as ping unsupported to prevent infinite reconnect loops**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#68658`, `#62212`, `#68658`
- **#65813: fix(mcp): increase list_tools keepalive timeout to 60s and improve error logging**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#65787`, `#65795`, `#65795`

## Fix status

- Merged to main in PR #20209 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-21`.
- Subsystem: `mcp`
