mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix: MCP toolset resolution for runtime and config (#3252)
Gateway sessions had their own inline toolset resolution that only read platform_toolsets from config, which never includes MCP server names. MCP tools were discovered and registered but invisible to the model. - Replace duplicated gateway toolset resolution in _run_agent() and _run_background_task() with calls to the shared _get_platform_tools() - Extend _get_platform_tools() to include globally enabled MCP servers at runtime (include_default_mcp_servers=True), while config-editing flows use include_default_mcp_servers=False to avoid persisting implicit MCP defaults into platform_toolsets - Add homeassistant to PLATFORMS dict (was missing, caused KeyError) - Fix CLI entry point to use _get_platform_tools() as well, so MCP tools are visible in CLI mode too - Remove redundant platform_key reassignment in _run_background_task Co-authored-by: kshitijk4poor <kshitijk4poor@users.noreply.github.com>
This commit is contained in:
parent
2c719f0701
commit
62f8aa9b03
6 changed files with 262 additions and 132 deletions
9
cli.py
9
cli.py
|
|
@ -7288,12 +7288,9 @@ def main(
|
||||||
else:
|
else:
|
||||||
toolsets_list.append(str(t))
|
toolsets_list.append(str(t))
|
||||||
else:
|
else:
|
||||||
# Check config for CLI toolsets, fallback to hermes-cli
|
# Use the shared resolver so MCP servers are included at runtime
|
||||||
config_cli_toolsets = CLI_CONFIG.get("platform_toolsets", {}).get("cli")
|
from hermes_cli.tools_config import _get_platform_tools
|
||||||
if config_cli_toolsets and isinstance(config_cli_toolsets, list):
|
toolsets_list = sorted(_get_platform_tools(CLI_CONFIG, "cli"))
|
||||||
toolsets_list = config_cli_toolsets
|
|
||||||
else:
|
|
||||||
toolsets_list = ["hermes-cli"]
|
|
||||||
|
|
||||||
parsed_skills = _parse_skills_argument(skills)
|
parsed_skills = _parse_skills_argument(skills)
|
||||||
|
|
||||||
|
|
|
||||||
152
gateway/run.py
152
gateway/run.py
|
|
@ -257,7 +257,25 @@ def _resolve_runtime_agent_kwargs() -> dict:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _resolve_gateway_model() -> str:
|
def _platform_config_key(platform: "Platform") -> str:
|
||||||
|
"""Map a Platform enum to its config.yaml key (LOCAL→"cli", rest→enum value)."""
|
||||||
|
return "cli" if platform == Platform.LOCAL else platform.value
|
||||||
|
|
||||||
|
|
||||||
|
def _load_gateway_config() -> dict:
|
||||||
|
"""Load and parse ~/.hermes/config.yaml, returning {} on any error."""
|
||||||
|
try:
|
||||||
|
config_path = _hermes_home / 'config.yaml'
|
||||||
|
if config_path.exists():
|
||||||
|
import yaml
|
||||||
|
with open(config_path, 'r', encoding='utf-8') as f:
|
||||||
|
return yaml.safe_load(f) or {}
|
||||||
|
except Exception:
|
||||||
|
logger.debug("Could not load gateway config from %s", _hermes_home / 'config.yaml')
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_gateway_model(config: dict | None = None) -> str:
|
||||||
"""Read model from env/config — mirrors the resolution in _run_agent_sync.
|
"""Read model from env/config — mirrors the resolution in _run_agent_sync.
|
||||||
|
|
||||||
Without this, temporary AIAgent instances (memory flush, /compress) fall
|
Without this, temporary AIAgent instances (memory flush, /compress) fall
|
||||||
|
|
@ -265,19 +283,12 @@ def _resolve_gateway_model() -> str:
|
||||||
when the active provider is openai-codex.
|
when the active provider is openai-codex.
|
||||||
"""
|
"""
|
||||||
model = os.getenv("HERMES_MODEL") or os.getenv("LLM_MODEL") or "anthropic/claude-opus-4.6"
|
model = os.getenv("HERMES_MODEL") or os.getenv("LLM_MODEL") or "anthropic/claude-opus-4.6"
|
||||||
try:
|
cfg = config if config is not None else _load_gateway_config()
|
||||||
import yaml as _y
|
model_cfg = cfg.get("model", {})
|
||||||
_cfg_path = _hermes_home / "config.yaml"
|
if isinstance(model_cfg, str):
|
||||||
if _cfg_path.exists():
|
model = model_cfg
|
||||||
with open(_cfg_path, encoding="utf-8") as _f:
|
elif isinstance(model_cfg, dict):
|
||||||
_cfg = _y.safe_load(_f) or {}
|
model = model_cfg.get("default", model)
|
||||||
_model_cfg = _cfg.get("model", {})
|
|
||||||
if isinstance(_model_cfg, str):
|
|
||||||
model = _model_cfg
|
|
||||||
elif isinstance(_model_cfg, dict):
|
|
||||||
model = _model_cfg.get("default", model)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -3571,52 +3582,12 @@ class GatewayRunner:
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Read model from config via shared helper
|
user_config = _load_gateway_config()
|
||||||
model = _resolve_gateway_model()
|
model = _resolve_gateway_model(user_config)
|
||||||
|
platform_key = _platform_config_key(source.platform)
|
||||||
|
|
||||||
# Determine toolset (same logic as _run_agent)
|
from hermes_cli.tools_config import _get_platform_tools
|
||||||
default_toolset_map = {
|
enabled_toolsets = sorted(_get_platform_tools(user_config, platform_key))
|
||||||
Platform.LOCAL: "hermes-cli",
|
|
||||||
Platform.TELEGRAM: "hermes-telegram",
|
|
||||||
Platform.DISCORD: "hermes-discord",
|
|
||||||
Platform.WHATSAPP: "hermes-whatsapp",
|
|
||||||
Platform.SLACK: "hermes-slack",
|
|
||||||
Platform.SIGNAL: "hermes-signal",
|
|
||||||
Platform.HOMEASSISTANT: "hermes-homeassistant",
|
|
||||||
Platform.EMAIL: "hermes-email",
|
|
||||||
Platform.DINGTALK: "hermes-dingtalk",
|
|
||||||
}
|
|
||||||
platform_toolsets_config = {}
|
|
||||||
try:
|
|
||||||
config_path = _hermes_home / 'config.yaml'
|
|
||||||
if config_path.exists():
|
|
||||||
import yaml
|
|
||||||
with open(config_path, 'r', encoding="utf-8") as f:
|
|
||||||
user_config = yaml.safe_load(f) or {}
|
|
||||||
platform_toolsets_config = user_config.get("platform_toolsets", {})
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
platform_config_key = {
|
|
||||||
Platform.LOCAL: "cli",
|
|
||||||
Platform.TELEGRAM: "telegram",
|
|
||||||
Platform.DISCORD: "discord",
|
|
||||||
Platform.WHATSAPP: "whatsapp",
|
|
||||||
Platform.SLACK: "slack",
|
|
||||||
Platform.SIGNAL: "signal",
|
|
||||||
Platform.HOMEASSISTANT: "homeassistant",
|
|
||||||
Platform.EMAIL: "email",
|
|
||||||
Platform.DINGTALK: "dingtalk",
|
|
||||||
}.get(source.platform, "telegram")
|
|
||||||
|
|
||||||
config_toolsets = platform_toolsets_config.get(platform_config_key)
|
|
||||||
if config_toolsets and isinstance(config_toolsets, list):
|
|
||||||
enabled_toolsets = config_toolsets
|
|
||||||
else:
|
|
||||||
default_toolset = default_toolset_map.get(source.platform, "hermes-telegram")
|
|
||||||
enabled_toolsets = [default_toolset]
|
|
||||||
|
|
||||||
platform_key = "cli" if source.platform == Platform.LOCAL else source.platform.value
|
|
||||||
|
|
||||||
pr = self._provider_routing
|
pr = self._provider_routing
|
||||||
max_iterations = int(os.getenv("HERMES_MAX_ITERATIONS", "90"))
|
max_iterations = int(os.getenv("HERMES_MAX_ITERATIONS", "90"))
|
||||||
|
|
@ -4742,67 +4713,16 @@ class GatewayRunner:
|
||||||
from run_agent import AIAgent
|
from run_agent import AIAgent
|
||||||
import queue
|
import queue
|
||||||
|
|
||||||
# Determine toolset based on platform.
|
user_config = _load_gateway_config()
|
||||||
# Check config.yaml for per-platform overrides, fallback to hardcoded defaults.
|
platform_key = _platform_config_key(source.platform)
|
||||||
default_toolset_map = {
|
|
||||||
Platform.LOCAL: "hermes-cli",
|
|
||||||
Platform.TELEGRAM: "hermes-telegram",
|
|
||||||
Platform.DISCORD: "hermes-discord",
|
|
||||||
Platform.WHATSAPP: "hermes-whatsapp",
|
|
||||||
Platform.SLACK: "hermes-slack",
|
|
||||||
Platform.SIGNAL: "hermes-signal",
|
|
||||||
Platform.HOMEASSISTANT: "hermes-homeassistant",
|
|
||||||
Platform.EMAIL: "hermes-email",
|
|
||||||
Platform.DINGTALK: "hermes-dingtalk",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Try to load platform_toolsets from config
|
from hermes_cli.tools_config import _get_platform_tools
|
||||||
platform_toolsets_config = {}
|
enabled_toolsets = sorted(_get_platform_tools(user_config, platform_key))
|
||||||
try:
|
|
||||||
config_path = _hermes_home / 'config.yaml'
|
|
||||||
if config_path.exists():
|
|
||||||
import yaml
|
|
||||||
with open(config_path, 'r', encoding="utf-8") as f:
|
|
||||||
user_config = yaml.safe_load(f) or {}
|
|
||||||
platform_toolsets_config = user_config.get("platform_toolsets", {})
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug("Could not load platform_toolsets config: %s", e)
|
|
||||||
|
|
||||||
# Map platform enum to config key
|
|
||||||
platform_config_key = {
|
|
||||||
Platform.LOCAL: "cli",
|
|
||||||
Platform.TELEGRAM: "telegram",
|
|
||||||
Platform.DISCORD: "discord",
|
|
||||||
Platform.WHATSAPP: "whatsapp",
|
|
||||||
Platform.SLACK: "slack",
|
|
||||||
Platform.SIGNAL: "signal",
|
|
||||||
Platform.HOMEASSISTANT: "homeassistant",
|
|
||||||
Platform.EMAIL: "email",
|
|
||||||
Platform.DINGTALK: "dingtalk",
|
|
||||||
}.get(source.platform, "telegram")
|
|
||||||
|
|
||||||
# Use config override if present (list of toolsets), otherwise hardcoded default
|
|
||||||
config_toolsets = platform_toolsets_config.get(platform_config_key)
|
|
||||||
if config_toolsets and isinstance(config_toolsets, list):
|
|
||||||
enabled_toolsets = config_toolsets
|
|
||||||
else:
|
|
||||||
default_toolset = default_toolset_map.get(source.platform, "hermes-telegram")
|
|
||||||
enabled_toolsets = [default_toolset]
|
|
||||||
|
|
||||||
# Tool progress mode from config.yaml: "all", "new", "verbose", "off"
|
# Tool progress mode from config.yaml: "all", "new", "verbose", "off"
|
||||||
# Falls back to env vars for backward compatibility
|
# Falls back to env vars for backward compatibility
|
||||||
_progress_cfg = {}
|
|
||||||
try:
|
|
||||||
_tp_cfg_path = _hermes_home / "config.yaml"
|
|
||||||
if _tp_cfg_path.exists():
|
|
||||||
import yaml as _tp_yaml
|
|
||||||
with open(_tp_cfg_path, encoding="utf-8") as _tp_f:
|
|
||||||
_tp_data = _tp_yaml.safe_load(_tp_f) or {}
|
|
||||||
_progress_cfg = _tp_data.get("display", {})
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
progress_mode = (
|
progress_mode = (
|
||||||
_progress_cfg.get("tool_progress")
|
user_config.get("display", {}).get("tool_progress")
|
||||||
or os.getenv("HERMES_TOOL_PROGRESS_MODE")
|
or os.getenv("HERMES_TOOL_PROGRESS_MODE")
|
||||||
or "all"
|
or "all"
|
||||||
)
|
)
|
||||||
|
|
@ -5025,7 +4945,7 @@ class GatewayRunner:
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
model = _resolve_gateway_model()
|
model = _resolve_gateway_model(user_config)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
runtime_kwargs = _resolve_runtime_agent_kwargs()
|
runtime_kwargs = _resolve_runtime_agent_kwargs()
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,7 @@ PLATFORMS = {
|
||||||
"slack": {"label": "💼 Slack", "default_toolset": "hermes-slack"},
|
"slack": {"label": "💼 Slack", "default_toolset": "hermes-slack"},
|
||||||
"whatsapp": {"label": "📱 WhatsApp", "default_toolset": "hermes-whatsapp"},
|
"whatsapp": {"label": "📱 WhatsApp", "default_toolset": "hermes-whatsapp"},
|
||||||
"signal": {"label": "📡 Signal", "default_toolset": "hermes-signal"},
|
"signal": {"label": "📡 Signal", "default_toolset": "hermes-signal"},
|
||||||
|
"homeassistant": {"label": "🏠 Home Assistant", "default_toolset": "hermes-homeassistant"},
|
||||||
"email": {"label": "📧 Email", "default_toolset": "hermes-email"},
|
"email": {"label": "📧 Email", "default_toolset": "hermes-email"},
|
||||||
"dingtalk": {"label": "💬 DingTalk", "default_toolset": "hermes-dingtalk"},
|
"dingtalk": {"label": "💬 DingTalk", "default_toolset": "hermes-dingtalk"},
|
||||||
}
|
}
|
||||||
|
|
@ -378,7 +379,29 @@ def _platform_toolset_summary(config: dict, platforms: Optional[List[str]] = Non
|
||||||
return summary
|
return summary
|
||||||
|
|
||||||
|
|
||||||
def _get_platform_tools(config: dict, platform: str) -> Set[str]:
|
def _parse_enabled_flag(value, default: bool = True) -> bool:
|
||||||
|
"""Parse bool-like config values used by tool/platform settings."""
|
||||||
|
if value is None:
|
||||||
|
return default
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
if isinstance(value, int):
|
||||||
|
return value != 0
|
||||||
|
if isinstance(value, str):
|
||||||
|
lowered = value.strip().lower()
|
||||||
|
if lowered in {"true", "1", "yes", "on"}:
|
||||||
|
return True
|
||||||
|
if lowered in {"false", "0", "no", "off"}:
|
||||||
|
return False
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _get_platform_tools(
|
||||||
|
config: dict,
|
||||||
|
platform: str,
|
||||||
|
*,
|
||||||
|
include_default_mcp_servers: bool = True,
|
||||||
|
) -> Set[str]:
|
||||||
"""Resolve which individual toolset names are enabled for a platform."""
|
"""Resolve which individual toolset names are enabled for a platform."""
|
||||||
from toolsets import resolve_toolset
|
from toolsets import resolve_toolset
|
||||||
|
|
||||||
|
|
@ -430,6 +453,37 @@ def _get_platform_tools(config: dict, platform: str) -> Set[str]:
|
||||||
enabled_toolsets.add(pts)
|
enabled_toolsets.add(pts)
|
||||||
# else: known but not in config = user disabled it
|
# else: known but not in config = user disabled it
|
||||||
|
|
||||||
|
# Preserve any explicit non-configurable toolset entries (for example,
|
||||||
|
# custom toolsets or MCP server names saved in platform_toolsets).
|
||||||
|
platform_default_keys = {p["default_toolset"] for p in PLATFORMS.values()}
|
||||||
|
explicit_passthrough = {
|
||||||
|
ts
|
||||||
|
for ts in toolset_names
|
||||||
|
if ts not in configurable_keys
|
||||||
|
and ts not in plugin_ts_keys
|
||||||
|
and ts not in platform_default_keys
|
||||||
|
}
|
||||||
|
|
||||||
|
# MCP servers are expected to be available on all platforms by default.
|
||||||
|
# If the platform explicitly lists one or more MCP server names, treat that
|
||||||
|
# as an allowlist. Otherwise include every globally enabled MCP server.
|
||||||
|
mcp_servers = config.get("mcp_servers", {})
|
||||||
|
enabled_mcp_servers = {
|
||||||
|
name
|
||||||
|
for name, server_cfg in mcp_servers.items()
|
||||||
|
if isinstance(server_cfg, dict)
|
||||||
|
and _parse_enabled_flag(server_cfg.get("enabled", True), default=True)
|
||||||
|
}
|
||||||
|
explicit_mcp_servers = explicit_passthrough & enabled_mcp_servers
|
||||||
|
enabled_toolsets.update(explicit_passthrough - enabled_mcp_servers)
|
||||||
|
if include_default_mcp_servers:
|
||||||
|
if explicit_mcp_servers:
|
||||||
|
enabled_toolsets.update(explicit_mcp_servers)
|
||||||
|
else:
|
||||||
|
enabled_toolsets.update(enabled_mcp_servers)
|
||||||
|
else:
|
||||||
|
enabled_toolsets.update(explicit_mcp_servers)
|
||||||
|
|
||||||
return enabled_toolsets
|
return enabled_toolsets
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1022,7 +1076,7 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
|
||||||
if first_install:
|
if first_install:
|
||||||
for pkey in enabled_platforms:
|
for pkey in enabled_platforms:
|
||||||
pinfo = PLATFORMS[pkey]
|
pinfo = PLATFORMS[pkey]
|
||||||
current_enabled = _get_platform_tools(config, pkey)
|
current_enabled = _get_platform_tools(config, pkey, include_default_mcp_servers=False)
|
||||||
|
|
||||||
# Uncheck toolsets that should be off by default
|
# Uncheck toolsets that should be off by default
|
||||||
checklist_preselected = current_enabled - _DEFAULT_OFF_TOOLSETS
|
checklist_preselected = current_enabled - _DEFAULT_OFF_TOOLSETS
|
||||||
|
|
@ -1074,7 +1128,7 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
|
||||||
platform_keys = []
|
platform_keys = []
|
||||||
for pkey in enabled_platforms:
|
for pkey in enabled_platforms:
|
||||||
pinfo = PLATFORMS[pkey]
|
pinfo = PLATFORMS[pkey]
|
||||||
current = _get_platform_tools(config, pkey)
|
current = _get_platform_tools(config, pkey, include_default_mcp_servers=False)
|
||||||
count = len(current)
|
count = len(current)
|
||||||
total = len(_get_effective_configurable_toolsets())
|
total = len(_get_effective_configurable_toolsets())
|
||||||
platform_choices.append(f"Configure {pinfo['label']} ({count}/{total} enabled)")
|
platform_choices.append(f"Configure {pinfo['label']} ({count}/{total} enabled)")
|
||||||
|
|
@ -1121,11 +1175,11 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
|
||||||
# Use the union of all platforms' current tools as the starting state
|
# Use the union of all platforms' current tools as the starting state
|
||||||
all_current = set()
|
all_current = set()
|
||||||
for pk in platform_keys:
|
for pk in platform_keys:
|
||||||
all_current |= _get_platform_tools(config, pk)
|
all_current |= _get_platform_tools(config, pk, include_default_mcp_servers=False)
|
||||||
new_enabled = _prompt_toolset_checklist("All platforms", all_current)
|
new_enabled = _prompt_toolset_checklist("All platforms", all_current)
|
||||||
if new_enabled != all_current:
|
if new_enabled != all_current:
|
||||||
for pk in platform_keys:
|
for pk in platform_keys:
|
||||||
prev = _get_platform_tools(config, pk)
|
prev = _get_platform_tools(config, pk, include_default_mcp_servers=False)
|
||||||
added = new_enabled - prev
|
added = new_enabled - prev
|
||||||
removed = prev - new_enabled
|
removed = prev - new_enabled
|
||||||
pinfo_inner = PLATFORMS[pk]
|
pinfo_inner = PLATFORMS[pk]
|
||||||
|
|
@ -1147,7 +1201,7 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
|
||||||
print(color(" ✓ Saved configuration for all platforms", Colors.GREEN))
|
print(color(" ✓ Saved configuration for all platforms", Colors.GREEN))
|
||||||
# Update choice labels
|
# Update choice labels
|
||||||
for ci, pk in enumerate(platform_keys):
|
for ci, pk in enumerate(platform_keys):
|
||||||
new_count = len(_get_platform_tools(config, pk))
|
new_count = len(_get_platform_tools(config, pk, include_default_mcp_servers=False))
|
||||||
total = len(_get_effective_configurable_toolsets())
|
total = len(_get_effective_configurable_toolsets())
|
||||||
platform_choices[ci] = f"Configure {PLATFORMS[pk]['label']} ({new_count}/{total} enabled)"
|
platform_choices[ci] = f"Configure {PLATFORMS[pk]['label']} ({new_count}/{total} enabled)"
|
||||||
else:
|
else:
|
||||||
|
|
@ -1159,7 +1213,7 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
|
||||||
pinfo = PLATFORMS[pkey]
|
pinfo = PLATFORMS[pkey]
|
||||||
|
|
||||||
# Get current enabled toolsets for this platform
|
# Get current enabled toolsets for this platform
|
||||||
current_enabled = _get_platform_tools(config, pkey)
|
current_enabled = _get_platform_tools(config, pkey, include_default_mcp_servers=False)
|
||||||
|
|
||||||
# Show checklist
|
# Show checklist
|
||||||
new_enabled = _prompt_toolset_checklist(pinfo["label"], current_enabled)
|
new_enabled = _prompt_toolset_checklist(pinfo["label"], current_enabled)
|
||||||
|
|
@ -1192,7 +1246,7 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# Update the choice label with new count
|
# Update the choice label with new count
|
||||||
new_count = len(_get_platform_tools(config, pkey))
|
new_count = len(_get_platform_tools(config, pkey, include_default_mcp_servers=False))
|
||||||
total = len(_get_effective_configurable_toolsets())
|
total = len(_get_effective_configurable_toolsets())
|
||||||
platform_choices[idx] = f"Configure {pinfo['label']} ({new_count}/{total} enabled)"
|
platform_choices[idx] = f"Configure {pinfo['label']} ({new_count}/{total} enabled)"
|
||||||
|
|
||||||
|
|
@ -1338,7 +1392,7 @@ def _configure_mcp_tools_interactive(config: dict):
|
||||||
|
|
||||||
def _apply_toolset_change(config: dict, platform: str, toolset_names: List[str], action: str):
|
def _apply_toolset_change(config: dict, platform: str, toolset_names: List[str], action: str):
|
||||||
"""Add or remove built-in toolsets for a platform."""
|
"""Add or remove built-in toolsets for a platform."""
|
||||||
enabled = _get_platform_tools(config, platform)
|
enabled = _get_platform_tools(config, platform, include_default_mcp_servers=False)
|
||||||
if action == "disable":
|
if action == "disable":
|
||||||
updated = enabled - set(toolset_names)
|
updated = enabled - set(toolset_names)
|
||||||
else:
|
else:
|
||||||
|
|
@ -1424,7 +1478,7 @@ def tools_disable_enable_command(args):
|
||||||
return
|
return
|
||||||
|
|
||||||
if action == "list":
|
if action == "list":
|
||||||
_print_tools_list(_get_platform_tools(config, platform),
|
_print_tools_list(_get_platform_tools(config, platform, include_default_mcp_servers=False),
|
||||||
config.get("mcp_servers") or {}, platform)
|
config.get("mcp_servers") or {}, platform)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -218,3 +218,112 @@ class TestReasoningCommand:
|
||||||
assert result["final_response"] == "ok"
|
assert result["final_response"] == "ok"
|
||||||
assert _CapturingAgent.last_init is not None
|
assert _CapturingAgent.last_init is not None
|
||||||
assert _CapturingAgent.last_init["reasoning_config"] == {"enabled": False}
|
assert _CapturingAgent.last_init["reasoning_config"] == {"enabled": False}
|
||||||
|
|
||||||
|
def test_run_agent_includes_enabled_mcp_servers_in_gateway_toolsets(self, tmp_path, monkeypatch):
|
||||||
|
hermes_home = tmp_path / "hermes"
|
||||||
|
hermes_home.mkdir()
|
||||||
|
(hermes_home / "config.yaml").write_text(
|
||||||
|
"platform_toolsets:\n"
|
||||||
|
" cli: [web, memory]\n"
|
||||||
|
"mcp_servers:\n"
|
||||||
|
" exa:\n"
|
||||||
|
" url: https://mcp.exa.ai/mcp\n"
|
||||||
|
" web-search-prime:\n"
|
||||||
|
" url: https://api.z.ai/api/mcp/web_search_prime/mcp\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
|
||||||
|
monkeypatch.setattr(gateway_run, "_env_path", hermes_home / ".env")
|
||||||
|
monkeypatch.setattr(gateway_run, "load_dotenv", lambda *args, **kwargs: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
gateway_run,
|
||||||
|
"_resolve_runtime_agent_kwargs",
|
||||||
|
lambda: {
|
||||||
|
"provider": "openrouter",
|
||||||
|
"api_mode": "chat_completions",
|
||||||
|
"base_url": "https://openrouter.ai/api/v1",
|
||||||
|
"api_key": "test-key",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
fake_run_agent = types.ModuleType("run_agent")
|
||||||
|
fake_run_agent.AIAgent = _CapturingAgent
|
||||||
|
monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent)
|
||||||
|
|
||||||
|
_CapturingAgent.last_init = None
|
||||||
|
runner = _make_runner()
|
||||||
|
|
||||||
|
source = SessionSource(
|
||||||
|
platform=Platform.LOCAL,
|
||||||
|
chat_id="cli",
|
||||||
|
chat_name="CLI",
|
||||||
|
chat_type="dm",
|
||||||
|
user_id="user-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = asyncio.run(
|
||||||
|
runner._run_agent(
|
||||||
|
message="ping",
|
||||||
|
context_prompt="",
|
||||||
|
history=[],
|
||||||
|
source=source,
|
||||||
|
session_id="session-1",
|
||||||
|
session_key="agent:main:local:dm",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["final_response"] == "ok"
|
||||||
|
assert _CapturingAgent.last_init is not None
|
||||||
|
enabled_toolsets = set(_CapturingAgent.last_init["enabled_toolsets"])
|
||||||
|
assert "web" in enabled_toolsets
|
||||||
|
assert "memory" in enabled_toolsets
|
||||||
|
assert "exa" in enabled_toolsets
|
||||||
|
assert "web-search-prime" in enabled_toolsets
|
||||||
|
|
||||||
|
def test_run_agent_homeassistant_uses_default_platform_toolset(self, tmp_path, monkeypatch):
|
||||||
|
hermes_home = tmp_path / "hermes"
|
||||||
|
hermes_home.mkdir()
|
||||||
|
(hermes_home / "config.yaml").write_text("", encoding="utf-8")
|
||||||
|
|
||||||
|
monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
|
||||||
|
monkeypatch.setattr(gateway_run, "_env_path", hermes_home / ".env")
|
||||||
|
monkeypatch.setattr(gateway_run, "load_dotenv", lambda *args, **kwargs: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
gateway_run,
|
||||||
|
"_resolve_runtime_agent_kwargs",
|
||||||
|
lambda: {
|
||||||
|
"provider": "openrouter",
|
||||||
|
"api_mode": "chat_completions",
|
||||||
|
"base_url": "https://openrouter.ai/api/v1",
|
||||||
|
"api_key": "test-key",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
fake_run_agent = types.ModuleType("run_agent")
|
||||||
|
fake_run_agent.AIAgent = _CapturingAgent
|
||||||
|
monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent)
|
||||||
|
|
||||||
|
_CapturingAgent.last_init = None
|
||||||
|
runner = _make_runner()
|
||||||
|
|
||||||
|
source = SessionSource(
|
||||||
|
platform=Platform.HOMEASSISTANT,
|
||||||
|
chat_id="ha",
|
||||||
|
chat_name="Home Assistant",
|
||||||
|
chat_type="dm",
|
||||||
|
user_id="user-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = asyncio.run(
|
||||||
|
runner._run_agent(
|
||||||
|
message="ping",
|
||||||
|
context_prompt="",
|
||||||
|
history=[],
|
||||||
|
source=source,
|
||||||
|
session_id="session-1",
|
||||||
|
session_key="agent:main:homeassistant:dm",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["final_response"] == "ok"
|
||||||
|
assert _CapturingAgent.last_init is not None
|
||||||
|
assert "homeassistant" in set(_CapturingAgent.last_init["enabled_toolsets"])
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,39 @@ def test_platform_toolset_summary_uses_explicit_platform_list():
|
||||||
assert summary["cli"] == _get_platform_tools(config, "cli")
|
assert summary["cli"] == _get_platform_tools(config, "cli")
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_platform_tools_includes_enabled_mcp_servers_by_default():
|
||||||
|
config = {
|
||||||
|
"mcp_servers": {
|
||||||
|
"exa": {"url": "https://mcp.exa.ai/mcp"},
|
||||||
|
"web-search-prime": {"url": "https://api.z.ai/api/mcp/web_search_prime/mcp"},
|
||||||
|
"disabled-server": {"url": "https://example.com/mcp", "enabled": False},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled = _get_platform_tools(config, "cli")
|
||||||
|
|
||||||
|
assert "exa" in enabled
|
||||||
|
assert "web-search-prime" in enabled
|
||||||
|
assert "disabled-server" not in enabled
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_platform_tools_keeps_enabled_mcp_servers_with_explicit_builtin_selection():
|
||||||
|
config = {
|
||||||
|
"platform_toolsets": {"cli": ["web", "memory"]},
|
||||||
|
"mcp_servers": {
|
||||||
|
"exa": {"url": "https://mcp.exa.ai/mcp"},
|
||||||
|
"web-search-prime": {"url": "https://api.z.ai/api/mcp/web_search_prime/mcp"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled = _get_platform_tools(config, "cli")
|
||||||
|
|
||||||
|
assert "web" in enabled
|
||||||
|
assert "memory" in enabled
|
||||||
|
assert "exa" in enabled
|
||||||
|
assert "web-search-prime" in enabled
|
||||||
|
|
||||||
|
|
||||||
def test_toolset_has_keys_for_vision_accepts_codex_auth(tmp_path, monkeypatch):
|
def test_toolset_has_keys_for_vision_accepts_codex_auth(tmp_path, monkeypatch):
|
||||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||||
(tmp_path / "auth.json").write_text(
|
(tmp_path / "auth.json").write_text(
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,23 @@ class TestToolsMixedTargets:
|
||||||
assert "web" not in saved["platform_toolsets"]["cli"]
|
assert "web" not in saved["platform_toolsets"]["cli"]
|
||||||
assert "create_issue" in saved["mcp_servers"]["github"]["tools"]["exclude"]
|
assert "create_issue" in saved["mcp_servers"]["github"]["tools"]["exclude"]
|
||||||
|
|
||||||
|
def test_builtin_toggle_does_not_persist_implicit_mcp_defaults(self):
|
||||||
|
config = {
|
||||||
|
"platform_toolsets": {"cli": ["web", "memory"]},
|
||||||
|
"mcp_servers": {"exa": {"url": "https://mcp.exa.ai/mcp"}},
|
||||||
|
}
|
||||||
|
with patch("hermes_cli.tools_config.load_config", return_value=config), \
|
||||||
|
patch("hermes_cli.tools_config.save_config") as mock_save:
|
||||||
|
tools_disable_enable_command(Namespace(
|
||||||
|
tools_action="disable",
|
||||||
|
names=["web"],
|
||||||
|
platform="cli",
|
||||||
|
))
|
||||||
|
saved = mock_save.call_args[0][0]
|
||||||
|
assert "web" not in saved["platform_toolsets"]["cli"]
|
||||||
|
assert "memory" in saved["platform_toolsets"]["cli"]
|
||||||
|
assert "exa" not in saved["platform_toolsets"]["cli"]
|
||||||
|
|
||||||
|
|
||||||
# ── List output ──────────────────────────────────────────────────────────────
|
# ── List output ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue