Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions acp_adapter/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
44 changes: 44 additions & 0 deletions tests/acp/test_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")))

Expand Down
1 change: 1 addition & 0 deletions website/docs/reference/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<CODEX_HOME>/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.<id>.stale_timeout_seconds` or `providers.<id>.models.<model>.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. |
Expand Down
22 changes: 22 additions & 0 deletions website/docs/user-guide/features/acp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading