diff --git a/docs/MACOS_IPHONE_SETUP.md b/docs/MACOS_IPHONE_SETUP.md index 102d93c..8580c13 100644 --- a/docs/MACOS_IPHONE_SETUP.md +++ b/docs/MACOS_IPHONE_SETUP.md @@ -527,6 +527,20 @@ Appium server 默认使用 4723;WDA 通常使用 8100。多设备必须为每 input、launch 和 UI tree 验证基础控制,再单独处理 PaddleOCR/PaddlePaddle 的 macOS wheel 与 Apple Silicon 兼容性。 +## MCP server (Hermes Agent integration) + +Host-agent now exposes an MCP server on the same port as the local +console (`127.0.0.1:8765/mcp`). To drive your iPhone from Hermes Agent +or any MCP-compatible client: + +1. Start host-agent normally. +2. Get the bearer token: `device-host-agent mcp-token`. +3. Configure Hermes per `docs/MCP_INTEGRATION.md`. + +The MCP path reuses the same WDA session that the cloud worker uses. +Per-device locking prevents both sides from driving the same device at +once; see `docs/MCP_INTEGRATION.md` for the full concurrency model. + ## 12. 完成检查表 - [ ] Xcode 能看到已解锁的 iPhone。 diff --git a/docs/MCP_INTEGRATION.md b/docs/MCP_INTEGRATION.md new file mode 100644 index 0000000..948d094 --- /dev/null +++ b/docs/MCP_INTEGRATION.md @@ -0,0 +1,115 @@ +# 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.