Android/iOS 控制
把 Android 手机(和 iOS 设备)接入 MCP:读取屏幕树/截图,并下发点击、输入、滚动、打开应用等命令。Agent 通过授权的 device_id 操控你配对的真实手机。
解决的核心问题Agent 只能生成文本或调用简单 API,要操作手机上的 App 就得自己接 ADB / WebDriverAgent、处理连接保活与凭据。device-control 把"读屏 + 下发操控命令"做成内置 MCP,配对一次即可被 Agent 调用,凭据与吊销都走平台治理。
特色:配对码换 token,10 分钟单次
- 创建设备实例:在用户侧「我的工具」新建设备控制实例,点「生成配对码」。配对码 10 分钟内有效且只能使用一次,存 Redis(
SET NX EX+GETDEL)以支持多 worker。 - 手机端配对:在 Android App 中输入服务器地址和配对码。地址可填域名(
https://host)或完整路径(https://host/mcp/device-control),App 会先试原地址,不通再补/mcp/device-control。 - 授权给 Agent:把该实例授权给需要用手机的 MCP principal / Agent。Agent 通过资源授权获得
device_id。 - 解除配对:清除服务端 token_hash 并停用设备;在线设备收到 close 4003,App 擦除凭据并停止重连。
设备 token ≠ MCP 访问 token手机端只用配对后返回的设备 token(连 WebSocket 时鉴权);Agent 调用 MCP 用的是MCP 访问 token,二者是两回事——和 CDP 的两类 token 同构。设备 token 只存 sha256,泄库也无法还原。
特色:15 个操控命令
服务端不下发设备未在 capabilities 里声明的命令——真下发了设备会回 unsupported。命令词汇表(spec §8):
| 命令 | 行为 |
|---|---|
get_screen_state | 读取屏幕树/当前状态(只读,可并发) |
tap / long_press / double_tap | 点击 / 长按 / 双击坐标或节点 |
swipe / scroll / scroll_to_node | 滑动 / 滚动 / 滚到指定节点 |
type_text / set_text | 输入文本 / 直接设置输入框文本 |
press_key | 按键:enter/tab/delete/backspace/escape/space/dpad_* |
dismiss_keyboard | 收起键盘 |
press_back / press_home / press_recents | 系统返回 / 主页 / 最近任务键 |
open_app / list_apps | 打开应用 / 列出已安装应用(list_apps 只读可并发) |
串行与并发设备必须按收到顺序串行执行会改 UI 状态的命令(服务端同样不并发下发,避免"两个 tap 交错");
get_screen_state 与 list_apps 是只读例外,可并发。v0 故意排除音量/电源键。特色:WebSocket v0 协议
设备打开 WS 到 /mcp/device-control/ws/device,握手双方带 protocol_version,服务端权威——不匹配直接 close 4004,绝不"假定兼容"。帧类型(spec §3):
- 设备 → 服务端:
register(登记设备与 capabilities)、heartbeat、call-response(命令结果)、event(capabilities-changed / control-revoked)。 - 服务端 → 设备:
registered(ack 与协商参数)、call(下发命令)、call-cancel(建议性取消,设备仍须回 call-response)。
关键常量(spec)
| 常量 | 值 | 含义 |
|---|---|---|
| 心跳间隔 / 超时 | 15s / 60s | 任意帧都刷新活性,超时收割(close 4010) |
| 单设备在途上限 | 8 | 第 9 条提前挡掉,设备会回 overloaded |
| call 超时 | 默认 15s / 上限 60s | 设备侧可 clamp 到 60s,服务端预算 = timeout + 5s |
| 最大帧 | 4 MiB | 超过 close 4002 |
| register 超时 | 10s | WS 打开后 10s 内必须发 register,否则 close 4008 |
特色:新连接踢旧,半开不再锁死
安卓半开连接远比 Chrome 频繁——doze、蜂窝/WiFi 切换、进程被后台回收都不发 TCP FIN。协议用新连接踢旧(last-writer-wins,旧连接 close 4009),而不是拒绝式,避免"服务端以为还连着、设备怎么都连不上"。空闲超时保留,但只是兜底清理,不再是设备能否重连的关键路径。
吊销即断用户在前端删掉/轮换设备凭据后,活连接必须当场断(close 4003),不能等下次心跳超时——与 CDP driver 的
apply_clients 吊销 diff 同一思路。WS 读循环每帧复查 token 是否仍在授权快照里。iOS:node-ios 独立二进制
iOS 走 node-ios 独立 Go 二进制(nodes/ios),用 go-ios + WebDriverAgent 驱动一台或多台 iPhone(USB/LAN),对服务端在线上与 Android App 不可区分:
node-ios pair --server URL --code CODE --udid X兑换配对码(纯 HTTP,无需连真机),写 0600 凭据并登记设备。node-ios run按配置为每台设备起一个连接循环;真机链路(go-ios discovery + WDA 启动/端口转发)在 device-control/ios driver,无设备时记 "no iOS device found" 并保活。- 不共用节点协议栈:device-control 用自己的配对码 → 长连接 token 鉴权与 WebSocket,不复用 execution 节点的 NodeConnect / TOTP 栈——所以没有把 go-ios 拖进每个节点。
两套栈不要混淆「节点」(cap-node) 主动连 NodeConnect、TOTP 鉴权、跑 CLI/终端/代理;「Android/iOS 控制」(本页) 主动连 device-control WS、配对码换 token、读屏下发操控命令。两者协议、鉴权、用途都不同,是两套独立栈。
特色:与 CDP 桥接的结构差异
| CDP 浏览器桥接 | device-control 手机 | |
|---|---|---|
| 结构层级 | client → user → pages(一浏览器扇出多标签) | device_id → DeviceContext(一台手机一个端点,无第三层) |
| 结果等待 | 同步轮询 | asyncio.Future 按 request_id 配对 |
| 重连模型 | already_connected 拒绝式 | 新连接踢旧(close 4009) |
| 用途 | 操作已登录网页 | 操作手机 App 与系统 |