MCP tool discovery missing in Desktop/TUI/Dashboard sessions
Cluster 37589 · apps/desktop. Data as of
3 open items. Fix merged in #122861 on 25 Sep 2026. Check it covers your case.
The short answer
- Known?
- Yes. 18 related items grouped here, 7 issues already closed.
- Fix event
- A fix was merged to main in PR #122861. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PRs #54161, #61949, #122861. That is separate from the fix event. - Still open
- 0 issues and 3 PRs.
Most members concern MCP tools not appearing in Desktop, TUI, or Dashboard sessions, with related changes to start or wait for MCP discovery and to rebuild tool schemas. A smaller subset covers resolving uv/uvx commands under GUI-style PATHs, which is an adjacent issue.
Open issues0
Open PRs3
Fixes on record
-
#44512 fix(desktop): load MCP tools for dashboard /api/ws backends
files:
hermes_cli/main.py,tests/hermes_cli/test_dashboard_unified_launch.py,tests/test_tui_gateway_server.py,tui_gateway/server.pyShow the diff
-
hermes_cli/main.py@@ -10431,6 +10431,26 @@ def cmd_dashboard(args): # the missing-provider state if it matters. print(f"⚠ Plugin discovery failed: {exc}", file=sys.stderr) + # Desktop chat uses the dashboard's in-process /api/ws gateway, which builds + # agents via tui_gateway.server._make_agent. That path only snapshots the + # tool registry — it never starts MCP discovery (the stdio TUI does that in + # tui_gateway/entry.py, which the dashboard process doesn't run). Without + # this, a profile's configured MCP servers never connect, so desktop + # sessions show no MCP tools. Spawn discovery in the background here so a + # slow/dead server can't block dashboard startup. + try: + from hermes_cli.mcp_startup import start_background_mcp_discovery + + start_background_mcp_discovery( + logger=logger, + thread_name="dashboard-mcp-discovery", + ) + except Exception: + logger.debug( + "Background MCP tool discovery failed at dashboard startup", + exc_info=True, + ) + from hermes_cli.web_server import start_server # The in-browser Chat tab (the embedded TUI over PTY/WebSocket) is always -
tests/hermes_cli/test_dashboard_unified_launch.py@@ -128,3 +128,45 @@ def test_reexec_child_does_not_reroute(self, main_mod, monkeypatch): with pytest.raises((SystemExit, AttributeError, ImportError, TypeError)): main_mod.cmd_dashboard(_args(open_profile="worker_x")) assert execs == [] + + def test_dashboard_starts_mcp_discovery_for_ws_backend(self, main_mod, monkeypatch): + """The dashboard process serves the /api/ws gateway but never runs + tui_gateway/entry.py, so it must kick off MCP discovery itself or + desktop sessions never see a profile's MCP tools.""" + monkeypatch.setattr( + "hermes_cli.profiles.get_active_profile_name", lambda: "default" + ) + monkeypatch.delenv("HERMES_WEB_DIST", raising=False) + monkeypatch.setattr(main_mod, "_sync_bundled_skills_quietly", lambda: None) + monkeypatch.setattr(main_mod, "_build_web_ui", lambda *_a, **_k: True) + monkeypatch.setitem(sys.modules, "fastapi", types.SimpleNamespace()) + monkeypatch.setitem(sys.modules, "uvicorn", types.SimpleNamespace()) + monkeypatch.setitem( + sys.modules, + "hermes_logging", + types.SimpleNamespace(setup_logging=lambda **_k: None), + ) + monkeypatch.setitem( + sys.modules, + "hermes_cli.plugins", + types.SimpleNamespace(discover_plugins=lambda: None), + ) + calls = [] + monkeypatch.setattr( + "hermes_cli.mcp_startup.start_background_mcp_discovery", + lambda **kwargs: calls.append(kwargs), + ) + monkeypatch.setitem( + sys.modules, + "hermes_cli.web_server", + types.SimpleNamespace(start_server=lambda **_kwargs: None), + ) + + main_mod.cmd_dashboard(_args()) + + assert calls == [ + { + "logger": main_mod.logger, + "thread_name": "dashboard-mcp-discovery", + } + ] -
tests/test_tui_gateway_server.py@@ -6096,6 +6096,24 @@ def test_make_agent_reads_nested_max_turns(monkeypatch): assert mock_agent.call_args.kwargs["max_iterations"] == 200 +def test_make_agent_waits_for_shared_mcp_discovery(monkeypatch): + _setup_make_agent_mocks(monkeypatch, {}) + waited = [] + + from hermes_cli import mcp_startup + + monkeypatch.setattr( + mcp_startup, + "wait_for_mcp_discovery", + lambda timeout=0.75: waited.append(timeout), + ) + + with patch("run_agent.AIAgent"): + server._make_agent("sid1", "key1") + + assert waited == [0.75] + + def test_make_agent_nested_max_turns_takes_priority(monkeypatch): _setup_make_agent_mocks( monkeypatch, {"agent": {"max_turns": 500}, "max_turns": 100} -
tui_gateway/server.py@@ -3072,11 +3072,17 @@ def _make_agent( from hermes_cli.runtime_provider import resolve_runtime_provider # MCP tool discovery runs in a background daemon thread at startup so a - # dead server can't freeze the shell (see tui_gateway/entry.py). The agent - # snapshots its tool list once here and never re-reads it, so briefly wait - # for in-flight discovery to land before building — bounded, so a slow/dead - # server still can't block. No-op once discovery has finished (every build - # after the first during a slow startup). + # dead server can't freeze the shell. The agent snapshots its tool list + # once here and never re-reads it, so briefly wait for in-flight discovery + # to land before building — bounded, so a slow/dead server still can't + # block. Dashboard /api/ws uses hermes_cli.mcp_startup; TUI stdio keeps + # its existing tui_gateway.entry-owned thread. + try: + from hermes_cli.mcp_startup import wait_for_mcp_discovery + + wait_for_mcp_discovery() + except Exception: + pass try: from tui_gateway.entry import wait_for_mcp_discovery
-
-
#54161 fix(tui): start MCP discovery for websocket sessions (#38945)
files:
infographic/mcp-ws-discovery/infographic.png,tests/test_tui_gateway_ws.py,tui_gateway/ws.py -
#61949 fix(tui): discover MCP tools in slash workers
files:
tests/tui_gateway/test_slash_worker_mcp_discovery.py,tui_gateway/slash_worker.pyShow the diff
-
tests/tui_gateway/test_slash_worker_mcp_discovery.py@@ -0,0 +1,105 @@ +"""Integration coverage for profile-local MCP discovery in slash workers.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +import queue +import subprocess +import sys +import textwrap +import threading + +import pytest +import yaml + +pytest.importorskip("mcp.server.fastmcp") + + +def test_profile_local_mcp_tool_is_visible_in_slash_worker(tmp_path): + profile_home = tmp_path / "profile-home" + profile_home.mkdir() + marker = "profile-local-61922" + server = tmp_path / "fastmcp_probe.py" + server.write_text( + textwrap.dedent( + f""" + from mcp.server.fastmcp import FastMCP + + mcp = FastMCP("profileprobe") + + @mcp.tool() + def hermes_61922_profile_probe() -> str: + return {marker!r} + + if __name__ == "__main__": + mcp.run(transport="stdio") + """ + ), + encoding="utf-8", + ) + (profile_home / "config.yaml").write_text( + yaml.safe_dump( + { + "mcp_servers": { + "profileprobe": { + "enabled": True, + "command": sys.executable, + "args": [str(server)], + } + } + } + ), + encoding="utf-8", + ) + + env = os.environ.copy() + for key in list(env): + if key.endswith("_API_KEY") or key.endswith("_TOKEN"): + env.pop(key) + env["HERMES_HOME"] = str(profile_home) + env["PYTHONPATH"] = str(Path(__file__).resolve().parents[2]) + env["HERMES_SLASH_WATCHDOG_GRACE_S"] = "0" + env["HERMES_SLASH_WATCHDOG_POLL_S"] = "0.05" + proc = subprocess.Popen( + [ + sys.executable, + "-u", + "-m", + "tui_gateway.slash_worker", + "--session-key", + "agent:main:tui:dm:mcp-profile-test", + ], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=env, + cwd=tmp_path, + ) + output: queue.Queue[str] = queue.Queue() + try: + assert proc.stdin is not None + assert proc.stdout is not None + stdout = proc.stdout + threading.Thread( + target=lambda: output.put(stdout.readline()), + daemon=True, + ).start() + proc.stdin.write(json.dumps({"id": 1, "command": "/tools"}) + "\n") + proc.stdin.flush() + try: + line = output.get(timeout=10) + except queue.Empty: + pytest.fail("slash worker produced no /tools response within 10 seconds") + response = json.loads(line) + assert response["ok"] is True + assert "mcp__profileprobe__hermes_61922_profile_probe" in response["output"] + finally: + proc.terminate() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait(timeout=5) -
tui_gateway/slash_worker.py@@ -65,6 +65,27 @@ def _is_orphaned(original_ppid, parent_create_time, getppid=os.getppid) -> bool: return True +def _prepare_slash_worker_runtime() -> None: + """Start bounded MCP discovery before HermesCLI snapshots tools. + + Each slash_worker child is its own process — the parent ``hermes serve`` + discovery thread does not populate this registry (issue #61891). + """ + import logging + + from hermes_cli.mcp_startup import ( + start_background_mcp_discovery, + wait_for_mcp_discovery, + ) + + logger = logging.getLogger(__name__) + start_background_mcp_discovery( + logger=logger, + thread_name="slash-worker-mcp-discovery", + ) + wait_for_mcp_discovery() + + def _start_parent_death_watchdog(original_ppid, parent_create_time) -> None: def _loop(): while not _is_orphaned(original_ppid, parent_create_time): @@ -129,6 +150,7 @@ def main(): except psutil.Error: parent_create_time = 0.0 _start_parent_death_watchdog(orig_ppid, parent_create_time) + _prepare_slash_worker_runtime() with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): cli = HermesCLI(model=args.model or None, compact=True, resume=args.session_key, verbose=False)
-
-
#122861 fix(mcp): resolve bare uv/uvx under GUI-style PATHs (#37589)
files:
hermes_platform/resolver/known_dirs.py,tests/tools/test_mcp_tool_issue_948.py,tools/mcp_tool_config.py
4 PRs closed without merging
From the close comments: done elsewhere 2 · superseded by another PR 1 · other 1
-
#39007 fix(tui): rebuild agent tool schema when background MCP discovery completes (fixes #38945) · done elsewhere →
93d6e73 -
#42703 fix(dashboard): start MCP tool discovery for Desktop/Dashboard sessions · done elsewhere →
7396977,#44512,#42694,#42703,#43189,#40407 -
#42797 fix(dashboard): start MCP discovery thread in Dashboard mode (#42694) · superseded by another PR →
#42703,#42694 -
#61922 fix(tui): MCP discovery for slash_worker and TUI agent builds (#61891) · other →
#61949
Closed issues · 7
- Closed#37589Desktop sessions miss configured MCP tools and uvx servers can fail under macOS GUI PATH
- Closed#38271[Bug]: Manual MCP reload needed on Start
- Closed#38945Desktop/TUI sessions do not reliably expose enabled MCP tools (Todoist)
- Closed#42694[Bug]: MCP tools not discovered in Desktop/Dashboard mode — start_server() bypasses MCP discovery thread
- Closed#42726[Bug]: Desktop skips MCP tool discovery — MCP tools not available
- Closed#51587[Bug]: MCP server tools connect and are enabled, but never surface into the agent's session toolset
- Closed#61891MCP tools load in CLI sessions but not in desktop/TUI sessions (slash_worker subprocess)
How this cluster was grouped
-
Link evidence (
partly structural links): 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.09: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab