AIRI DevLog 2025.04.22:基于 MCP 协议让 AI 接管 Android 手机——从 ADB 交互到桌面宠物的实战记录
AIRI DevLog 2025.04.22:基于 MCP 协议让 AI 接管 Android 手机——从 ADB 交互到桌面宠物的实战记录
本篇文章解析 AIRI 项目 DevLog(2025.04.22)中记录的「让 LLM 通过 MCP 协议操作 Android 手机」的完整探索历程:从 MCP(Model Context Protocol)三大核心概念的正确理解,到基于 FastMCP + ADB 构建 airi-android 服务器的最小可运行代码,再到 Cursor 操作手机的「截图 → UI 自动化 → 点击/滑动 → 循环」工作流及其四个待解难题。与此同时,文章结合当前仓库中 stage-tamagotchi 的 MCP stdio 管理器源码,说明 AIRI 桌面端如今如何以 mcp.json 配置接入任意 MCP 服务器,以及工具注册、调用与超时容错的具体实现。读者读完可掌握:MCP 的 Resources / Prompts / Tools 三要素的判别方法、如何用 Python 几行代码写出一个可被 LLM 调用的 MCP 服务器、以及把 MCP 工具接入现有 AI 桌面应用时需要面对的真实工程问题。
背景:从「桌面宠物」到「手机操控」
AIRI 的核心形态是桌面宠物(desktop pet)。两个月前,AIRI 的 Web 界面被移植到 Electron(对应 PR #7),能够像宠物一样悬浮在屏幕上;随后这一移植又进一步重构为 Tauri 实现(对应 PR #90)。桌面形态解决的是「AI 出现在你的电脑屏幕上」,而作者 @LemonNeko 一直在思考的下一件事是:能不能让 AIRI 真的去「用」手机?
这个念头被搁置了一段时间,直到 2025.04.20 的周末,作者抽空写出了第一个演示:一个能够与 ADB(Android Debug Bridge)交互的 MCP 服务器 demo——airi-android。它给 AIRI(以及几乎所有支持工具调用的 LLM)提供了基础的移动端操控能力。demo 视频保存在 docs/content/en/blog/DevLog-2025.04.22/assets/cursor-open-settings.mp4,展示了 Cursor 通过该服务器打开手机设置的过程。作者同时将其打包为 Docker 镜像,并登记到了公共 MCP 服务器列表,供感兴趣的人直接使用。
为什么是 MCP:一次「追赶潮流」的技术选型
作者最初的设想其实非常简单直接:自己写一段 Tool Calling 代码,修改一下 Prompt,告诉 LLM「我们有这些工具可以操作手机」,然后结束。但当时 MCP 正处于流行风口,作者抱着 FOMO(错失恐惧)的心态,最终选择用 MCP 来实现。
这个选择有一个实际收益:MCP 是一个标准化协议,而不是某个应用私有的工具格式。一旦服务器写好了,任何理解 MCP 的 LLM 客户端都能直接使用它——作者在文中明确表示,事实上「大多数 LLM 都可以通过它来操作手机」。这正是协议层抽象的价值:一次实现,多处复用。
MCP 核心概念:Resources、Prompts、Tools
MCP(Model Context Protocol)的目标是标准化「应用程序如何向 LLM 提供上下文」。协议提出了三个核心原语:
- Resources(资源):服务器可以把数据和内容作为上下文提供给 LLM,相当于「只读的知识来源」;
- Prompts(提示词):用来创建可复用的提示模板与工作流;
- Tools(工具):允许 LLM 通过你的服务器执行动作,是「读写与副作用」的入口。
这三者最容易混淆的是 Resources 与 Tools 的边界,而本文下一节正好记录了一次真实踩坑。
一次真实的踩坑:设备列表到底是 Resource 还是 Tool?
作者初学 MCP 时,脑中带着 Ruby on Rails 的「资源(resource)」概念——在 Rails 里用户就是一种资源。于是很自然地产生疑问:ADB 设备也是资源吗?要让 LLM 查看已连接设备的列表,是不是应该这样写:
from mcp.server.fastmcp import FastMCP
from ppadb.client import Client
mcp = FastMCP("airi-android")
adb_client = Client()
@mcp.resource("adb://devices")
def get_devices():
return adb_client.devices()
答案是错的。 当作者让 Cursor 去获取设备列表时,Cursor 并不知道该如何操作。关键的区别在于语义:
- 如果只是被动地向 LLM 提供一段固定内容,那是 Resource;
- 如果 LLM 需要主动触发、动态查询、并且结果随调用而变化,那就是 Tool。
「主动确认当前有哪些设备连接」是一个动作(查询动作),它返回的内容会随设备接入状态实时变化,因此它应该被建模为 Tool,而不是 Resource。作者由此意识到自己此前并没有真正理解二者的差异——这个反思也恰好呼应了 MCP 设计文档中对两类原语的定位:Resources 提供内容,Tools 产生行为。
Cursor 操作手机的观察:一套「感知—定位—操作—循环」流程
在 airi-android 演示中,作者观察到 Cursor 操作手机的典型工作方式,可以概括为四步循环:
- 截图感知:利用截图功能大致了解手机屏幕上有什么;
- UI 自动化定位:借助 UI 自动化工具拿到想要操作元素的精确坐标;
- 点击或滑动:对该元素执行点击、滑动等操作;
- 循环:重复上述步骤,直到任务完成。
这套流程本身运转良好,但作者也借此提出了四个尚未解决的开放问题:
- 游戏等自绘画面的可操作性:如果屏幕上运行的是用图形 API(如 OpenGL/Vulkan/游戏引擎)直接绘制、而非标准 UI 组件构成的应用,UI 自动化工具拿不到元素位置,自然也就无法操作;
- 长任务的分步触发:LLM 响应存在长度上限,复杂操作必须拆成多个步骤依次执行。能否像仓库中
airi-factorio那样,在每一步完成后自动通知 LLM 触发下一步? - 动画导致的截图时序问题:有些 App 带有华丽动画,操作刚结束立刻截图往往看不到结果。是应该操作后等待片刻再截图,还是干脆改用屏幕录制?
- 安全风险:让 AI 直接操作手机,权限边界在哪里?存在哪些潜在风险?
这四个问题至今仍是「AI 操控设备」类项目需要正视的工程课题,涉及无障碍/UI 树信息获取、状态机驱动的长任务编排、感知时序设计以及权限最小化。
从 demo 到产品:AIRI 桌面端已有的 MCP 基建
airi-android 是独立仓库中的演示项目,但「让 AIRI 连接 MCP 服务器」这件事在当前仓库中已经有完整的生产级实现,位于 apps/stage-tamagotchi/src/main/services/airi/mcp-servers/index.ts。这正好接上了 DevLog 末尾的计划:「把 AIRI 桌面宠物接到 MCP 服务器上」。
mcp.json:一份可校验的服务器配置
MCP 服务器通过配置文件声明,文件默认存放在 Electron 的 userData 目录下(getConfigPath() 使用 app.getPath('userData') 拼接 mcp.json)。如果文件不存在,管理器会写入默认的空配置 { "mcpServers": {} } 并自动创建。单个服务器的结构在 apps/stage-tamagotchi/src/shared/mcp-config.ts 中用 Zod schema 严格约束:
{
"mcpServers": {
"airi-android": {
"command": "docker",
"args": ["run", "--rm", "-i", "airi-android"],
"env": {},
"cwd": "",
"enabled": true
}
}
}
字段说明(对应 electronMcpStdioServerConfigSchema):
| 字段 | 类型 | 说明 |
|---|---|---|
command |
string(非空,必填) | 启动 MCP 服务器的可执行命令,如 npx、docker、python |
args |
string[](可选) | 命令参数,例如 ["-y", "@some/mcp-server"] |
env |
Record<string, string>(可选) | 传给服务器的环境变量 |
cwd |
string(可选) | 服务器进程的工作目录 |
enabled |
boolean(可选) | 设为 false 时跳过启动(见 applyAndRestart 中的 skipped 逻辑) |
schema 使用 .strict(),意味着多余的字段会被校验拒绝,这能有效防止配置漂移;解析失败时会通过 formatElectronMcpConfigIssues 输出如 mcpServers.fs.command: Too small... 这类带路径的错误信息,方便定位。
会话生命周期与运行状态机
管理器(createMcpStdioManager)以 Map<serverName, McpServerSession> 维护每个服务器的客户端会话,每个会话包含 Client(来自 @modelcontextprotocol/sdk/client)、StdioClientTransport 以及原始配置:
- 启动:应用启动时
applyAndRestart()遍历配置,逐台创建StdioClientTransport并client.connect(transport);服务器 stderr 会被管道捕获并写入日志; - 状态机:每个服务器维护
running / stopped / error三种运行状态,并记录pid与lastError;enabled: false的服务器进入stopped并标记为skipped; - 退出清理:通过
onAppBeforeQuit钩子在应用退出前调用stopAll(),逐个client.close()优雅关闭会话。
工具命名与调用容错
listTools() 会对每台服务器调用 SDK 的 client.listTools(),并将工具改名为 <serverName>::<toolName>(分隔符为 ::),防止多服务器间的工具名冲突。调用时(callTool)通过 parseQualifiedToolName 拆回服务器名与工具名,然后 client.callTool() 执行,并把返回结果规范化为 content / structuredContent / isError / toolResult 四个字段。
值得一提的细节是 resolveFallbackToolName 的容错设计:部分服务器返回的工具名带 stdio:: 或 stdo:: 前缀,首次调用失败后,代码会尝试剥离前缀后重试(如 stdio::adb::screenshot → adb::screenshot),这一逻辑也覆盖了「只保留最后一个 :: 之后的片段」的兜底情况。请求超时方面,单次请求超时 10 秒、总超时 15 秒(mcpRequestTimeoutMsec / mcpRequestMaxTotalTimeoutMsec),测试模式下还会把 stderr 尾部最多 16,000 字符带回给设置界面展示。
工具如何进入 LLM 上下文
在渲染进程侧,apps/stage-tamagotchi/src/renderer/stores/tools/mcp.ts 通过 createMcpTools({ listTools, callTool }) 把 MCP 工具注册进 LLM 工具存储,注册时统一加上 mcp: 前缀(toolIdPrefix),这样:
- 每次
refresh()先移除所有旧的mcp:工具,再重新拉取并注册,保证工具列表与mcp.json实时同步; - 工具描述与输入 schema(
inputSchema)原样透传给 LLM,作为模型决定「何时调用、传什么参数」的依据。
换句话说,DevLog 中设想的「让桌面宠物连上 MCP 服务器」,在当前仓库里已经落地为「配置文件声明 + 主进程托管 + 渲染进程注册」的完整链路;而设置页(如 apps/stage-tamagotchi/src/renderer/pages/settings/modules/mcp.vue)则提供了服务器表单、JSON 编辑器和连接测试面板。
开发中的小技巧与协作体验
在开发 airi-android 的过程中,作者记录了两个有价值的经验:
用命令行启动 Android 模拟器,省掉 Android Studio 的内存开销:
emulator -avd Pixel_6_Pro_API_34
直接在命令行拉起模拟器,无需打开完整的 Android Studio,显著降低了开发时的内存压力,也让自动化测试环境更轻量。
与 AI 结对开发的角色反转。作者写道,这是第一次产生「像和人类一起写代码」的感觉——因为目标是「让 AI 使用自己的工具」,AI 反而变成了客户,作者需要不断根据 AI 的反馈修正代码;同时 AI 又像同事,需要一起思考、一起解问题。配套的协作截图保存在 docs/content/en/blog/DevLog-2025.04.22/assets/develop-with-cursor.avif,画面中能看到作者与 Cursor 在同一个屏幕前讨论代码。
下一步:桌面宠物接管 Telegram
DevLog 的末尾给出了明确的路线图:下一步要把 AIRI 桌面宠物连接到 MCP 服务器,观察它想做什么——或许它会像当前的 ReLU 一样打开 Telegram 与我们聊天,但不依赖 Telegram 官方 API,而是通过模拟操控 UI 的方式完成。这与上文四个开放问题中的「UI 自动化与自绘画面」议题直接相关:绕过 API 意味着 AI 将以「看屏幕 + 点屏幕」的人类方式使用应用,而这也正是 airi-android 这类 ADB MCP 服务器存在的意义。
总结
这篇 DevLog 的价值在于它诚实记录了一条「从想法到 demo」的真实路径:先用 Tool Calling + Prompt 的朴素思路规划,再因生态趋势转向 MCP;通过 FastMCP 几行代码把 ADB 设备查询暴露成服务器;在「Resource 还是 Tool」的辨析中加深了对协议的理解;最后通过观察 Cursor 的四步操作循环,提炼出感知、定位、时序与安全四个值得长期投入的问题。而这些讨论在今天仓库的 MCP stdio 管理器 中,已经演化为带有配置校验、会话状态机、工具命名规范与调用容错的工程化实现——对任何想在自有应用中接入 MCP 工具的开发者而言,这份源码都是一份可对照的参考答案。