Files
hermes-agent/tests/tools/test_setup_mcp_tool.py
nftpoetrist d6a6d87c4a fix(tools): restore setup_mcp's never-hand-edit instruction
9d9f44d638 removed the desktop platform hint's "never hand-edit
mcp_servers config for them" sentence, reasoning it was a "word-for-word
duplicate of the setup_mcp tool schema... taught on every call." The
schema has never contained that instruction — only "never re-ask after
a decline." setup_mcp is desktop_ui-toolset-only and no runtime guard
in agent/file_safety.py covers mcp_servers config, so removing the only
place teaching this left a real gap: a model asked to add/configure an
MCP server could just write_file into mcp_servers config directly,
bypassing the consent-card/OAuth flow the tool exists to enforce.

Restored the instruction directly in SETUP_MCP_SCHEMA's description —
completing the original commit's stated intent (move it to the schema)
rather than reverting to the platform hint, since the schema reaches
every setup_mcp call regardless of platform hint wording changes.

Added a regression test asserting the schema description forbids
hand-editing mcp_servers config, so a future prompt-diet pass can't
silently drop it again without a test failing.
2026-08-29 17:58:51 -07:00

84 lines
2.7 KiB
Python

"""setup_mcp tool — the desktop inline MCP consent card's tool half.
Behavior contracts:
- no callback (not the desktop app) → tool_error pointing at the CLI path
- empty/invalid args → tool_error
- callback answer passes through as JSON
- empty callback answer (timeout) → status "unanswered", never an error
"""
import json
import pytest
from tools.setup_mcp_tool import SETUP_MCP_SCHEMA, setup_mcp_tool
def test_schema_forbids_hand_editing_mcp_servers_config():
# Nothing else teaches the model this: the tool is desktop_ui-only, so
# without it a model could just write_file into mcp_servers config
# directly, bypassing the consent-card/OAuth flow this tool exists for.
assert "hand-edit" in SETUP_MCP_SCHEMA["description"]
assert "mcp_servers" in SETUP_MCP_SCHEMA["description"]
def test_requires_desktop_callback():
result = json.loads(setup_mcp_tool(server="linear", callback=None))
assert "error" in result
assert "hermes mcp install" in result["error"]
def test_requires_server_name():
result = json.loads(setup_mcp_tool(server=" ", callback=lambda *a: ""))
assert "error" in result
def test_rejects_unknown_action():
result = json.loads(
setup_mcp_tool(server="linear", action="uninstall", callback=lambda *a: "")
)
assert "error" in result
assert "action" in result["error"]
def test_passes_through_renderer_outcome():
outcome = {"status": "installed", "server": "linear"}
def cb(server, action, reason):
assert server == "linear"
assert action == "install"
assert reason == "to read tickets"
return json.dumps(outcome)
result = json.loads(
setup_mcp_tool(server="linear", action="install", reason="to read tickets", callback=cb)
)
assert result == outcome
def test_timeout_returns_unanswered_not_error():
result = json.loads(setup_mcp_tool(server="figma", callback=lambda *a: ""))
assert result["status"] == "unanswered"
assert result["server"] == "figma"
def test_callback_exception_is_tool_error():
def cb(*a):
raise RuntimeError("gateway went away")
result = json.loads(setup_mcp_tool(server="figma", callback=cb))
assert "error" in result
def test_non_json_answer_wrapped_as_error_status():
result = json.loads(setup_mcp_tool(server="figma", callback=lambda *a: "garbage"))
assert result["status"] == "error"
@pytest.mark.parametrize("action", ["install", "enable", "authorize"])
def test_all_actions_accepted(action):
result = json.loads(
setup_mcp_tool(server="x", action=action, callback=lambda s, a, r: json.dumps({"status": "declined", "server": s}))
)
assert result["status"] == "declined"