# MCP 手机控制手册(MCP.md) 多模态 AI(DeepSeek / Claude 等)通过 [MCP(Model Context Protocol)](https://modelcontextprotocol.io) 操作 Android 手机的统一出口。平台自带的「AI 控制台」(`mcp_agent/`)与外部 MCP 客户端使用的都是这一批工具。 > 本文 = **使用手册**(工具清单 / 用法 / 接入)。设计取舍与演进见 [MCP_DESIGN.md](MCP_DESIGN.md);AI 控制台的会话/经验/动作机制见 [AI_CONSOLE.md](AI_CONSOLE.md)。 --- ## 1. 架构与边界 ``` AI 控制台 / 外部 MCP 客户端 │ Streamable HTTP(http://:8033/mcp) ▼ MCP Server(mcp_server/mcp_server.py) ← 20 个 de_* 工具 │ 平台 HTTP API(登录 + CSRF) 或 轻量 adb/u2 直连 ▼ auto_control 平台(:18050) ← 设备池 / 任务 / 看屏 │ adb / uiautomator2 / uiautodev / OCR ▼ Android 设备(IP:5555) ``` - **轻量通道直连**(adb monkey 开 App、u2 输入、本地 OCR)不绕平台,省时间省 token - **坐标换算、UI 吸附、剪贴板注入**等细节全部在 Server 层消化,模型只给"意图" - **只暴露设备层能力**:平台级的任务增改/分组/设备池/APK/备份等 REST 只给 Web 前端用,未做成 MCP 工具(补齐规划见 [AI_TASK_GEN.md](AI_TASK_GEN.md) §9) --- ## 2. 运行与配置 生产跑在 220 的 `python-app` 容器内(`scripts/start.sh` 自动拉起),端点 `http://192.168.20.220:8033/mcp`。 ```bash # 本机手动启动 MCP_ALLOW_WRITE=1 MCP_PLATFORM_USER=admin MCP_PLATFORM_PASS=<平台密码> \ MCP_AUDIT_FILE=data/mcp_audit.log python -m mcp_server.mcp_server ``` | 环境变量 | 默认 | 说明 | |---|---|---| | `MCP_PLATFORM_URL` | `http://127.0.0.1:18050` | 平台地址 | | `MCP_PLATFORM_USER` / `MCP_PLATFORM_PASS` | `admin` / 空(`start.sh` 兜底 `admin123`) | 平台登录凭据。**改过 admin 密码必须同步**,否则登录失败 | | `MCP_ALLOW_WRITE` | `0`(`start.sh` 内强制 `1`) | **写门控**:=1 才允许点击/输入/开关 App(只读工具不受限) | | `MCP_ALLOWED_SERIALS` | 空 | 设备白名单(逗号分隔)。非空 = 只允许列出的 serial;**为空时只校验 serial 非空,不按平台设备池过滤**(语义缺口见 [backlog](backlog/TODO.md)) | | `MCP_HTTP_HOST` / `MCP_HTTP_PORT` | `0.0.0.0` / `8033` | 监听 | | `MCP_SCREENSHOT_WIDTH` | `540` | 截图返回宽度上限(模型看到的坐标系) | | `MCP_JPEG_QUALITY` | `70` | 截图 JPEG 质量 | | `MCP_AUDIT_FILE` | `/var/log/mcp/audit.log`(容器兜底 `/tmp/mcp_audit.log`) | 审计日志路径 | | `MCP_PLATFORM_TIMEOUT` | `30` | 平台请求超时(秒) | | `MCP_ENABLED` | `1` | **只被 `scripts/start.sh` 消费**:=0 则不后台拉起 MCP | > `mcp_server/config.py` **只读进程环境变量,不读 `.env`**;`scripts/supervise.sh` 不拉起 MCP(容器场景由 `start.sh` 负责)。 --- ## 3. 坐标空间(重要) - `de_screenshot` 返回 **≤540px 宽**的 JPEG(display 空间),同时给出 `native_size`(设备原生分辨率)与 `screen_state` - `de_tap` / `de_swipe` 的坐标一律用 **`de_screenshot` 返回图像的坐标系**,Server 按比例换算成原生坐标 - **必须先截图再点击**:Server 需要最近一次截图来建立坐标空间,否则报 `invalid_param`「请先对该设备执行 de_screenshot」 - `de_tap` 带**自动吸附**:落点若在某个可点击元素内,实际点该元素中心 → 坐标**只需大致对准**;返回 `snapped` / `label` 便于核对 --- ## 4. 工具清单(20 个) 统一返回约定: ```json {"ok": true, "data": { … }} {"ok": false, "error": {"code": "device_busy", "message": "设备正在执行任务…"}} ``` 错误码:`invalid_param` · `device_not_allowed` · `write_disabled` · `device_busy` · `platform_unavailable` · `device_offline` · `text_not_found`。 ### 4.1 设备与状态(只读) | 工具 | 参数 | 返回 / 说明 | |------|------|------------| | `de_list_devices` | — | `[{serial, name, model, online, task_job, worker_status, foreground_app}]`。**开局第一步**;`name` 是设备在平台里的名称(身份),**向用户汇报时用名称**(同型号多台靠它区分),调工具仍用 `serial` | | `de_foreground_app` | `serial` | `{foreground_app}` 当前前台包名(dumpsys,MIUI 焦点为空时兜底) | | `de_list_tasks` | — | 平台任务计划 `[{id,name,task_type,enabled,schedule}]`(只读) | ### 4.2 观察屏幕(只读) | 工具 | 参数 | 返回 / 说明 | |------|------|------------| | `de_screenshot` | `serial` | `{image:{type:"image",data:,mimeType:"image/jpeg"}, width, height, native_size, screen_state}`。多模态模型直接看图 | | `de_ui_tree` | `serial`, `limit`(150,1-300) | `{count, elements:[{text,id,desc,class,clickable,bounds}]}`,**可点击元素排前**;`limit` 控 token | | `de_snapshot` | `serial`, `limit`(120,1-300) | **截图 + 元素树一次取齐**(推荐用它代替 `de_screenshot`+`de_ui_tree`):平台侧同一个 u2 连接背靠背取 + 双截图校验。返回 `{image, width, height, native_size, screen_state, unstable, cost_ms, count, elements}`;`unstable=true` 表示抓取期间界面在变化(此时元素坐标不可信,让设备静下来再取)。**同样会建立 `de_tap/de_swipe` 的坐标空间** | | `de_ocr` | `serial` | `{count, texts:[{text,score}]}`(≤100 条)。UI 树拿不到的图片/WebView 文字用它 | | `de_read_clipboard` | `serial` | `{clipboard}` | | `de_list_apps` | `serial`, `keyword`("") | `{count, packages}` 第三方已装包名(`pm list packages -3`,≤200) | ### 4.3 点击与滑动(**写操作**) | 工具 | 参数 | 说明 | |------|------|------| | `de_tap_text` | `serial`, `text`(≤100 字符) | **推荐**:按屏幕可见文字点击。原生控件走 UI 树,WebView/图片文字自动 **OCR 兜底**;找不到 → `text_not_found` | | `de_tap_element` | `serial`, `by`(`text`/`id`/`desc`/`text_contains`/`desc_contains`), `value`, `index`(1) | 按元素属性点击,无需坐标;多命中用 `index` | | `de_tap` | `serial`, `x`, `y` | 坐标点击(截图坐标系 + 自动吸附)。**纯图形目标才用** | | `de_swipe` | `serial`, `x1,y1,x2,y2`, `duration`(0.2) | 滑动(长按 = 同点起止 + `duration≥1`) | | `de_press_key` | `serial`, `key` | `back` / `home` / `recent` / `menu` / `power` / `volume_up` / `volume_down` / `enter` / `delete` / `search` / `camera` | ### 4.4 输入与剪贴板(**写操作**) | 工具 | 参数 | 说明 | |------|------|------| | `de_type_text` | `serial`, `text` | 向当前输入框输入(支持中文,直设 EditText,不依赖剪贴板) | | `de_set_clipboard` | `serial`, `text` | 写入设备剪贴板(设备端 Agent 通道 + 读回验证) | ### 4.5 App 管理(**写操作**,走 adb 直连不建 u2 会话) | 工具 | 参数 | 说明 | |------|------|------| | `de_open_app` | `serial`, `package` | `adb monkey` 直启(最快路径,无需知道 activity) | | `de_stop_app` | `serial`, `package` | `am force-stop` | ### 4.6 亮屏 / 熄屏(**写操作**) | 工具 | 参数 | 说明 | |------|------|------| | `de_wake` | `serial` | 亮屏并解锁(熄屏时先调它再截图) | | `de_sleep` | `serial` | 熄屏(**会中断正在运行的任务,慎用**) | > 后两组分别经 `direct_ops`(adb)与平台 `POST /api/device/screen_all` 实现,不在 MCP 进程内建 u2 会话。 ### 4.7 写操作门控(三连) 所有写工具在执行前依次检查: 1. `_check_write()` —— `MCP_ALLOW_WRITE=1`?否则 `write_disabled` 2. `_check_serial()` —— serial 非空 / 在白名单内?否则 `device_not_allowed` 3. `_ensure_device_free()` —— 设备 `worker_status` 不是 `running`/`connecting`?否则 `device_busy`(**AI 不与任务抢设备**) > 设备状态查询有 **5 秒缓存**;查询本身异常时**放行**(不阻塞),由平台侧兜底。 --- ## 5. 推荐使用模式 1. **`de_list_devices`** 确认目标设备在线 2. **`de_screenshot`** 看图理解当前界面(图像会在下一回合送达模型) 3. **点击定位优先级**: - 目标有可见文字 → **`de_tap_text`**(一次调用完成"找到并点击",最可靠) - 文字有歧义/多候选 → `de_ui_tree` 确认后用 `de_tap_element`(可用 `text_contains` 模糊匹配) - 纯图形目标 → `de_tap` 坐标(**无需精算**,会自动吸附) 4. **输入文字**:先点中输入框,再 `de_type_text` 5. **每步验证**:关键操作后再 `de_screenshot`——界面变了 = 成功;没变 = 未命中 → 换 `de_tap_text`/`de_tap_element` 重新定位,**不要重复点同一坐标** 6. **收尾**:用中文总结做了什么、当前状态、注意事项 7. **效率**:界面未变时不重复截图/点击;连续 6 步无进展就停止并总结 --- ## 6. 安全与审计 | 机制 | 说明 | |------|------| | **写门控** | `MCP_ALLOW_WRITE=0`(默认)时所有写操作被拒,只读可用 | | **任务互斥** | 写操作前检查设备是否在跑任务,忙则 `device_busy` | | **平台会话** | `platform_client` 内部登录(`MCP_PLATFORM_USER/PASS`)、会话失效自动重登、POST 自动带 `X-CSRF-Token`;**平台凭据不暴露给客户端** | | **白名单** | `MCP_ALLOWED_SERIALS` 非空时限制可操作 serial | | **审计** | 每次调用(**含只读**)写一行 JSON 到 `MCP_AUDIT_FILE`:`{ts, tool, serial, args 摘要, result 摘要}` | | **红线** | 任何工具都不执行 `adb kill-server` / `adb disconnect` | | **端点鉴权** | ⚠️ MCP HTTP 端点自身**没有** token/账号校验,只做出站登录 → **必须靠网络隔离**(同机/内网),不要直接暴露公网 | --- ## 7. 客户端接入示例 ```python import asyncio from fastmcp import Client async def main(): async with Client("http://192.168.20.220:8033/mcp", timeout=30) as c: devs = await c.call_tool("de_list_devices", {}) serial = devs.data["data"][0]["serial"] # 第一台设备 shot = await c.call_tool("de_screenshot", {"serial": serial}) img = shot.data["data"]["image"] # base64 JPEG(给多模态模型看) await c.call_tool("de_tap_text", {"serial": serial, "text": "搜索"}) asyncio.run(main()) ``` > 给外部数字员工的使用约定与红线,另见 [staffdeck/KNOWLEDGE_BASE.md](staffdeck/KNOWLEDGE_BASE.md)。 --- ## 8. AI 控制台(内置 Agent) Web 的「AI 控制台」Tab 内建 Agent(`mcp_agent/`,OpenAI 兼容模型)用的就是本 Server 的同一批工具: - 流式输出 + 每步 MCP 调用实时展示(含截图缩略) - 多轮会话(同会话保留上下文) - **自进化记忆**:经验库(任务配方)+ 动作库(命名动作),相似任务自动注入参考 - 模型与 Key 在控制台右上角 ⚙ 配置(存平台 `app_meta`) > **依赖提示**:AI 控制台依赖本 MCP Server(默认 `http://127.0.0.1:8033/mcp`)。未启动时平台会把 SDK 的含糊报错映射成明确文案「**MCP server(8033) 不可达 …**」。 --- > **维护约定**:新增 / 修改 / 删除任何一个 MCP 工具或相关配置,必须**同步更新本文与 [MCP_DESIGN.md](MCP_DESIGN.md)**(doc 同步红线)。