hermes - 💡(How to fix) Fix MCP startup hangs: synchronous HTTP call in osv_check blocks asyncio event loop [3 pull requests]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

When the TUI gateway starts, discover_mcp_tools() calls _run_stdio() for each MCP server using stdio transport. Inside _run_stdio() (an async def method), check_package_for_malware() makes a synchronous blocking urllib.request.urlopen() call to api.osv.dev. When the OSV API SSL handshake hangs (intermittent network issue), the entire asyncio event loop is frozen — blocking MCP discovery for up to 120s.

Meanwhile, the TUI's startup timeout is only 15s, so the user sees "gateway startup timeout" / "error: gateway exited" and the TUI becomes unusable.

Error Message

Meanwhile, the TUI's startup timeout is only 15s, so the user sees "gateway startup timeout" / "error: gateway exited" and the TUI becomes unusable.

Root Cause

File: tools/mcp_tool.py, line 1272, inside async def _run_stdio():

malware_error = check_package_for_malware(command, args)

check_package_for_malware_query_osvurllib.request.urlopen() is synchronous blocking I/O running inside the asyncio event loop thread. No await, no thread pool — it blocks the loop directly.

File: tools/osv_check.py, line 150:

with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:

Python's urllib timeout doesn't always cover the SSL handshake phase — the crash log confirms the thread is stuck in ssl.py:do_handshake().

Fix Action

Fixed

Code Example

malware_error = check_package_for_malware(command, args)

---

with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:

---

=== SIGTERM received · 2026-05-20 14:55:12 ===
main-thread stack:
  File "tui_gateway/entry.py", line 215, in main
    discover_mcp_tools()
  File "tools/mcp_tool.py", line 3312, in discover_mcp_tools
    tool_names = register_mcp_servers(servers)
  File "tools/mcp_tool.py", line 3262, in register_mcp_servers
    _run_on_mcp_loop(_discover_all, timeout=120)
  File "tools/mcp_tool.py", line 2208, in _run_on_mcp_loop
    return future.result(timeout=wait_timeout)
  (blocked waiting on future)

--- thread mcp-event-loop ---
  File "tools/mcp_tool.py", line 1272, in _run_stdio
    malware_error = check_package_for_malware(command, args)
  File "tools/osv_check.py", line 47, in check_package_for_malware
    malware = _query_osv(package, ecosystem, version)
  File "tools/osv_check.py", line 150, in _query_osv
    with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:
  File "urllib/request.py", line 1348, in do_open
    h.request(req.get_method(), req.selector, req.data, headers, ...)
  File "http/client.py", line 1458, in connect
    self.sock = self._context.wrap_socket(self.sock, ...)
  File "ssl.py", line 1346, in do_handshake
    self._sslobj.do_handshake()

---

# tools/mcp_tool.py, line 1272
malware_error = await asyncio.to_thread(check_package_for_malware, command, args)
RAW_BUFFERClick to expand / collapse

Summary

When the TUI gateway starts, discover_mcp_tools() calls _run_stdio() for each MCP server using stdio transport. Inside _run_stdio() (an async def method), check_package_for_malware() makes a synchronous blocking urllib.request.urlopen() call to api.osv.dev. When the OSV API SSL handshake hangs (intermittent network issue), the entire asyncio event loop is frozen — blocking MCP discovery for up to 120s.

Meanwhile, the TUI's startup timeout is only 15s, so the user sees "gateway startup timeout" / "error: gateway exited" and the TUI becomes unusable.

Root Cause

File: tools/mcp_tool.py, line 1272, inside async def _run_stdio():

malware_error = check_package_for_malware(command, args)

check_package_for_malware_query_osvurllib.request.urlopen() is synchronous blocking I/O running inside the asyncio event loop thread. No await, no thread pool — it blocks the loop directly.

File: tools/osv_check.py, line 150:

with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:

Python's urllib timeout doesn't always cover the SSL handshake phase — the crash log confirms the thread is stuck in ssl.py:do_handshake().

Stack Trace (from ~/.hermes/logs/tui_gateway_crash.log)

=== SIGTERM received · 2026-05-20 14:55:12 ===
main-thread stack:
  File "tui_gateway/entry.py", line 215, in main
    discover_mcp_tools()
  File "tools/mcp_tool.py", line 3312, in discover_mcp_tools
    tool_names = register_mcp_servers(servers)
  File "tools/mcp_tool.py", line 3262, in register_mcp_servers
    _run_on_mcp_loop(_discover_all, timeout=120)
  File "tools/mcp_tool.py", line 2208, in _run_on_mcp_loop
    return future.result(timeout=wait_timeout)
  (blocked waiting on future)

--- thread mcp-event-loop ---
  File "tools/mcp_tool.py", line 1272, in _run_stdio
    malware_error = check_package_for_malware(command, args)
  File "tools/osv_check.py", line 47, in check_package_for_malware
    malware = _query_osv(package, ecosystem, version)
  File "tools/osv_check.py", line 150, in _query_osv
    with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:
  File "urllib/request.py", line 1348, in do_open
    h.request(req.get_method(), req.selector, req.data, headers, ...)
  File "http/client.py", line 1458, in connect
    self.sock = self._context.wrap_socket(self.sock, ...)
  File "ssl.py", line 1346, in do_handshake
    self._sslobj.do_handshake()

Reproduction

  1. Configure multiple MCP servers with stdio transport (uvx/npx commands)
  2. Run hermes --tui
  3. When api.osv.dev has intermittent connectivity or SSL issues, the gateway hangs during startup
  4. After 15s, TUI shows "gateway startup timeout"
  5. After up to 120s, discovery times out → but TUI is already in broken state

The malware check itself is fast when the API is reachable (~0.00s measured). The issue is the intermittent SSL handshake hang with no async fallback.

Environment

  • Hermes version: v2026.5.16-587-g340d2b6de
  • macOS 14.8.7, Python 3.11.4
  • 5 MCP servers configured (fetch/uvx, filesystem/npx, github/npx, intellij/sse, time/uvx)

Suggested Fix

Use asyncio.to_thread() to run the malware check off the event loop:

# tools/mcp_tool.py, line 1272
malware_error = await asyncio.to_thread(check_package_for_malware, command, args)

Or alternatively, use an async HTTP client (aiohttp, httpx) inside _query_osv so the call is non-blocking.

Related

  • #16856 (closed): moved MCP discovery from lazy import to explicit startup — fixed the gateway freeze on first message, but the underlying synchronous I/O in the malware check was not addressed.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING