Files
agentic-mobile-control/docs/MCP_INTEGRATION.md
T

4.2 KiB

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:

<identity_path.parent>/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:

mcp_servers:
  apex_device:
    url: "http://127.0.0.1:8765/mcp"
    headers:
      Authorization: "Bearer <paste-token-here>"

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:<prefix>"
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.