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 <noreply@anthropic.com>
This commit is contained in:
1 parent
90b814f673
commit
dbf0179c53
4 files changed
+25
-5
No files matched your search
+2
-1
@@ -29835,7 +29835,7 @@ def _handle_notes_sources_list(handler):
|
|||||||
servers = cfg.get("mcp_servers", {})
|
servers = cfg.get("mcp_servers", {})
|
||||||
if not isinstance(servers, dict):
|
if not isinstance(servers, dict):
|
||||||
servers = {}
|
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"
|
servers, "/api/notes/sources"
|
||||||
)
|
)
|
||||||
return j(handler, {
|
return j(handler, {
|
||||||
@@ -29843,6 +29843,7 @@ def _handle_notes_sources_list(handler):
|
|||||||
"sources": _notes_sources_from_mcp_inventory(server_summaries, tools),
|
"sources": _notes_sources_from_mcp_inventory(server_summaries, tools),
|
||||||
"source": source,
|
"source": source,
|
||||||
"inventory_scope": "already_known_runtime_only",
|
"inventory_scope": "already_known_runtime_only",
|
||||||
|
"runtime_scope": runtime_scope,
|
||||||
"attach_supported": False,
|
"attach_supported": False,
|
||||||
"automatic_recall_unchanged": True,
|
"automatic_recall_unchanged": True,
|
||||||
"recent_ai_notes": _joplin_recent_ai_notes(limit=6),
|
"recent_ai_notes": _joplin_recent_ai_notes(limit=6),
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ Interpret the two together:
|
|||||||
|
|
||||||
## MCP panel shows another profile's servers, or "Live status for this profile is unavailable"
|
## 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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -5415,8 +5415,13 @@ function _renderExternalNotesSources() {
|
|||||||
const recall = data.automatic_recall_unchanged !== false
|
const recall = data.automatic_recall_unchanged !== false
|
||||||
? `<div class="memory-detail-mtime">${esc(t('external_notes_auto_recall_hint'))}</div>`
|
? `<div class="memory-detail-mtime">${esc(t('external_notes_auto_recall_hint'))}</div>`
|
||||||
: '';
|
: '';
|
||||||
|
// 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'
|
||||||
|
? `<div class="memory-detail-mtime">${esc(t('mcp_runtime_scope_unavailable'))}</div>`
|
||||||
|
: '';
|
||||||
if (!sources.length) {
|
if (!sources.length) {
|
||||||
body.innerHTML = `<div class="main-view-content">${recall}<div class="memory-empty">${esc(t('external_notes_empty'))}</div></div>`;
|
body.innerHTML = `<div class="main-view-content">${recall}${scopeNotice}<div class="memory-empty">${esc(t('external_notes_empty'))}</div></div>`;
|
||||||
} else {
|
} else {
|
||||||
const selected = sources.find(src => (src.name || '').toLowerCase() === (_notesSelectedSource || '').toLowerCase()) || sources[0];
|
const selected = sources.find(src => (src.name || '').toLowerCase() === (_notesSelectedSource || '').toLowerCase()) || sources[0];
|
||||||
_notesSelectedSource = (selected && selected.name) || 'joplin';
|
_notesSelectedSource = (selected && selected.name) || 'joplin';
|
||||||
@@ -5463,7 +5468,7 @@ function _renderExternalNotesSources() {
|
|||||||
${searchError}
|
${searchError}
|
||||||
${resultHtml}
|
${resultHtml}
|
||||||
</section>`;
|
</section>`;
|
||||||
body.innerHTML = `<div class="main-view-content">${recall}${recentAiHtml}${searchUi}${previewHtml}${cards}</div>`;
|
body.innerHTML = `<div class="main-view-content">${recall}${scopeNotice}${recentAiHtml}${searchUi}${previewHtml}${cards}</div>`;
|
||||||
}
|
}
|
||||||
body.style.display = '';
|
body.style.display = '';
|
||||||
if (empty) empty.style.display = 'none';
|
if (empty) empty.style.display = 'none';
|
||||||
|
|||||||
@@ -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):
|
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)
|
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)
|
agent.connect("atlassian", READ, scope=None)
|
||||||
monkeypatch.setenv("HERMES_HOME", str(agent.base / "profiles" / "profile-write")) # turn mirror
|
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]["status"] == "configured"
|
||||||
assert servers["servers"][0]["tool_count"] is None
|
assert servers["servers"][0]["tool_count"] is None
|
||||||
assert _call("profile-write", _handle_mcp_tools_list)["total"] == 0
|
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"):
|
with pytest.raises(RuntimeError, match="could not be confirmed"):
|
||||||
_reload("profile-write")
|
_reload("profile-write")
|
||||||
assert not [c for c in agent.calls if c[0] in ("discover", "shutdown")]
|
assert not [c for c in agent.calls if c[0] in ("discover", "shutdown")]
|
||||||
|
|||||||
Reference in new issue
Block a user