From dbf0179c537012f3f769e46e612550fa09de7610 Mon Sep 17 00:00:00 2001 From: tancou Date: Tue, 22 Sep 2026 14:52:36 +0200 Subject: [PATCH] fix(mcp): report runtime_scope on /api/notes/sources and surface it in the Notes drawer Review follow-up on #7720 (Greptile P2): the external Notes drawer reads the same profile-bound MCP inventory as the MCP panel but dropped the ``runtime_scope`` label, so a profile whose runtime scope could not be confirmed rendered its note sources as inactive/configured with no explanation. ``/api/notes/sources`` now returns ``runtime_scope`` and the drawer shows the existing ``mcp_runtime_scope_unavailable`` notice (same key, no new locale strings) when it is ``unavailable``. Tests: the skewed-routing unit test now also asserts the notes response carries ``runtime_scope: "unavailable"`` with its sources inactive. docs/troubleshooting.md mentions the drawer next to the panel. Co-Authored-By: Claude Fable 5.1 --- api/routes.py | 3 ++- docs/troubleshooting.md | 2 +- static/panels.js | 9 +++++++-- tests/test_mcp_runtime_profile_scope_unit.py | 16 +++++++++++++++- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/api/routes.py b/api/routes.py index 127ad108..cc9ff399 100644 --- a/api/routes.py +++ b/api/routes.py @@ -29835,7 +29835,7 @@ def _handle_notes_sources_list(handler): servers = cfg.get("mcp_servers", {}) if not isinstance(servers, dict): servers = {} - server_summaries, tools, source, _runtime_scope = _mcp_profile_runtime_inventory( + server_summaries, tools, source, runtime_scope = _mcp_profile_runtime_inventory( servers, "/api/notes/sources" ) return j(handler, { @@ -29843,6 +29843,7 @@ def _handle_notes_sources_list(handler): "sources": _notes_sources_from_mcp_inventory(server_summaries, tools), "source": source, "inventory_scope": "already_known_runtime_only", + "runtime_scope": runtime_scope, "attach_supported": False, "automatic_recall_unchanged": True, "recent_ai_notes": _joplin_recent_ai_notes(limit=6), diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 36fc8d56..33c4c0ca 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -311,7 +311,7 @@ Interpret the two together: ## MCP panel shows another profile's servers, or "Live status for this profile is unavailable" -**Symptom.** With several profiles, the MCP settings panel of profile A shows a server as *Active* with a tool count while the tool inventory is empty (or lists profile B's tools); `/reload-mcp` on one profile stops the other profile's servers; or the panel shows the notice *"Live status for this profile is unavailable right now"* and `/reload-mcp` answers *"MCP runtime scope could not be confirmed"*. +**Symptom.** With several profiles, the MCP settings panel of profile A shows a server as *Active* with a tool count while the tool inventory is empty (or lists profile B's tools); `/reload-mcp` on one profile stops the other profile's servers; or the MCP panel and the external Notes drawer show the notice *"Live status for this profile is unavailable right now"* and `/reload-mcp` answers *"MCP runtime scope could not be confirmed"*. **Why.** Hermes Agent keeps one in-process MCP ledger per WebUI process and keys a connection by profile only when it can tell the request serves a profile other than the process's own. The WebUI binds every MCP status read and `/reload-mcp` to the request profile (`ARCHITECTURE.md` ยง4.10). While a chat turn is streaming, the WebUI mirrors that turn's profile into `HERMES_HOME`; Agents that predate `hermes_constants.pin_process_hermes_home` cannot distinguish that mirror from the process profile, so the WebUI withholds runtime data and refuses the reload instead of showing or resetting another profile's connection. diff --git a/static/panels.js b/static/panels.js index c10ea35b..93f28af5 100644 --- a/static/panels.js +++ b/static/panels.js @@ -5415,8 +5415,13 @@ function _renderExternalNotesSources() { const recall = data.automatic_recall_unchanged !== false ? `
${esc(t('external_notes_auto_recall_hint'))}
` : ''; + // Same withheld-runtime state as the MCP panel: sources still list, but their + // live status/tools are hidden until the profile's runtime scope is confirmed. + const scopeNotice = data.runtime_scope === 'unavailable' + ? `
${esc(t('mcp_runtime_scope_unavailable'))}
` + : ''; if (!sources.length) { - body.innerHTML = `
${recall}
${esc(t('external_notes_empty'))}
`; + body.innerHTML = `
${recall}${scopeNotice}
${esc(t('external_notes_empty'))}
`; } else { const selected = sources.find(src => (src.name || '').toLowerCase() === (_notesSelectedSource || '').toLowerCase()) || sources[0]; _notesSelectedSource = (selected && selected.name) || 'joplin'; @@ -5463,7 +5468,7 @@ function _renderExternalNotesSources() { ${searchError} ${resultHtml} `; - body.innerHTML = `
${recall}${recentAiHtml}${searchUi}${previewHtml}${cards}
`; + body.innerHTML = `
${recall}${scopeNotice}${recentAiHtml}${searchUi}${previewHtml}${cards}
`; } body.style.display = ''; if (empty) empty.style.display = 'none'; diff --git a/tests/test_mcp_runtime_profile_scope_unit.py b/tests/test_mcp_runtime_profile_scope_unit.py index 0993cc28..d1887dc1 100644 --- a/tests/test_mcp_runtime_profile_scope_unit.py +++ b/tests/test_mcp_runtime_profile_scope_unit.py @@ -423,9 +423,17 @@ def test_adopted_connection_serves_status_inventory_and_reload(monkeypatch, tmp_ def test_skewed_routing_hides_runtime_and_refuses_reload(monkeypatch, tmp_path): - from api.routes import _handle_mcp_servers_list, _handle_mcp_tools_list + from api.routes import ( + _handle_mcp_servers_list, + _handle_mcp_tools_list, + _handle_notes_sources_list, + ) agent = _install(monkeypatch, tmp_path, pin_supported=False) + _configure(agent, "profile-write", { + "atlassian": {"command": "fake", "fake_tools": WRITE}, + "joplin": {"command": "fake", "fake_tools": []}, # a notes-drawer source + }) agent.connect("atlassian", READ, scope=None) monkeypatch.setenv("HERMES_HOME", str(agent.base / "profiles" / "profile-write")) # turn mirror @@ -434,6 +442,12 @@ def test_skewed_routing_hides_runtime_and_refuses_reload(monkeypatch, tmp_path): assert servers["servers"][0]["status"] == "configured" assert servers["servers"][0]["tool_count"] is None assert _call("profile-write", _handle_mcp_tools_list)["total"] == 0 + # The notes drawer reads the same inventory and must be told the runtime is withheld. + monkeypatch.setenv("HERMES_WEBUI_EXTERNAL_NOTES_SOURCES", "1") + notes = _call("profile-write", _handle_notes_sources_list) + assert notes["runtime_scope"] == "unavailable" + assert [s["name"] for s in notes["sources"]] == ["joplin"] + assert notes["sources"][0]["active"] is False with pytest.raises(RuntimeError, match="could not be confirmed"): _reload("profile-write") assert not [c for c in agent.calls if c[0] in ("discover", "shutdown")]