feat(desktop): setup_mcp tool — inline MCP consent card over the clarify-style blocking bridge
New desktop_ui tool: the agent proposes an MCP server (install/enable/ authorize + a one-line reason) and blocks on mcp.setup.request until the renderer's consent card answers mcp.setup.respond with the outcome (installed/enabled/authorized/declined/unanswered/error). Same lifecycle as clarify: 10-min timeout, allow_expired late answers, tool lifecycle events forced on so the card mounts even with tool progress off. Desktop prompt hint steers the model to the tool instead of hand-editing config; every other surface keeps the schema out and is pointed at hermes mcp install.
This commit is contained in:
committed by
brooklyn!
parent
a9eb7e09d9
commit
adbc77eb50
@@ -0,0 +1,75 @@
|
||||
"""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_tool
|
||||
|
||||
|
||||
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"
|
||||
Reference in New Issue
Block a user