AIRI 开发日志用 Tauri MCP 插件打通 LLM 与安卓手机的调用链路【免费下载链接】airi Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi本篇技术指南来自 AIRI 项目 2025.04.28 的官方开发日志DevLog-2025.04.28由项目成员 LemonNeko 撰写核心讲述了为 Tauri 桌面应用编写 MCPModel Context Protocol插件的完整过程从最朴素的 Tauri command 出发逐步演进为可发布的官方插件最终让 AIRI 能够与任意 MCP 服务器交互、并通过 ADB 操作安卓手机。读完本文你将掌握 Tauri 2 插件化改造、状态管理、权限系统、JSON 参数透传等关键实战技巧以及 LLM → Tauri → MCP 服务器 → 设备的完整调用链路设计思路。原日志用一张架构图总结了整条链路即从 LLM 调用安卓手机背景MCP 服务器的前半部分与后半部分在 上一篇 DevLog-2025.04.22 中作者介绍了 AIRI 操作手机的前半部分用 Python 的FastMCP编写了一个名为airi-android的 MCP 服务器 Demo通过ppadbPython ADB 客户端为 LLM 提供基础的手机交互能力例如查询设备列表、截屏、点击、滑动等并且打包成了 Docker 镜像提交到了 MCP 服务器列表。但仅有 MCP 服务器是不够的——AIRI 还需要能与 MCP 服务器交互即后半部分。在 2025.04.28 这两天作者完成了这一部分给 Tauri 写了一个 MCP 插件对应当时的 PR #144现在 AIRI 可以与现有的所有 MCP 服务器交互了。在理解这段开发故事之前有必要先明确 MCP 的核心概念。根据上一篇日志的总结MCP 尝试标准化应用如何给 LLM 提供上下文并提出了三类核心原语Resources资源、Prompts提示词和Tools工具。airi-android 暴露给 LLM 的就是 Tools——LLM 通过调用input_swipe这类工具来完成滑动屏幕等动作。如果你想直观感受效果可以参考日志附带的两段演示视频AIRI 的 MCP 服务器设置演示 与 AIRI 在手机上输入Hello World演示。起点先别写插件从暴露命令开始作者一开始并没有打算写一个完整的 Tauri 插件只是想给 JavaScript 侧暴露一些命令。最初的 Rust 侧代码非常简单#[Tauri::command] fn list_tools() - VecString { // 之后再实现 }JavaScript 侧则写一些工具函数来调用它们import { invoke } from Tauri-apps/api/core export const mcp [ { name: list_tools, description: List all tools, execute: async () { return await invoke(list_tools) } } ]但很快作者意识到一个问题如果想在命令中使用 MCP 客户端就需要让 MCP 客户端作为状态的一部分由 Tauri 来管理。MCP 客户端需要在整个应用生命周期内保持连接会话、工具列表等都是有状态的而 Tauri 的 command 是无状态的函数调用因此必须借助 Tauri 的State机制// main.rs fn main() { Tauri::Builder::default() .setup(|app| { app.manage(State::new(Mutex::new::OptionMcpClient(None))); // 管理状态 }) .run(Tauri::generate_context!()) } // mcp.rs #[Tauri::command] async fn list_tools(state: State_, MutexOptionMcpClient) - ResultVecTool, String { // 可以在参数中拿到状态 // ...rest code }这里有一个值得注意的实现细节状态被包在MutexOptionMcpClient里Mutex保证多线程下的并发安全Option则表达客户端可能尚未初始化/连接失败的语义异步命令中通过state.lock().await获取锁。这为后面插件化打下了基础。演进把命令打包成 Tauri 插件有了命令、有了状态作者决定将其正式做成一个 Tauri 插件这样既能公开发布也让代码的组织方式更规范。但插件化带来了一系列连锁改动。调用方式的改变成为一个插件后命令的调用方式变了需要通过插件命名空间来调用import { invoke } from Tauri-apps/api/core export mcp [ { name: list_tools, description: List all tools, execute: async () { - return await invoke(list_tools) return await invoke(plugin:mcp|list_tools) } } ]plugin:mcp|list_tools是 Tauri 插件命令的标准调用格式前缀plugin:表明这是插件提供的命令mcp是插件名list_tools是命令名。从 diff 看改动其实只有一行还算轻松。Tauri 2 的权限机制然而 Tauri 2 引入了权限permissions机制插件作者需要在build.rs中声明插件的命令以便在构建时自动生成权限列表const COMMANDS: [str] [ list_tools, ]; fn main() { Tauri_plugin::Builder::new(COMMANDS).build(); }这样在构建时项目根目录下会生成permissions文件夹其中包含权限声明、描述等文件。凡是想要在应用侧调用插件命令都必须显式授予对应权限——这为应用的安全性提供了细粒度的控制。小插曲第二次构建时作者升级了tauri-plugin的版本新版本中生成模板发生了变化一些空格被删掉了看起来就像文件被格式化过一样。作者花了整整一个小时到处寻找是什么东西在格式化它最后才发现只是文件被重新生成了。作者用 表情纪念这个被吃掉的一小时——这也是所有插件开发者都可能踩到的坑构建生成物generated files不要手工维护也不要为它们的外观变化感到困惑检查生成逻辑即可。打通参数链路从 LLM 到 Python MCP 服务器的完整旅程根据架构图当 LLM 调用 MCP 工具时参数最终会被传递给 Python 侧的 MCP 服务器。以input_swipe为例airi-android 的服务端是这样定义的# mcp_server.py from mcp.server.fastmcp import FastMCP from ppadb.client import Client mcp FastMCP(airi-android) adb_client Client() mcp.tool() def input_swipe(x1: int, y1: int, x2: int, y2: int, duration: int 500): return adb_client.input_swipe(x1, y1, x2, y2, duration)那么问题来了Rust 侧怎么把这些参数组装成 MCP 协议要求的格式MCP Rust SDKrmcp中的CallToolRequestParam是这样定义的pub struct CallToolRequestParam { pub name: Cowstatic, str, pub arguments: OptionJsonObject, }关键在arguments: OptionJsonObject——参数就是一个 JSON 对象。而 Tauri 命令的参数可以是任何能被序列化成 JSON 的对象所以作者选择直接给命令传一个MapString, Value实现JSON 到 JSON的无缝透传#[Tauri::command] async fn call_tool(state: State_, MutexOptionMcpClient, name: String, args: OptionMapString, Value) - Result(), () { let client state.lock().await.unwrap(); client.call_tool(CallToolRequestParam { name: name.into(), arguments: args }).await.unwrap(); Ok(()) }JavaScript 侧只需要传一个普通对象即可完全不需要关心类型转换的细节import { invoke } from Tauri-apps/api/core invoke(call_tool, { name: input_swipe, args: { x1: 100, y1: 100, x2: 200, y2: 200, duration: 500 } })作者对这套方案的评价是超方便——因为整个参数链路从 LLM 的 JSON 到 JavaScript 对象、再到 Rust 的MapString, Value、最后到 MCP 协议的JsonObject全程都是 JSON 形态几乎没有类型转换成本。返回值相信 LLM 会处理好参数传递之外还需要接收 MCP 工具的返回值。既然 Tauri 命令的返回值也可以是任何能被序列化成 JSON 的对象作者选择摆烂把 MCP 工具返回的原始结果整个丢给 LLM相信 LLM 会处理好。这个设计思路在如今仓库的代码中得到了印证在 MCP stdio 管理器 中callTool会把 MCP 响应的content、structuredContent、isError、toolResult等字段原样提取后返回不做任何语义加工渲染进程侧则把工具列表动态注册进 LLM 的工具存储见 渲染进程 MCP 工具 store工具 ID 统一使用mcp:前缀与createMcpTools生成的可执行工具描述一起提供给 LLM。这正呼应了日志中把工具的返回整个丢给 LLM的理念LLM 是最终的理解者MCP 链路只负责无损透传。源码印证如今仓库里的 MCP 实现有意思的是日志发表时 AIRI 还在使用 Tauri而当前仓库中的桌面端 stage-tamagotchi 已经演化为 Electron Vue 架构MCP 支持也变得更加完整。这些实现正好可以作为日志中所描述思路的后日谈佐证会话管理createMcpStdioManager使用StdioClientTransport拉起 MCP 服务器子进程并用modelcontextprotocol/sdk的Client建立连接每个服务器对应一个 session统一存放在sessionsMap 中退出应用时通过生命周期钩子统一stopAll。配置与校验配置持久化在用户数据目录下的mcp.json由 mcp-config.ts 中定义的 Zod schema 做严格校验。每个服务器条目支持command、args、env、cwd、enabled五个字段——command必填且非空其余均可选这实际上是 MCP stdio 服务器配置的完整字段集。运行时状态管理器维护每个服务器的运行状态running/stopped/error记录 PID、启动命令和最后一次错误信息供设置界面展示。容错与超时所有 MCP 请求都设置了超时如 10 秒请求超时、15 秒总超时listTools失败时只记录警告而不中断整体流程callTool在工具名带传输前缀或分隔符时还会尝试规范化名称后重试一次。连通性测试testServer会临时拉起一个独立 transport 并执行listTools把成功获取的工具名列表或失败时的 stderr 尾部最多 16,000 字符返回给设置页——对应日志中AIRI 的 MCP 服务器设置演示视频里的连接测试能力。UI 支持设置页提供了 McpServerForm.vue、McpJsonEditor.vue、McpConnectionTestPanel.vue 等组件并配套 mcp-config.test.ts、mcp.test.ts 等测试用例保证了配置解析与工具注册逻辑的正确性。此外工具名的跨服务器命名空间也很值得一提listTools会把工具名组装为${serverName}::${toolName}的限定形式从而支持多 MCP 服务器共存而不产生命名冲突——这实际上已经部分回答了日志末尾多 MCP 服务器支持的展望。开放问题作者留给社区的讨论日志中作者抛出了三个值得深思的问题这里完整保留供读者在理解架构后继续思考工具列表能否预加载进系统提示词从演示视频可以看到对话中 AIRI 先是获取了一下工具列表再执行输入文本。那能不能在初始化的时候就去获取工具列表直接追加到系统提示词中Cursor 就是这样做的开发 MCP 服务器时每次改动工具列表都需要重启 Cursor 才能生效。这样做也许会牺牲灵活性但普通用户会频繁改动工具列表吗要允许 AIRI 同时连接到多个手机吗AIRI 可能会想使用多台手机吗作者调侃道她会不会想拿去做电信诈骗——此处是玩笑仓库结构如何演进现在 AIRI 仓库中已经有了 Tauri 应用和 Tauri 插件要怎么管理比较好CI 要怎么配置如何同步 Tauri 插件的 Rust 侧和 JavaScript 侧的版本号从如今的仓库代码看第一个问题已经在实践中得到了倾向性答案MCP 工具是通过运行时动态refresh注册进 LLM 工具存储的见 mcp.ts而不是写死在系统提示词中这样工具的增删改无需重启应用即可生效第三个问题的版本管理难题在 monorepo 环境下也演变成了更常规的包版本同步问题。未来计划日志中的 Roadmap日志结尾作者列出了一份清晰的未来计划支持图片返回值这样 AIRI 就可以像 上一篇 DevLog 中展示的 Cursor 那样直接通过视觉能力看到手机上的内容然后再决定用什么方式来交互。让 AIRI 自己学习设备的使用方法如果每种设备都要单独写提示词工作量是巨大的。多 MCP 服务器支持MCP 提供了一种通用的接口可以允许 AIRI 做各种各样的事AIRI 应该不会满足于只操作手机吧。SSE 支持这样浏览器中的 AIRI 也可以使用 MCP 服务器了。值得一提的是日志发表前的 DevLog-2025.04.22 中已经讨论过 AI 操作手机的具体流程截屏了解内容 → UI 自动化获取元素位置 → 点击或滑动 → 重复以及游戏画面无法被 UI 自动化工具解析、复杂操作需要多步触发、动画场景下截屏时机、操作安全性等问题。将这些讨论与本篇日志对照阅读可以完整看到 AIRI 从写一个 MCP 服务器 Demo到让桌宠真正接入 MCP的演进脉络。小结这篇日志的价值在于完整记录了一个 Tauri 插件从 0 到 1 的真实开发过程从先暴露命令的最小可行方案到发现状态管理需求再到升级为插件并适配 Tauri 2 的权限体系从 MCP 协议参数结构的分析到利用JSON 透传巧妙打通 LLM 与 Python MCP 服务器的调用链。其核心设计哲学——链路只做无损透传理解交给 LLM——在今天的 AIRI 仓库实现中依然清晰可见。如果你正在为桌面应用接入 MCP或想了解如何让 LLM 通过统一协议操作外部设备这篇日志是一个很好的起点。【免费下载链接】airi Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考