
最近在折腾移动端 AI 工具链发现 MCP 这个关键词已经躲不掉了。所谓 mobile-mcp就是我正在做的一个项目把 Model Context Protocol 放到手机上跑让大模型能真正调用到手机上的本地能力——通知、定位、摄像头、传感器、剪贴板而不是只在一个云端沙箱里打转。这个项目解决的核心问题很直接AI 助手和真实移动设备之间有道墙MCP 就是拆墙的工具。这篇博文会把我的整体设计、核心模块、实操步骤和踩坑记录完整写出来适合正在做移动端 AI 应用、或者想把自己的智能硬件/手机助手接入 MCP 生态的开发者参考。1. 从 MCP 到 mobile-mcp为什么要在手机上承载协议层1.1 MCP 是什么AI 世界的“通用插口”MCPModel Context Protocol最早是 Anthropic 推出来的一套开放协议解决的是大模型和外部工具之间的互联问题。它的设计思路很像 USB-C不管你是摄像头、U盘还是显示器都通过同一个物理接口和电脑通信协议层标准化设备之间不再需要一堆转接头。MCP 把“工具调用、资源读取、提示词复用”这三件事抽象成了标准接口大模型通过 MCP 客户端去发现并调用远端或本地的能力。过去我们接一个第三方 API通常是为某一个模型单独写一套 function calling 的 JSON 定义再写一套请求调度逻辑。模型换一个工具可能就得推倒重来。MCP 出现之后工具提供方只需要实现一份 MCP Server任何支持 MCP 的客户端Claude Desktop、各类 Agent 框架都能直接拉起来用。这套抽象在服务端世界已经跑得很成熟了但放到移动端情况要复杂得多。我一开始想得很简单既然 MCP Server 本质上就是一个跑的进程那手机上也跑一个不就是了可真正动手才发现移动环境里“跑一个进程”这个事本身就充满了限制。手机有后台冻结机制有权限弹窗门槛有网络切换频繁的问题还有应用商店对敏感权限的审查。mobile-mcp 这个项目就是在回答一个问题移动端要有怎样一套 MCP 实现才不是“能跑但没人敢用”的玩具。1.2 移动端接入 MCP 的核心矛盾传统 MCP 的部署场景基本是桌面和服务器。Server 常驻客户端连上就能用传输层用 stdio标准输入输出或者局域网 HTTP 都行网络相对稳定权限基本不受系统限制。但在 Android / iOS 上这些前提全部不成立。第一个矛盾是进程生命周期。手机操作系统随时可能杀掉你的后台进程尤其是国内 ROM 的激进省电策略一锁屏就把 App 的联网和计算能力冻结。MCP Server 如果保活做不到一次长对话中间断线状态就丢了。第二个矛盾是权限与安全体验。MCP 的一个重要能力是让模型调用工具但手机上的工具大多涉及敏感权限读取位置、读取联系人、发送通知、访问相册。如果模型每次调用都直接执行用户隐私和安全完全失控。可如果每步都弹十次授权框体验又废了。MCP 协议本身只定义了调用的通道并没有定义移动端的授权交互规范这块必须自己补。第三个矛盾是网络模型。移动网络在 Wi-Fi 和蜂窝数据之间切换时 IP 会变HTTP 长连接会断socket 也会断。MCP 目前的 Streamable HTTP 传输要求客户端维护会话 ID一旦断网重连会话上下文就丢了。这些细节在桌面端没人关注在移动端全是主流程问题。1.3 这个项目的定位与选型mobile-mcp 不是一个单纯的 SDK也不是一个完整的 App它更像一个“移动端 MCP 适配层”。具体来说它包含三个部分一个内嵌在 App 里的 MCP Host负责和远端模型/Agent 通信一组把手机本地能力包装成 MCP Tools 的 Server 实现以及一套处理权限确认、后台保活、网络切换的运行时逻辑。技术选型上我最后选了 TypeScript。原因很简单MCP 官方 SDK 目前对 TypeScript/JavaScript 的支持最成熟modelcontextprotocol/sdk可以直接跑在 React Native 的 Hermes 引擎上也能通过 Node.js 跑在桌面端做联调。Flutter 和 Kotlin 的社区实现还比较初级没必要给自己挖坑。另外JS 生态里 JSON Schema 的处理、HTTP 客户端、WebSocket 等库都很顺手对快速迭代原型非常友好。实际架构上我采用“Host 在端上、Server 可本地可远端”的混合模式。简单场景下手机 App 里直接内嵌一个 MCP Host模型服务跑在云端工具服务跑在端上复杂场景下手机本身也可以作为 MCP Server被同一局域网内的其他设备调用。两种模式各有用途下面会展开讲。2. 核心模块拆解设备能力如何变成 MCP 工具2.1 工具调用Tools设计把系统 API 翻译成 JSON SchemaMCP 的工具调用本质上就是让模型输出一个符合预设 JSON Schema 的调用请求客户端解析后执行再把结果回传给模型。所以 mobile-mcp 的第一个核心模块是把手机系统能力翻译成结构化定义。这里有个容易被忽视的细节MCP 工具定义里的inputSchema不是越精确越好而是越简单越好。大模型是通过 JSON Schema 来理解“这个工具能干什么、参数怎么填”的如果 schema 复杂到嵌套了四五层模型大概率会在调用时漏填参数或填错格式。以我封装的通知工具为例最初版本用了完整的 Notification 字段定义包括渠道、优先级、震动模式、桌面角标等等结果模型经常把channel_id漏了调用成功率只有六成。后来我把暴露给模型的 schema 精简成四个字段标题、正文、可选的附加数据、是否使用震动。其余能力全部隐藏由 App 侧根据 notification 的属性做默认处理。调用成功率一下就上去了。另一个关键点是错误返回的设计。工具执行失败时不能只返回一个error字符串最好返回一个结构化的对象例如包含code、message、可恢复提示。这样模型在下一轮就能根据错误信息自行调整参数而不是绕圈子重试。{ success: false, error: { code: PERMISSION_DENIED, message: 用户未授予通知权限, recoverable: true } }2.2 上下文组织Resources 和大模型怎么合作MCP 除了 Tools还有 Resources 和 Prompts。Resources 在我这个项目里的典型用途是暴露手机的上下文数据源比如当前连接的网络状态、地理位置、日历日程、剪贴板内容。这里有一个边界问题需要想清楚资源数据是“由模型按需读取”的不是一股脑全塞给模型的。如果你把一个包含数百条联系人数据的列表暴露为 Resource模型读取的时候会消耗大量 token而且大部分数据根本用不上。我的做法是把资源访问做成“两级结构”第一级是资源目录只返回类型和元数据第二级才是具体内容。比如device://context/network这个 URI读取时只返回网络类型、信号强度、是否代理等轻量信息要想拿详细状态模型会指定device://context/network/detailed。这样既能控制 token 消耗又能保证数据真实可用。Prompts 则适合放一些固定的任务模板。比如“生成一条短信回复”这个任务它的 prompt 可以预置为“你是一个手机助手请根据用户的意图生成一条不超过70字的中文短信并直接返回 JSON”配合工具调用每次执行都复用这段逻辑不用让模型自由发挥。这类模板工程化之后整个应用的响应稳定性会提高很多。2.3 权限确认移动端最大的一道自研题MCP 协议本身没有定义权限确认的交互方式。在桌面端用户基本信任本地执行的工具在移动端这完全行不通。我的方案是引入一个“敏感度分级”机制。每类工具在注册时声明自己的敏感等级低敏感如获取当前时间直接执行中敏感如读取剪贴板在对话流里插入一条确认卡片高敏感如读取位置、发送短信必须弹独立授权页由用户手动确认后后调用。这里踩过一个比较大的坑如果确认弹窗在模型执行完一轮后才出现用户点击“同意”之后上一轮对话的上下文可能已经丢了。所以我处理的方式是在 Host 层拦截工具调用请求先把“用户是否已授权”作为前置条件未授权时直接把工具调用标记为“等待确认”并向模型返回一个中间态。下一轮用户确认后Host 再重新发起调用。这个机制相当于在 MCP 协议之外做了一层自己的状态机但它能解决体验的一致性。type ToolConfidence auto | confirm | strict_confirm;对于strict_confirm级别的工具Host 在生成工具调用参数前会先检查权限缓存如果没有授权记录直接向模型返回“该工具需要用户授权请等待用户操作后重试”然后触发本地授权 UI。这样模型不用猜用户也不会被突然打断得莫名其妙。2.4 会话状态与上下文生命周期移动端还有一个桌面端不需要过度操心的问题一个大模型会话可能持续几十分钟中间用户可能切换 App、接电话、锁屏。会话状态如果全部放在内存里操作系统一杀进程就全丢了。我的选择是把会话核心状态工具调用的请求 ID、授权状态、资源 URI 列表持久化到本地 SQLite同时把大体积的上下文比如历史消息按需重组。这样即使 App 被系统回收重建时依然可以从断点恢复。此外MCP 的 Streamable HTTP 传输会给每个会话分配一个Mcp-Session-Id这个 ID 也需要持久化。移动网络切换时HTTP 连接断掉不可怕只要 Session ID 还在重新连接后仍可恢复上下文。这个 ID 其实相当于服务端会话的一把钥匙丢了就真的从头开始了。3. 实操从零搭建一个 mobile-mcp 运行环境3.1 项目骨架和最小依赖我建议直接用 TypeScript React Native 起步调试时先在 Node.js 环境跑通 MCP Server再挪到移动端。项目初始化可以用你熟悉的 CLI核心依赖只需要一个npm install modelcontextprotocol/sdk其他例如react-native-permissions、react-native-community/geolocation、react-native-community/netinfo看你要接什么本地能力来加。一个最精简的 mobile-mcp 工程大致长这样mobile-mcp/ ├── src/ │ ├── server/ │ │ ├── index.ts # MCP Server 入口 │ │ ├── tools/ │ │ │ ├── notification.ts │ │ │ └── location.ts │ │ └── resources/ │ │ └── device.ts │ ├── host/ │ │ ├── mcpHost.ts # 客户端连接管理 │ │ └── permission.ts # 权限分级控制器 │ └── native/ │ └── bridge.ts # React Native 桥接层 └── package.json我在本地先跑一个纯 Node 版本的 Server用McpServer类注册工具然后通过StdioServerTransport或StreamableHTTPServerTransport对外提供服务。移动端正式运行时不会再走 stdio而是走 HTTP但本地调试 stdio 的速度优势很明显能省掉大量打包和日志断点的时间。3.2 端侧 Server 连一个怎么配Streamable HTTP 传输MCP 目前推荐的 HTTP 传输是 Streamable HTTP它相比早期版本的 HTTPSSE最大的变化是支持在同一个端点平滑处理 GET、POST、DELETE 请求会话管理也更简单。下面是在 Node 环境注册一个基于 Express 的 MCP HTTP 服务的完整最小示例import express from express; import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StreamableHTTPServerTransport } from modelcontextprotocol/sdk/server/streamableHttp.js; const app express(); app.use(express.json()); const server new McpServer({ name: mobile-mcp, version: 0.1.0 }); server.registerTool( send_notification, { title: 发送本地通知, description: 在手机设备上显示一条系统通知, inputSchema: { type: object, properties: { title: { type: string }, body: { type: string }, vibrate: { type: boolean, default: false } }, required: [title, body] } }, async (params) { // 此处映射到原生通知模块 return { content: [{ type: text, text: 通知已发送 }] }; } ); let transport; app.post(/mcp, async (req, res) { transport new StreamableHTTPServerTransport({ sessionIdGenerator: req.body.method initialize ? undefined : undefined }); res.on(close, () {}); await server.connect(transport); await transport.handleRequest(req, res); }); app.listen(3000);这段代码需要注意StreamableHTTPServerTransport每次初始化时会生成Mcp-Session-Id如果复用同一个 transport 变量断线重连时会出现 session 不存在的报错。正确的做法是建立 session 映射表用收到的Mcp-Session-Id找到对应 transport 实例再处理请求。3.3 把手机摄像头变成工具一个完整示例我们来看一个更实际的例子把“拍照”封装成一个 MCP 工具。这个工具在设备端会做三件事申请相机权限、拉起拍照 UI、把拍到的照片保存到本地然后返回图片路径和缩略图描述给模型。注册工具时schema 设计如下server.registerTool( capture_photo, { title: 拍照, description: 调用手机摄像头拍照返回图片路径和基础信息, inputSchema: { type: object, properties: { purpose: { type: string, description: 拍照用途用于权限提示说明 }, returnBase64: { type: boolean, default: false } }, required: [purpose] } }, async (params) { const granted await requestCameraPermission(params.purpose); if (!granted) { return { content: [{ type: text, text: JSON.stringify({ code: PERMISSION_DENIED }) }], isError: true }; } const photo await nativeBridge.capturePhoto(); return { content: [{ type: text, text: JSON.stringify({ path: photo.path, width: photo.width, height: photo.height, size: photo.size }) }] }; } );这里的重点是purpose参数。移动端系统在首次弹权限框时必须说明用途如果调用时没有这个信息只能硬编码一句笼统的“App需要相机权限”用户一看就不想给。把模型的意图比如“帮我把桌上文档拍下来”提取成用途文案授权通过率会明显好很多。3.4 内嵌 Host 怎么连远程 MCP Server如果你的模型跑在云端手机端只需要作为 Host 连接远程 MCP Server那过程更轻量。用官方 SDK 里的Client类import { Client } from modelcontextprotocol/sdk/client/index.js; import { StreamableHTTPClientTransport } from modelcontextprotocol/sdk/client/streamableHttp.js; const client new Client({ name: mobile-host, version: 0.1.0 }); const transport new StreamableHTTPClientTransport({ requestUrl: https://your-mcp-server.example.com/mcp, sessionId: restoredSessionId // 关键恢复持久化的 session }); await client.connect(transport); const tools await client.listTools(); console.log(tools);移动端这里最值得注意的就是sessionId的恢复。如果网络切换后 session 失效服务端会返回 400 或 404。此时你需要重新初始化会话并考虑是否要重置上下文。我在 Host 层做了个简单策略如果连接恢复后 session 已失效先尝试向模型发送一条“上下文可能丢失”的提醒避免模型在缺失历史的情况下执行高风险操作。3.5 鉴权与密钥管理别把 Token 写进 AppMCP 协议对于远程 Server 的鉴权标准是 OAuth 2.1但目前各个实现程度参差不齐。在 mobile-mcp 项目里我采取了一套更务实的做法如果远程 Server 是我们自己控制的就直接用短时 Bearer Token如果对接第三方服务则走 MCP 官方支持的标准 OAuth 流程。但不管你选哪种密钥绝不能硬编码进移动端包里。应用包拿出去逆向一翻密钥就漏了。我的方案是把 Token 放到 KeychainiOS/ KeystoreAndroid首次登录后从服务端换发短期 Token并且每次启动时校验有效期。如果 Token 过期Host 端维护一个刷新队列先让后续请求处于 pending 状态等待刷新完成再放行。4. 常见问题与排查实录4.1 传输层选型对比stdio、HTTP 还是 WebSocketMCP 的传输方式在不同环境下表现差异很大。我实测下来在移动端最实用的还是 Streamable HTTP。维度stdioStreamable HTTP进程模型要求父子进程无要求独立部署适用场景本地快速调试移动端/局域网/远程会话保持依赖进程存活依赖 Session ID端上资源占用高需常驻进程低按请求唤醒断线恢复进程重启即丢失可恢复需服务端支持防火墙穿透不适合适合也可走 HTTPS自己在电脑上调试用 stdio到了手机上老老实实用 HTTP。我在项目早期试过在 React Native 里跑 stdio结果发现 RN 的 Polyfill 对子进程支持是个大坑光解决process.stdin的兼容问题就花了两天最后全砍了换 HTTP。这里给后来者一个忠告不要在移动端折腾 stdio就算能跑也会被系统生命周期搞得痛不欲生。4.2 网络切换和后台冻结的五个必踩坑移动端联调时最烦的就是网络问题。我列一下最容易踩的坑Wi-Fi 切蜂窝TCP 连接直接断Streamable HTTP 的 POST 流也会中断。解决方案监听网络变化事件主动调用 transport 的 reconnect并携带旧的 Session ID。IP 变化导致回话地址失效如果你的 MCP Server 部署在局域网网段一变就全断。建议远程 Server 一律用域名 HTTPS避免 IP 直连。iOS ATS 限制默认禁止 HTTP 明文请求如果你测试时用http://192.168.x.x必须在 Info.plist 里做本地网络声明和 ATS 例外否则接口直接黑屏。后台推送唤醒如果你的工具包括“定时拉取天气并发通知”不能指望 App 一直活着。我最后的方案是走系统推送通道在后台唤醒处理用完立刻释放进程。Android 的电池优化白名单重要 MCP Server 服务需要引导用户把应用加入白名单否则锁屏十分钟后必被冻结。4.3 JSON Schema 复杂度和模型调用成功率这是我在项目里实验性总结出的一个经验规律模型调用工具的成功率和输入 Schema 的复杂度呈反比。特别是嵌套allOf、anyOf、多层级required时模型经常产出非法 JSON。我的建议是每个工具的输入参数控制在 6 个以内所有字段使用最基本的类型string、number、boolean不要用 enum 超过 5 个选项不要搞oneOf这种抽象结构。如果你实在需要多种调用形式拆成两个工具来设计比让模型去理解复杂 schema 可靠得多。可以在 Server 端做一层“Schema 预校验”在真正执行工具前用ajv或zod校验模型传入的参数不合法直接返回错误码例如INVALID_PARAMS并告诉模型具体是哪个字段不合法。模型下一轮就能根据反馈自纠成功率会高不少。4.4 权限弹窗连续触发用户体验的崩盘点移动端最容易让用户体验崩掉的一个场景是模型连续调用三个高敏感工具导致系统权限弹窗一个接一个弹出来。比如用户说“帮我把当前位置发给正在开会的同事并拍一张现场照片”Host 触发定位权限、通讯录权限、相机权限三个弹窗先后出现任何正常人都会烦躁。我的解法是在权限控制器里加了“弹窗合并”策略一段时间内的多个敏感工具请求会排队最多只弹一个弹窗等到当前请求有了结果之后再弹下一个。同时在界面交互上给用户一个“本次会话允许”的粒度选项而不是每次都弹系统框。这实际上是在系统的“一次性授权”和“永久授权”之间增加了一个动态档位比较符合移动端直觉。4.5 工具执行结果的“大块内容”处理有些工具返回值很大比如读取一个 20MB 的 PDF、或者扫描整个相册的缩略图。直接把全部内容塞进content返回给模型不仅容易把上下文塞爆模型也理解不了。我的办法是对输出做“分层摘要”先返回元数据和摘要具体内容由模型决定是否继续读取。这样既不会丢掉信息又能在 token 约束内完成交互。以文件读取为例返回字段就带uri、size、mimeType、excerpt四项模型如果需要更多可以调用read_file_segment工具按偏移量读。这个设计其实和 MCP Resources 的延迟加载思路是同构的工具调用本身也可以这样。移动端的内存和流量都宝贵别学桌面端那种“拿过来再说”的风格。5. 后续还能往哪走端侧模型 MCP 的组合工具跑通之后我最大的感受是移动端 MCP 真正有潜力的方向是配合端侧小模型做一个完全离线的个人助手。手机上的 MCP Server 把本地所有能力统一暴露给端侧模型模型在本地完成工具调用和决策数据不出设备延迟又低隐私问题也被绕过去了。我在一台中端 Android 机上做过实验端侧模型加 mobile-mcp 的延迟大概在 800 毫秒到 2 秒之间虽然不能和大云端比但日常场景已经够用。另外可以考虑把移动设备作为“边缘 MCP 网关”向家中的其他设备智能家居、路由器、电视盒子暴露操作能力。手机在这里既不完全是 Host也不完全是 Server更像一个可移动的控制面。这类玩法目前文档和最佳实践都还很少但方向很值得关注。最后分享一个我个人的体会做 mobile-mcp 这类项目最忌讳一开始就想着做全。先把“通知 位置 拍照”三个工具跑通能在一个真实对话里被模型连续调用就已经算成功了一大半。后续要加新能力基本都是按模式复制架构不会变。MCP 的设计初衷本是解耦但移动端接入时最重要的反而是控制耦合边界——哪些工具永远在端上哪些工具走远程这个边界想清楚整个项目的气质就稳了。