From 5c2c85c5452f227e6f3b79d90d2c50f7adaddf8f Mon Sep 17 00:00:00 2001 From: Cornna <96944678+ymylive@users.noreply.github.com> Date: Sun, 28 Jun 2026 02:53:08 -0700 Subject: [PATCH] fix(tui): start MCP discovery for websocket sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The desktop app and dashboard chat reach the agent through the /api/ws JSON-RPC sidecar (tui_gateway.ws.handle_ws), NOT through tui_gateway.entry.main() — the stdio-TUI path that spawns the background MCP discovery thread. In the WS process discovery was therefore never started: _make_agent only *waits* (wait_for_mcp_discovery), which no-ops when the thread was never created, so the agent snapshotted an MCP-less tool list. The only discovery trigger reachable was a manual /reload-mcp, which is why tools appeared after a reload but vanished on restart. Start the shared, idempotent, config-gated background discovery in handle_ws right after accept() and before gateway.ready, so the first agent build picks up already-spawning servers (and the existing late-binding refresh handles slow ones). Fixes #38945. --- tests/test_tui_gateway_ws.py | 36 ++++++++++++++++++++++++++++++++++++ tui_gateway/ws.py | 16 ++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/tests/test_tui_gateway_ws.py b/tests/test_tui_gateway_ws.py index 39a9d61a9f6..1a9b37259c0 100644 --- a/tests/test_tui_gateway_ws.py +++ b/tests/test_tui_gateway_ws.py @@ -2,10 +2,46 @@ import asyncio import threading import time +from hermes_cli import mcp_startup from tui_gateway import server from tui_gateway import ws as ws_mod +def test_ws_startup_starts_background_mcp_discovery(monkeypatch): + """The desktop app and dashboard chat reach the agent through this WS + sidecar, not through tui_gateway.entry.main() (which spawns the discovery + thread for the stdio TUI). handle_ws must start discovery itself, otherwise + _make_agent's wait_for_mcp_discovery no-ops and the agent snapshots an + MCP-less tool list. Regression test for #38945.""" + calls = [] + monkeypatch.setattr( + mcp_startup, + "start_background_mcp_discovery", + lambda **kw: calls.append(kw), + ) + + class FakeWS: + async def accept(self): + pass + + async def send_text(self, line): + pass + + async def receive_text(self): + raise ws_mod._WebSocketDisconnect() + + async def close(self): + pass + + server._sessions.clear() + try: + asyncio.run(ws_mod.handle_ws(FakeWS())) + finally: + server._sessions.clear() + + assert calls == [{"logger": ws_mod._log, "thread_name": "tui-ws-mcp-discovery"}] + + def _run_disconnect(monkeypatch, seed): """Drive handle_ws to its disconnect `finally`, seeding sessions against the live WSTransport the moment it exists. Returns nothing; inspect _sessions.""" diff --git a/tui_gateway/ws.py b/tui_gateway/ws.py index b487e934842..62218e60b9d 100644 --- a/tui_gateway/ws.py +++ b/tui_gateway/ws.py @@ -190,6 +190,22 @@ async def handle_ws(ws: Any) -> None: transport = WSTransport(ws, asyncio.get_running_loop(), peer=peer) + # The desktop app and dashboard chat reach the agent through this WS + # sidecar, NOT through tui_gateway.entry.main() (the stdio TUI path that + # spawns the background MCP discovery thread). Without starting it here, + # discovery never runs in this process: _make_agent only *waits* on the + # thread (wait_for_mcp_discovery), which no-ops when it was never + # created, so the agent snapshots an MCP-less tool list and the only way + # to surface MCP tools is a manual /reload-mcp. Start it once per + # process here (idempotent, config-gated) before gateway.ready so the + # first agent build can pick up already-spawning servers. (#38945) + from hermes_cli.mcp_startup import start_background_mcp_discovery + + start_background_mcp_discovery( + logger=_log, + thread_name="tui-ws-mcp-discovery", + ) + ready_ok = await transport.write_async( { "jsonrpc": "2.0",