# NousResearch/hermes-agent · Defer MCP discovery away from model_tools import blocking event loop

## Open now

- Nothing open on GitHub.

Three members relate to issue #16856: a report that lazily importing model_tools can block the asyncio event loop on the first gateway message when an MCP server is slow or unreachable, and two pull requests that propose moving or deferring MCP discovery out of the import path. The titles show the intended approach but do not indicate outcomes.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #16899. Verify it addresses your case.
- **Recommended:** read PR #16899 (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: `resolved` · deciding PR(s): `pr-16899`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: 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

- #16856: Lazy import of model_tools blocks asyncio event loop on first gateway message when an MCP server is slow/unreachable

## Prior & competing PRs

- **#16877: fix(gateway): defer MCP discovery to executor when imported inside event loop (#16856)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#1`, `#16899`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#16899: fix(mcp): move discovery out of model_tools import side effect (#16856)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `acp_adapter/entry.py`, `gateway/run.py`, `hermes_cli/main.py`, `model_tools.py`, `tui_gateway/entry.py`
  - fix hunks:
  - `acp_adapter/entry.py`

    ```diff
    @@ -112,6 +112,17 @@ def main() -> None:
         import acp
         from .server import HermesACPAgent
     
    +    # MCP tool discovery from config.yaml — run before asyncio.run() so
    +    # it's safe to use blocking waits.  (ACP also registers per-session
    +    # MCP servers dynamically via asyncio.to_thread inside the event
    +    # loop; that path is unaffected.)  Moved from model_tools.py module
    +    # scope to avoid freezing the gateway's loop on lazy import (#16856).
    +    try:
    +        from tools.mcp_tool import discover_mcp_tools
    +        discover_mcp_tools()
    +    except Exception:
    +        logger.debug("MCP tool discovery failed at ACP startup", exc_info=True)
    +
         agent = HermesACPAgent()
         try:
             asyncio.run(acp.run_agent(agent, use_unstable_protocol=True))
    ```
  - `gateway/run.py`

    ```diff
    @@ -11671,6 +11671,19 @@ def restart_signal_handler():
         atexit.register(remove_pid_file)
         atexit.register(release_gateway_runtime_lock)
     
    +    # MCP tool discovery — run in an executor so the asyncio event loop
    +    # stays responsive even when a configured MCP server is slow or
    +    # unreachable.  discover_mcp_tools() uses a blocking 120s wait
    +    # internally; calling it from the loop thread would freeze platform
    +    # heartbeats (Discord shard, Telegram polling) until it returned.
    +    # See #16856.
    +    try:
    +        from tools.mcp_tool import discover_mcp_tools
    +        _loop = asyncio.get_running_loop()
    +        await _loop.run_in_executor(None, discover_mcp_tools)
    +    except Exception as e:
    +        logger.debug("MCP tool discovery failed: %s", e)
    +
         # Start the gateway
         success = await runner.start()
         if not success:
    ```
  - `hermes_cli/main.py`

    ```diff
    @@ -10101,6 +10101,17 @@ def cmd_acp(args):
                 logger.debug(
                     "plugin discovery failed at CLI startup", exc_info=True,
                 )
    +        try:
    +            # MCP tool discovery — no event loop running in CLI/TUI startup,
    +            # so inline is safe.  Moved here from model_tools.py module scope
    +            # to avoid freezing the gateway's event loop on its first message
    +            # via the same lazy import path (#16856).
    +            from tools.mcp_tool import discover_mcp_tools
    +            discover_mcp_tools()
    +        except Exception:
    +            logger.debug(
    +                "MCP tool discovery failed at CLI startup", exc_info=True,
    +            )
             try:
                 from hermes_cli.config import load_config
                 from agent.shell_hooks import register_from_config
    ```
  - `model_tools.py`

    ```diff
    @@ -138,12 +138,18 @@ def _run_async(coro):
     
     discover_builtin_tools()
     
    -# MCP tool discovery (external MCP servers from config)
    -try:
    -    from tools.mcp_tool import discover_mcp_tools
    -    discover_mcp_tools()
    -except Exception as e:
    -    logger.debug("MCP tool discovery failed: %s", e)
    +# MCP tool discovery (external MCP servers from config) used to run here as
    +# a module-level side effect.  It was removed because discover_mcp_tools()
    +# internally uses a blocking future.result(timeout=120) wait, and the
    +# gateway lazy-imports this module from inside the asyncio event loop on
    +# the first user message — freezing Discord/Telegram heartbeats for up to
    +# 120s whenever any configured MCP server was slow or unreachable (#16856).
    +#
    +# Each entry point now runs discovery explicitly at its own startup:
    +#   - gateway/run.py            -> start_gateway() uses run_in_executor
    +#   - cli.py, hermes_cli/*      -> inline on startup (no event loop)
    +#   - tui_gateway/server.py     -> inline on startup (no event loop)
    +#   - acp_adapter/server.py     -> asyncio.to_thread on session init
     
     # Plugin tool discovery (user/project/pip plugins)
     try:
    ```
  - `tui_gateway/entry.py`

    ```diff
    @@ -105,6 +105,17 @@ def _log_exit(reason: str) -> None:
     def main():
         _install_sidecar_publisher()
     
    +    # MCP tool discovery — inline is safe here: TUI entry is a plain
    +    # sync loop with no asyncio event loop to block.  Previously ran as
    +    # a model_tools.py module-level side effect; moved to explicit
    +    # startup calls to avoid freezing the gateway's loop on lazy import
    +    # (#16856).
    +    try:
    +        from tools.mcp_tool import discover_mcp_tools
    +        discover_mcp_tools()
    +    except Exception:
    +        pass
    +
         if not write_json({
             "jsonrpc": "2.0",
             "method": "event",
    ```

## Fix status

- Merged to main in PR #16899 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-04-28`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
