{"repo":"NousResearch/hermes-agent","public_id":"16856","display_label":"Defer MCP discovery away from model_tools import blocking event loop","summary":"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.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":16899,"merged_at":"2026-04-28T08:17:59+00:00"},"resolution":{"state":"resolved","by":"pr-16899"},"freshness":{"state":"stale","last_verified_at":"2026-04-28T08:17:59+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":16856,"pr_number":16899,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-04-28T08:17:59+00:00"}],"members":[{"kind":"issue","number":16856,"title":"Lazy import of model_tools blocks asyncio event loop on first gateway message when an MCP server is slow/unreachable","url":null,"state":"closed"},{"kind":"pr","number":16877,"title":"fix(gateway): defer MCP discovery to executor when imported inside event loop (#16856)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#1","number":1},{"kind":"pr","value":"#16899","number":16899}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":16899,"title":"fix(mcp): move discovery out of model_tools import side effect (#16856)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["acp_adapter/entry.py","gateway/run.py","hermes_cli/main.py","model_tools.py","tui_gateway/entry.py"],"fix_hunks":[{"file":"acp_adapter/entry.py","hunks":["@@ -112,6 +112,17 @@ def main() -> None:\n     import acp\n     from .server import HermesACPAgent\n \n+    # MCP tool discovery from config.yaml — run before asyncio.run() so\n+    # it's safe to use blocking waits.  (ACP also registers per-session\n+    # MCP servers dynamically via asyncio.to_thread inside the event\n+    # loop; that path is unaffected.)  Moved from model_tools.py module\n+    # scope to avoid freezing the gateway's loop on lazy import (#16856).\n+    try:\n+        from tools.mcp_tool import discover_mcp_tools\n+        discover_mcp_tools()\n+    except Exception:\n+        logger.debug(\"MCP tool discovery failed at ACP startup\", exc_info=True)\n+\n     agent = HermesACPAgent()\n     try:\n         asyncio.run(acp.run_agent(agent, use_unstable_protocol=True))"]},{"file":"gateway/run.py","hunks":["@@ -11671,6 +11671,19 @@ def restart_signal_handler():\n     atexit.register(remove_pid_file)\n     atexit.register(release_gateway_runtime_lock)\n \n+    # MCP tool discovery — run in an executor so the asyncio event loop\n+    # stays responsive even when a configured MCP server is slow or\n+    # unreachable.  discover_mcp_tools() uses a blocking 120s wait\n+    # internally; calling it from the loop thread would freeze platform\n+    # heartbeats (Discord shard, Telegram polling) until it returned.\n+    # See #16856.\n+    try:\n+        from tools.mcp_tool import discover_mcp_tools\n+        _loop = asyncio.get_running_loop()\n+        await _loop.run_in_executor(None, discover_mcp_tools)\n+    except Exception as e:\n+        logger.debug(\"MCP tool discovery failed: %s\", e)\n+\n     # Start the gateway\n     success = await runner.start()\n     if not success:"]},{"file":"hermes_cli/main.py","hunks":["@@ -10101,6 +10101,17 @@ def cmd_acp(args):\n             logger.debug(\n                 \"plugin discovery failed at CLI startup\", exc_info=True,\n             )\n+        try:\n+            # MCP tool discovery — no event loop running in CLI/TUI startup,\n+            # so inline is safe.  Moved here from model_tools.py module scope\n+            # to avoid freezing the gateway's event loop on its first message\n+            # via the same lazy import path (#16856).\n+            from tools.mcp_tool import discover_mcp_tools\n+            discover_mcp_tools()\n+        except Exception:\n+            logger.debug(\n+                \"MCP tool discovery failed at CLI startup\", exc_info=True,\n+            )\n         try:\n             from hermes_cli.config import load_config\n             from agent.shell_hooks import register_from_config"]},{"file":"model_tools.py","hunks":["@@ -138,12 +138,18 @@ def _run_async(coro):\n \n discover_builtin_tools()\n \n-# MCP tool discovery (external MCP servers from config)\n-try:\n-    from tools.mcp_tool import discover_mcp_tools\n-    discover_mcp_tools()\n-except Exception as e:\n-    logger.debug(\"MCP tool discovery failed: %s\", e)\n+# MCP tool discovery (external MCP servers from config) used to run here as\n+# a module-level side effect.  It was removed because discover_mcp_tools()\n+# internally uses a blocking future.result(timeout=120) wait, and the\n+# gateway lazy-imports this module from inside the asyncio event loop on\n+# the first user message — freezing Discord/Telegram heartbeats for up to\n+# 120s whenever any configured MCP server was slow or unreachable (#16856).\n+#\n+# Each entry point now runs discovery explicitly at its own startup:\n+#   - gateway/run.py            -> start_gateway() uses run_in_executor\n+#   - cli.py, hermes_cli/*      -> inline on startup (no event loop)\n+#   - tui_gateway/server.py     -> inline on startup (no event loop)\n+#   - acp_adapter/server.py     -> asyncio.to_thread on session init\n \n # Plugin tool discovery (user/project/pip plugins)\n try:"]},{"file":"tui_gateway/entry.py","hunks":["@@ -105,6 +105,17 @@ def _log_exit(reason: str) -> None:\n def main():\n     _install_sidecar_publisher()\n \n+    # MCP tool discovery — inline is safe here: TUI entry is a plain\n+    # sync loop with no asyncio event loop to block.  Previously ran as\n+    # a model_tools.py module-level side effect; moved to explicit\n+    # startup calls to avoid freezing the gateway's loop on lazy import\n+    # (#16856).\n+    try:\n+        from tools.mcp_tool import discover_mcp_tools\n+        discover_mcp_tools()\n+    except Exception:\n+        pass\n+\n     if not write_json({\n         \"jsonrpc\": \"2.0\",\n         \"method\": \"event\",\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}