# Host-Agent MCP Server Integration The host-agent process exposes a Streamable HTTP MCP server on the same port as the local console (default `127.0.0.1:8765`), at path `/mcp`. This lets any MCP-compatible client — Hermes Agent, Claude Desktop, custom scripts using the `mcp` Python SDK — drive devices directly through the same `DeviceManager` the cloud worker uses. ## Prerequisites - Host-agent built from this repo (see `docs/MACOS_IPHONE_SETUP.md`). - An MCP client that supports the Streamable HTTP transport (mcp SDK 1.20+ on the client side). ## Get the bearer token The first time host-agent starts after this feature ships, it generates a random bearer token and writes it to: /host_mcp_token.json (Default: `tasks/host_mcp_token.json` next to `host_identity.json`.) To print it for copy/paste: device-host-agent mcp-token To rotate: delete the file and restart host-agent. Old tokens stop working immediately. ## Hermes Agent configuration Add to `~/.hermes/config.yaml`: ```yaml mcp_servers: apex_device: url: "http://127.0.0.1:8765/mcp" headers: Authorization: "Bearer " ``` Start (or restart) Hermes. Verify by asking Hermes to list devices: > Use the apex_device MCP to list connected devices. ## Tools exposed All 11 device tools from `api/mcp.py`: - `take_screenshot(device_id?)` - `tap(x, y, device_id?)` - `swipe(start_x, start_y, end_x, end_y, duration_ms?, device_id?)` - `input_text(text, device_id?)` - `launch_app(app_id, device_id?)` - `find_text(query, device_id?)` - `find_icon(name, device_id?)` - `get_ui_tree(device_id?, include_app_info?)` - `describe_screen(device_id?)` - `list_devices()` - `device_status(device_id)` ## Concurrency model - The cloud worker and MCP clients share the same `DeviceManager`. - Per-device, session-level locking: the first caller (cloud or MCP) to touch a device holds it; the other side sees a busy error. - MCP sessions hold their lock until the session ends OR 60 seconds of inactivity. Cloud assignments hold theirs until the assignment terminates. - The cloud scheduler is told about MCP-held devices via the heartbeat `mcp_busy_device_ids` field, so it normally won't even try to dispatch to them. A 30-second window exists between an MCP acquire and the next heartbeat; during that window cloud may dispatch, and the host-agent will fail-fast the assignment with `failure_reason="device held by an active MCP session"`. ## Network binding The MCP endpoint is bound to the same address as the local console. By default this is `127.0.0.1` (loopback only). To expose on a different interface, set `HOST_AGENT_CONSOLE_BIND_HOST` AND `HOST_AGENT_CONSOLE_ALLOW_NON_LOOPBACK=true` — both are required. This is the same escape hatch the local console uses; there is no MCP-only override. ## Error responses | Condition | HTTP / JSON-RPC | Body | |---|---|---| | Missing/wrong bearer token | HTTP 401 | `{"error": "invalid token"}` + `WWW-Authenticate: Bearer` | | Device busy (cloud) | JSON-RPC `-32000` | `"device X is busy (held by cloud assignment)"`, `data.busy_owner = "cloud_assignment"` | | Device busy (other MCP) | JSON-RPC `-32000` | `"..."`, `data.busy_owner = "mcp_session:"` | | Unknown device | JSON-RPC `-32602` | `"unknown device: X"` | | Tool error | JSON-RPC `-32000` | Original exception message | ## Troubleshooting - **`list_devices` returns `[]`**: no devices registered. Use the local console at `http://127.0.0.1:8765/` to add one (Login → Devices). - **`device X is busy` even when cloud console says device is idle**: check whether another MCP session is holding it. The local console dashboard shows active MCP sessions and held device_ids. - **Token verification fails after restart**: confirm you copied the token from the current `host_mcp_token.json`, not an older one. Rotation = delete file + restart. ## Out of scope (current version) - `wait_until_usable` MCP tool: implemented internally but not exposed. MVP callers must handle busy errors themselves. - MCP call history in the local console: only current state is surfaced, not a call log. - Token rotation CLI: use delete-and-restart for now. - Non-loopback binding without explicit opt-in.