fix(mcp): scope MCP status, inventory and /reload-mcp to the request profile

Two profiles configuring a same-named MCP server shared one connection and
the MCP settings panel showed another profile's runtime ("Active, N tools"
next to an empty inventory); /reload-mcp stopped every profile's servers and
rediscovered from the process-default config.

- api.profiles._set_hermes_home() is the single writer of the process-profile
  home: it records get_process_profile_home() and hands it to Hermes Agent's
  hermes_constants.pin_process_hermes_home when available, at startup and on
  switch_profile(process_wide=True), so the per-turn HERMES_HOME mirror no
  longer makes every turn look like the process profile (bare-name,
  cross-profile MCP keys) and the anchor cannot drift from HERMES_HOME.
- Run get_mcp_status/registry reads and /reload-mcp inside
  api.mcp_runtime.mcp_runtime_scope(): request profile's Hermes-home override
  and secret scope (root included), no os.environ mutation, restored on exit.
- One ledger predicate, ledger_key_serves_view() (owned via
  _server_scope_keys/key scope, or adopted via _server_tool_scopes), is shared
  by status, inventory and the reload summary. The Agent's launch-profile view
  is process-wide and name-indexed, so filter_runtime_status_to_view()
  rebuilds connected/tools/status/error from the entries serving the profile:
  the root profile never shows a routed profile's connection (nor lets its own
  parked failed-spawn task borrow a routed profile's live same-named one).
- Inventory lists only tools in the profile's own registry slot for servers it
  configures; responses carry runtime_scope.
- Fail closed when the agent's routing does not match the resolved profile:
  hide runtime data (runtime_scope "unavailable", shown as a notice in the MCP
  panel, 14 locales) and refuse /reload-mcp.
- /reload-mcp shuts down only the profile's own ledger names (parked
  included), drops its own connect cooldown/error entries so a failed spawn is
  retried (the wildcard cleared every cooldown; a scoped shutdown only clears
  the live keys it tears down), rediscovers under the profile's home and
  counts live connections only. The bare wildcard remains for agents without
  profile-scoped MCP.
- Signature introspection is centralized (accepts_keywords) and falls back to
  the bare call instead of blanking every status; ledger key helpers resolve
  to the Agent's tools.mcp_tool_scope when present; tools.mcp_tool is
  resolved by full name so stubbed agents still work.
- ARCHITECTURE.md §4.10 and docs/troubleshooting.md document the boundary.
- Tests: agent-free contract (models adoption, parked tasks, cooldowns and the
  process-wide root view) and real-Agent integration (fake stdio MCP server,
  skips without hermes-agent + mcp SDK).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
tancouandClaude Opus 5 committed 2026-09-24 09:53:02 +02:00
1 parent 2d1e6f812a
commit 90b814f673
12 files changed
+1628 -58

No files matched your search

+46
View File
@@ -475,6 +475,52 @@ pointer-removed cases to `tests/test_agent_compat.py`. The resolver is
compatibility-only: delete it, and import directly from the new homes, once the WebUI
stops supporting Agents that predate the split.
### 4.10 MCP Runtime Profile Boundary
Hermes Agent owns the in-process MCP ledger. It keys a connection by
`(profile_home_key, name)` and registers its tools in that profile's registry overlay
only when the calling task serves a *routed* profile: the context-local Hermes-home
override differs from the process home. Otherwise the connection uses the bare server
name and the global registry slot, which belong to the process profile.
- `api.profiles._set_hermes_home()` is the single writer of the process-profile home:
startup (`init_profile_state()`) and `switch_profile(process_wide=True)` both go
through it, so `get_process_profile_home()` and the Agent pin
(`hermes_constants.pin_process_hermes_home()`, when the Agent provides it) are updated
in the same step as `HERMES_HOME`. Streaming turns still mirror their profile into
`os.environ['HERMES_HOME']` for legacy readers; without the pin, that mirror makes a
turn's own profile look like the process profile, so same-named servers of different
profiles share one bare-name connection.
- `/api/mcp/servers`, `/api/mcp/tools`, `/api/notes/sources` and `/reload-mcp` run
their Agent calls inside `api.mcp_runtime.mcp_runtime_scope()`, which binds the
request profile's home and secret scope (root profile included) without touching
`os.environ`. Status, tool count and inventory are read from one scope; the inventory
lists only tools in that profile's own registry slot for servers it configures.
- A connection *serves* a profile when that profile owns it (`_server_scope_keys`, else
the scope in the key) or adopted it (`_server_tool_scopes`, an identical shared
connection). `api.mcp_runtime.ledger_key_serves_view()` is the one predicate for
status, inventory and the reload summary. The Agent's launch-profile view (scope
`None`) is process-wide, so `filter_runtime_status_to_view()` downgrades
`get_mcp_status()` rows of servers the root profile does not serve to `configured`
rather than showing a routed profile's connection, tool count or connect error.
- The scope is trusted only when the Agent's routing decision matches the profile WebUI
resolved. When it cannot be confirmed (for example a same-profile turn's mirror on an
Agent without the pin), status and inventory withhold runtime data
(`runtime_scope: "unavailable"`, shown as a notice in the MCP panel) and
`/reload-mcp` refuses instead of resetting another owner's connection.
- `/reload-mcp` calls `shutdown_mcp_servers(scope=..., names=...)` with the profile's
own scope and the names of its live connections; `scope=None` without `names` is the
process-wide wildcard and is used only with Agents that predate profile-scoped MCP
(`runtime_scope: "legacy_process"`). A scoped shutdown only clears connect backoff for
the live keys it tears down, so WebUI also drops the profile's own cooldown/error
entries (`clear_profile_connect_cooldowns()`) before rediscovery: a server that failed
to spawn is retried by the reload, as the wildcard did, without touching another
owner's backoff.
Status and inventory stay passive: they never start or probe an MCP server. Ledger key
helpers resolve to Hermes Agent's `tools.mcp_tool_scope` when present so the key shape
has one owner; the local fallbacks only cover Agents that predate that module.
---
## 5. Frontend Architecture: Current State