diff --git a/acp_adapter/entry.py b/acp_adapter/entry.py index 55773536122a..fb9ed95450c3 100644 --- a/acp_adapter/entry.py +++ b/acp_adapter/entry.py @@ -32,6 +32,7 @@ import argparse import asyncio import logging +import os import sys from pathlib import Path from hermes_constants import get_hermes_home @@ -251,11 +252,13 @@ def main(argv: list[str] | None = None) -> None: # MCP servers dynamically via asyncio.to_thread inside the event # loop; that path is unaffected.) Moved from model_tools.py module # scope to avoid freezing the gateway's loop on lazy import (#16856). - try: - from tools.mcp_tool import discover_mcp_tools - discover_mcp_tools() - except Exception: - logger.debug("MCP tool discovery failed at ACP startup", exc_info=True) + # Metadata-only hosts can opt out of unrelated global MCP startup. + if os.environ.get("HERMES_ACP_SKIP_CONFIGURED_MCP", "").strip() != "1": + try: + from tools.mcp_tool import discover_mcp_tools + discover_mcp_tools() + except Exception: + logger.debug("MCP tool discovery failed at ACP startup", exc_info=True) agent = HermesACPAgent() try: diff --git a/tests/acp/test_entry.py b/tests/acp/test_entry.py index 1d881565bd90..7ceee3660624 100644 --- a/tests/acp/test_entry.py +++ b/tests/acp/test_entry.py @@ -23,6 +23,50 @@ async def fake_run_agent(agent, **kwargs): assert calls["kwargs"]["use_unstable_protocol"] is True +def test_main_skips_configured_mcp_discovery_when_requested(monkeypatch): + discovery_calls = [] + + async def fake_run_agent(agent, **kwargs): + pass + + monkeypatch.setattr(entry, "_setup_logging", lambda: None) + monkeypatch.setattr(entry, "_load_env", lambda: None) + monkeypatch.setenv("HERMES_ACP_SKIP_CONFIGURED_MCP", "1") + monkeypatch.setattr( + "tools.mcp_tool.discover_mcp_tools", + lambda: discovery_calls.append(True), + ) + monkeypatch.setattr(acp, "run_agent", fake_run_agent) + + entry.main([]) + + assert discovery_calls == [] + + +@pytest.mark.parametrize("skip_value", [None, "", "0", "false"]) +def test_main_discovers_configured_mcp_when_skip_is_not_enabled(monkeypatch, skip_value): + discovery_calls = [] + + async def fake_run_agent(agent, **kwargs): + pass + + monkeypatch.setattr(entry, "_setup_logging", lambda: None) + monkeypatch.setattr(entry, "_load_env", lambda: None) + if skip_value is None: + monkeypatch.delenv("HERMES_ACP_SKIP_CONFIGURED_MCP", raising=False) + else: + monkeypatch.setenv("HERMES_ACP_SKIP_CONFIGURED_MCP", skip_value) + monkeypatch.setattr( + "tools.mcp_tool.discover_mcp_tools", + lambda: discovery_calls.append(True), + ) + monkeypatch.setattr(acp, "run_agent", fake_run_agent) + + entry.main([]) + + assert discovery_calls == [True] + + def test_main_version_prints_without_starting_server(monkeypatch, capsys): monkeypatch.setattr(entry, "_setup_logging", lambda: (_ for _ in ()).throw(AssertionError("started server"))) diff --git a/website/docs/reference/environment-variables.md b/website/docs/reference/environment-variables.md index dfd122d68904..2b293d60dd7b 100644 --- a/website/docs/reference/environment-variables.md +++ b/website/docs/reference/environment-variables.md @@ -731,6 +731,7 @@ Advanced per-platform knobs for throttling the outbound message batcher. Most us | `HERMES_QUIET` | Suppress non-essential output (`true`/`false`) | | `CODEX_HOME` | When [Codex app-server runtime](../user-guide/features/codex-app-server-runtime) is enabled, override the directory Codex CLI reads its config + auth from (default: `~/.codex`). Hermes' migration writes the managed block to `/config.toml`. | | `HERMES_KANBAN_TASK` | Set by the kanban dispatcher when spawning a worker (task UUID). Workers and the spawned `hermes-tools` MCP subprocess inherit it so kanban tools gate correctly. Don't set manually. | +| `HERMES_ACP_SKIP_CONFIGURED_MCP` | Set by an [ACP host](../user-guide/features/acp#host-integration) on the Hermes subprocess it spawns. `1` skips starting the globally configured `config.yaml` MCP servers before the ACP JSON-RPC loop, for hosts that pass the session's MCP servers through `session/new` themselves. Servers supplied by the ACP session are still registered; any other value keeps the default. Don't set manually. | | `HERMES_API_TIMEOUT` | LLM API call timeout in seconds (default: `1800`) | | `HERMES_API_CALL_STALE_TIMEOUT` | Non-streaming stale-call timeout in seconds (default: `90`). Auto-disabled for local providers when left unset, and may scale upward for very large contexts. Also configurable via `providers..stale_timeout_seconds` or `providers..models..stale_timeout_seconds` in `config.yaml`. | | `HERMES_STREAM_READ_TIMEOUT` | Streaming socket read timeout in seconds (default: `120`). Auto-increased to `HERMES_API_TIMEOUT` for local providers. Increase if local LLMs time out during long code generation. | diff --git a/website/docs/user-guide/features/acp.md b/website/docs/user-guide/features/acp.md index ccdaabc5832d..4983465d2fd0 100644 --- a/website/docs/user-guide/features/acp.md +++ b/website/docs/user-guide/features/acp.md @@ -156,6 +156,28 @@ ACP mode uses the same Hermes configuration as the CLI: Provider resolution uses Hermes' normal runtime resolver, so ACP inherits the currently configured provider and credentials. Hermes also advertises a terminal auth method (`--setup`) for first-run ACP clients; this opens Hermes' interactive model/provider setup. +## Host integration + +These variables are set by an **ACP host process** (an editor or another agent +harness) on the Hermes subprocess it spawns. They are not user configuration — +do not set them by hand in `.env` or `config.yaml`. + +| Variable | Value | Effect | +|----------|-------|--------| +| `HERMES_ACP_SKIP_CONFIGURED_MCP` | `1` | Skip starting the **globally configured** MCP servers from `config.yaml` before the ACP JSON-RPC loop begins. | + +Hermes normally starts every MCP server configured in `config.yaml` before it +enters the ACP JSON-RPC loop. A host that owns MCP itself — passing the +session's servers explicitly through `session/new` — does not need that global +startup, and an unrelated slow or interactive MCP server would otherwise delay +`initialize`. Setting the marker to exactly `1` lets such a host skip it. + +Only the global `config.yaml` discovery is skipped. **MCP servers supplied by +the ACP session through `session/new` are still registered**, so a host loses +no capability it asked for. Any other value (unset, empty, `0`, `false`) keeps +the default behavior, so an unrelated truthy-looking string cannot silently +disable MCP. + ## Session behavior ACP sessions are tracked by the ACP adapter's in-memory session manager while the server is running.