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

View file

@ -4,11 +4,14 @@ from pathlib import Path
from unittest.mock import MagicMock, patch
def _make_cli(tmp_path, mcp_servers=None):
def _make_cli(tmp_path, mcp_servers=None, extra_config=None):
"""Create a minimal HermesCLI instance with mocked config."""
import cli as cli_mod
obj = object.__new__(cli_mod.HermesCLI)
obj.config = {"mcp_servers": mcp_servers or {}}
cfg = {"mcp_servers": mcp_servers or {}}
if extra_config:
cfg.update(extra_config)
obj.config = cfg
obj._agent_running = False
obj._last_config_check = 0.0
obj._config_mcp_servers = mcp_servers or {}
@ -101,3 +104,35 @@ class TestMCPConfigWatch:
obj._check_config_mcp_changes() # should not raise
obj._reload_mcp.assert_not_called()
def test_optout_disables_auto_reload(self, tmp_path, capsys):
"""When mcp.auto_reload_on_config_change is False, a changed
mcp_servers section must NOT trigger an automatic reload — but the
change is still detected and the user is told how to apply it.
This protects the provider prompt cache: every automatic reload
rebuilds the agent tool surface and invalidates cached prefixes.
"""
import yaml
obj, cfg_file = _make_cli(
tmp_path,
mcp_servers={},
)
# Simulate a changed mcp_servers section
cfg_file.write_text(yaml.dump({"mcp_servers": {"github": {"url": "https://mcp.github.com"}}}))
obj._config_mtime = 0.0 # force stale mtime
# Opt out via the loaded config (the watcher reads load_config(),
# not obj.config, so we patch the loader).
mocked_cfg = {"mcp": {"auto_reload_on_config_change": False}}
with patch("hermes_cli.config.get_config_path", return_value=cfg_file), \
patch("hermes_cli.config.load_config", return_value=mocked_cfg):
obj._check_config_mcp_changes()
obj._reload_mcp.assert_not_called()
out = capsys.readouterr().out
assert "reload skipped" in out
assert "/reload-mcp" in out
assert "prompt cache" in out