# 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 **20 seconds of inactivity** (the `McpBusyTracker` default TTL). The mcp SDK 1.28.1 does not expose a per-session shutdown callback, so a clean Hermes disconnect is also recovered via the 20s TTL sweep — see the implementation note in spec §6.5. 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 The mcp SDK 1.28.1 forces tool errors into `CallToolResult(isError=true, content=[TextContent(message)])` — there is no public path that surfaces JSON-RPC `-32000` with a structured `data.busy_owner` field from a tool call site. The busy-owner value lives inside the text content (full string for `cloud_assignment`, truncated session_id prefix for `mcp_session:` collisions). | Condition | JSON-RPC envelope | `result.content[0].text` | |---|---|---| | Missing/wrong bearer token | HTTP 401 (transport-level) | `{"error": "invalid token"}` + `WWW-Authenticate: Bearer` | | Device busy (cloud) | `result.isError = true` | `"device is busy (held by cloud assignment)"` | | Device busy (other MCP) | `result.isError = true` | `"device is busy (held by mcp_session:<8-char-prefix>)"` | | Unknown device | `result.isError = false` | JSON `{"ok": false, "error": "device not found: "}` | | Tool error | `result.isError = true` | `"Error executing tool : "` | ## 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.