feat(mcp): add opt-out for automatic MCP reload on config change (cache-safe)

The automatic MCP reload added in #1474 watches config.yaml's mcp_servers
section every 5s and reloads on any change. Every reload rebuilds the agent
tool surface and INVALIDATES the provider prompt cache — the next message
re-sends the full input prefix, which is expensive on long-context /
high-reasoning models. When config.yaml is rewritten frequently (external
tooling, multiple Hermes instances, or a flapping MCP server that rewrites
config), this causes silent, repeated cache-breaking reloads.

Add `mcp.auto_reload_on_config_change` (default: true, backward compatible).
When set to false:
- The config change is still DETECTED (watcher keeps running).
- No automatic reload happens.
- The user is told the config changed, that new settings are NOT yet
  applied, and how to apply them on their own terms with /reload-mcp —
  including the explicit warning that /reload-mcp invalidates the prompt
  cache.

Manual /reload-mcp is unaffected and still works for users who want to
apply changes deliberately.

Tests: extend TestMCPConfigWatch with test_optout_disables_auto_reload.

Co-authored-by: Turgut Kural <turgut.kural@gmail.com>
This commit is contained in:
Turgut Kural 2026-07-19 14:11:57 +03:00 committed by Teknium
parent f46ae96963
commit 5c2d098bb0
3 changed files with 88 additions and 5 deletions

46
cli.py
View file

@ -9992,13 +9992,26 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
print(f" Error generating insights: {e}")
def _check_config_mcp_changes(self) -> None:
"""Detect mcp_servers changes in config.yaml and auto-reload MCP connections.
"""Detect mcp_servers changes in config.yaml and react.
Called from process_loop every CONFIG_WATCH_INTERVAL seconds.
Compares config.yaml mtime + mcp_servers section against the last
known state. When a change is detected, triggers _reload_mcp() and
informs the user so they know the tool list has been refreshed.
known state. When a change is detected:
* By default (``mcp.auto_reload_on_config_change: true``) it
auto-triggers ``_reload_mcp()`` and informs the user legacy
behaviour from #1474.
* When opted out (``mcp.auto_reload_on_config_change: false``) it
does NOT reload. Instead it notifies the user that the config
changed and that they can apply it with ``/reload-mcp`` while
warning that ``/reload-mcp`` rebuilds the tool surface and
**invalidates the provider prompt cache** (the next message
re-sends the full input prefix, expensive on long-context /
high-reasoning models). This stops silent cache-breaking reloads
when config.yaml is rewritten frequently by external tooling or
other Hermes instances.
"""
import yaml as _yaml
CONFIG_WATCH_INTERVAL = 5.0 # seconds between config.yaml stat() calls
@ -10042,7 +10055,34 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
if new_mcp == self._config_mcp_servers:
return # mcp_servers unchanged (some other section was edited)
# Detected a change in the mcp_servers section. By default we
# auto-reload (legacy behaviour), but if the user has opted out we
# notify instead of reloading — because every reload rebuilds the
# agent tool surface and INVALIDATES the provider prompt cache (the
# next message re-sends the full input prefix, which is expensive on
# long-context / high-reasoning models).
try:
from hermes_cli.config import load_config as _load_cfg
_cfg = _load_cfg()
_mcp = _cfg.get("mcp") if isinstance(_cfg, dict) else None
_auto = _mcp.get("auto_reload_on_config_change", True) if isinstance(_mcp, dict) else True
except Exception:
_auto = True
self._config_mcp_servers = new_mcp
if not _auto:
# Notify the user that the config changed but do NOT auto-reload.
# They can apply the new settings on their own terms with
# /reload-mcp — which we explicitly warn may invalidate the cache.
print()
print("🔄 MCP server config changed — reload skipped (auto-reload disabled).")
print(" New settings are NOT applied yet. To apply them now, run:")
print(" /reload-mcp")
print(" ⚠️ Note: /reload-mcp rebuilds the tool set and invalidates the")
print(" provider prompt cache (next message re-sends full input tokens).")
return
# Notify user and reload. Run in a separate thread with a hard
# timeout so a hung MCP server cannot block the process_loop
# indefinitely (which would freeze the entire TUI).