如果你和我一样,白天在电脑前跑了好几个 MCP Server,晚上只想窝在沙发上用手机让 AI 去查点电脑里的东西,你大概率会碰一鼻子灰——MCP(Model Context Protocol)这词最近热得不行,但真要把它搬到手机上,你会发现它的传输层设计默认只服务桌面端。我折腾了两三天,搭了一套mobile-mcp的桥接方案,现在手机和电脑之间调工具已经非常顺手了。下面这些内容是我整理出来的完整思路和踩坑记录,写给所有想从手机/平板接入 MCP 的朋友。
1. MCP 天生偏心桌面端:两个传输细节决定了你连不上
1.1 stdio 传输意味着 MCP Server 与你必须同机
MCP 的底层是 JSON-RPC 2.0 消息,它定义了客户端和服务端之间怎么交换initialize、tools/list、tools/call这些方法。但协议里最常用的第一种传输方式是 stdio——也就是客户端启动一个子进程,通过标准输入和标准输出跟它对话。
这种方式对桌面端非常友好:Claude Desktop 在本地启动 Python 或 Node 写的 MCP Server,用管道灌数据,复杂度低、不需要管网络和端口。但放到手机上就麻烦了,手机上的 AI 应用压根没有"子进程"这个概念,它没法去 spawn 一个跑在你家电脑上的进程,更没法跟这个进程共享 stdin/stdout。所以想让手机和电脑上的 MCP Server 通信,第一件事就是绕开 stdio 这条路。
1.2 HTTP + SSE 虽然走网络,但默认绑定回环地址
MCP 后来引入了 HTTP + SSE 传输。Server 监听一个本地端口,客户端先通过 GET/sse建立 Server-Sent Events 长连接,拿到一个 session id,然后通过 POST 往这个 session 发请求,服务端把结果通过 SSE 推回来。这种模式理论上天然支持远程调用,但坑在于绝大多数 MCP Server 的默认配置只监听127.0.0.1。你在电脑上启动服务,它只对本机回环地址开放,手机当然连不上。
就算你手动改成监听0.0.0.0,也只是解决了"地址可达"的问题。手机和电脑不在同一局域网的话,中间还有家用路由器 NAT、运营商 NAT,还有就是 SSE 本身是单向的——服务端只管推,客户端要用另一个 HTTP 请求回传数据。在移动网络环境下这种"一长一短"两条通道的模型不是不能做,但从工程角度来说,它比 WebSocket 这类全双工通道要别扭不少。
1.3 鉴权模型:默认零信任本地,远程暴露是红线
MCP 协议目前对鉴权没有内置标准。它的默认假设是"客户端和服务端跑在同一台可信机器上",所以本地开发时你几乎不需要考虑 token、密钥、权限这些事。可一旦你想把 MCP Server 暴露给手机、暴露到公网,"零信任"立刻变成"裸奔"。
更麻烦的是,移动端的 MCP 客户端通常只提供一个地址输入框,你没地方填 Header。于是 token 只能被塞进 URL query 参数里,例如wss://your-host:8765/mcp?token=xxxx。这意味着网关必须同时支持从 query 和 Header 两种位置取 token,否则就会遇到"桌面客户端能连、手机客户端连不上"的尴尬。这些细节在官方文档里基本是空白,谁踩谁知道。
2. mobile-mcp 的架构思路:把手机当作 MCP 远程客户端来对待
2.1 正道不是"手机装 Server",而是"手机连网关"
我最初的想法非常天真:在手机上装一个 MCP Server 不就行了?试了一圈发现完全没必要。手机既没有足够的算力承载大模型推理,也不该承担工具执行的繁重工作。真正的需求是:手机上的 AI 助手作为 MCP 客户端,去调用电脑上的工具——查本地文件、跑浏览器自动化、执行 Python 脚本、读取开发服务器状态。
所以mobile-mcp的架构其实是三层:
手机上的 MCP 客户端 --- wss ---> 自建网关 --- SSE/stdio ---> 本机 MCP Server手机是远端 MCP Client,网关充当远程 MCP Server;网关再作为本机 Client 去连电脑上的 MCP Server。这样做的好处是,本地那堆 MCP Server 完全不需要改动,它们依然监听127.0.0.1,由网关做转发即可。手机端也只需要配一个 wss 地址,跟用一台远程服务器没什么区别。
2.2 为什么选 WSS + token:一次真机抓包的教训
网关的对外接口我一开始图省事用了明文ws://,想着反正自己家里用,问题不大。后来用手机流量在外面测试时顺手抓了一次包,看到整个 JSON-RPC 请求和响应明文躺在网络里,token 也跟在 query 后面原样传输,瞬间就慌了。移动网络环境不像家里局域网那么可信,运营商节点、公共 WiFi 都有可能被嗅探,换 WSS 是必须的,没有任何商量余地。
WSS 本质上就是 WebSocket over TLS,既能保证数据加密,又是全双工长连接,很适合 MCP 这种"客户端持续发请求、服务端持续推结果"的交互模式。而 token 放在 query 里是因为移动端 MCP 客户端的配置界面普遍只支持填一个 URL。网关里把 query 参数和 Authorization Header 都解析一遍,两种方式都能认证,实测下来兼容性最好。
2.3 与小智 MCP 这类公共网关平台的路线对比
折腾的过程中我也关注过被大家提到很多的小智 MCP 平台,它属于"公共 MCP 网关"路线:平台已经帮你把一堆 MCP Server 聚合成一个 wss 端点,用户只需要在客户端里填一个类似wss://api.xiaozhi.me/mcp/?token=xxx的地址就能用。这种方案对不想维护服务器、不想折腾内网穿透的同学非常友好,开箱即用,工具也丰富。
但公共网关有两个绕不开的问题:一是工具和数据会经过第三方平台,对涉及本地文件、浏览器会话、私人笔记这类场景,心理上总觉得不踏实;二是无法针对你家里的专属工具做定制。mobile-mcp的价值恰恰在这里——它只解决"通道"问题,后面的工具链完全由你自己掌控。两条路线也可以组合:公共网关用来覆盖通用需求,自建网关用来接入本地私有工具,互补使用。
3. 自建 mobile-mcp 网关:从零到手机可用的完整过程
3.1 网关选型与目录结构
技术栈我选了 Node.js 20 +ws库,理由很简单:MCP 官方 TypeScript SDK 就是 Node 生态,ws的 WebSocket 服务端实现非常稳定,一个文件就能跑起来。本地 MCP Server 我用了一个跑在127.0.0.1:8931的 Playwright MCP 做测试,它的 SSE 端点路径是/sse,很适合验证网关转发链路。
目录结构保持极简:
mobile-mcp/ ├── gateway.js # 网关节点的核心逻辑 ├── package.json └── logs/ # 运行日志依赖只需要ws:
npm init -y npm install ws3.2 核心代码:SSE 到 WebSocket 的协议桥
网关的核心逻辑说起来只有三件事:校验 token、跟上游 MCP Server 建立 SSE 连接、把手机发来的 JSON-RPC 请求转发过去,再把上游推回来的消息原样推回手机。下面这个就是最小可运行版本:
import { WebSocketServer } from 'ws'; const TOKEN = process.env.MCP_TOKEN || 'dev-token-change-me'; const UPSTREAM_SSE = 'http://127.0.0.1:8931/sse'; const PORT = 8765; const wss = new WebSocketServer({ port: PORT, path: '/mcp' }); let sessionId = ''; wss.on('connection', async (ws, req) => { const queryToken = new URL(req.url, 'http://localhost').searchParams.get('token'); const headerToken = req.headers['authorization']?.replace('Bearer ', ''); if (queryToken !== TOKEN && headerToken !== TOKEN) { ws.close(4001, 'invalid token'); return; } console.log('[mcp] client connected'); // 连上游 SSE const sse = await fetch(UPSTREAM_SSE, { headers: { Accept: 'text/event-stream' } }); const reader = sse.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; const pump = async () => { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const chunks = buffer.split('\n\n'); buffer = chunks.pop() || ''; for (const chunk of chunks) { const dataLine = chunk.split('\n').find(l => l.startsWith('data:')); if (!dataLine) continue; const payload = dataLine.slice(5).trim(); try { const parsed = JSON.parse(payload); if (parsed.id === 'connection-ready') { sessionId = parsed.result?.sessionId; console.log('[mcp] upstream session:', sessionId); continue; } } catch {} ws.send(payload); } } }; pump(); ws.on('message', async (raw) => { const json = JSON.parse(raw.toString()); await fetch(UPSTREAM_SSE, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Mcp-Session-Id': sessionId }, body: JSON.stringify(json) }); }); ws.on('close', () => { reader.cancel().catch(() => {}); console.log('[mcp] client disconnected'); }); }); console.log(`[mobile-mcp] listening on wss://0.0.0.0:${PORT}/mcp`);注意:这只是演示级代码,生产使用建议以 MCP 官方 SDK 为基础再加错误处理和重试。协议里
tools/call这类耗时操作的返回往往不是 POST 响应的 body,而是通过 SSE 推回的message事件,所以转发逻辑里"读 SSE、推给 WebSocket"的 pump 循环才是主干,POST 只是把请求塞进去。
3.3 手机端配置:把 wss 地址填进客户端
手机端配置比我想象的简单,前提是找对客户端。目前支持 MCP 且允许自定义 wss 地址的移动 AI 应用还不多,但基本都是同一个套路:
- 打开应用里的"模型服务"或"MCP 配置"入口。
- 新建一个 MCP Client,填服务地址:
wss://你的域名或IP:8765/mcp?token=你生成的长随机串。 - 保存后应用会自动发起
initialize握手,成功的话会显示协议版本号。 - 在对话里直接说"帮我调用某个工具",或在工具面板里看到已加载的工具列表。
如果暂时没有合适的手机应用,也可以用浏览器调试。电脑上和手机同一局域网时,直接在手机浏览器控制台跑一个小脚本验证链路:
const ws = new WebSocket('ws://192.168.1.100:8765/mcp?token=xxxx'); ws.onopen = () => { ws.send(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'mobile-mcp-debug', version: '0.1' } } })); }; ws.onmessage = e => console.log(JSON.parse(e.data));浏览器能收到响应,说明整条链路已经通了,再回到 App 配置就能少走弯路。
3.4 验证链路:握手、工具列表、真实调用
我在验证时习惯按三步走,每一步都在网关日志里确认状态。首先是握手:手机 App 连接后,网关日志会打印client connected和upstream session,说明 MCP 协议层已经建立了 session。这一步失败九成是 token 不匹配或 WSS 证书问题。
其次是工具列表:告诉 AI"把可用工具列出来",如果网关日志里出现tools/list的 JSON-RPC 请求,而且 App 工具面板显示了 Playwright 相关工具,说明tools/list走了完整回路。最后是真实调用,比如让 AI 打开百度首页并截图。这一步会同时看到tools/call请求和 SSE 推回的 tool result,跑通后基本就可以放心日常使用了。
4. 真机踩坑记录:四个让我差点放弃的问题
4.1 握手成功但 tools/list 返回空
第一次在手机上连上后,initialize正常,session 也拿到了,但工具列表一直是空的。排查了半天发现不是网关的问题,而是本地那个 MCP Server 的tools/list懒加载了一堆依赖,启动时没读到环境变量PLAYWRIGHT_BROWSERS_PATH,导致浏览器相关的工具初始化失败被默默过滤掉了。
解决办法是在网关进程的环境变量里把这些依赖都配上,然后重启网关。由此我总结出一个经验:网关只是一个通道,本地 MCP Server 本身能否正常工作、能否列出工具,你得先把它在桌面端调通,再连手机上测试——不要在链路中间加变量。
4.2 WSS 连接几秒就被断开
手机锁屏后 WebSocket 连接经常被系统挂起,或者被运营商 NAT 的空闲超时掐断。表现为连接建立后几分钟没有任何消息,再发请求就超时。WebSocket 协议本身有 ping/pong 帧,但很多移动端客户端并不会主动发心跳,所以得在网关侧主动做心跳。
我在代码里加了一个 30 秒定时器,向所有连接发送 ping,如果 30 秒内没收到 pong,就主动关闭连接,让客户端重连。这个改动工作量很小,但稳定性提升非常明显。锁屏后回来再解锁,连接通常也在几秒内恢复,不会卡死。
4.3 超时参数是不是越小越好
移动端应用对超时时间的设置也是个大坑。很多客户端默认只有 10 秒超时,对initialize和tools/list这些操作够用,但tools/call一旦碰到耗时工具就完蛋。我用 Playwright 控制浏览器打开复杂页面时,一个navigate加上等页面渲染,经常要十几秒甚至更久,10 秒超时直接判负。
我的建议是分类型设置:
| 请求类型 | 建议超时 | 原因 |
|---|---|---|
| initialize | 10-15 秒 | 能力协商,正常情况很快 |
| tools/list | 20-30 秒 | 部分 Server 首次懒加载较慢 |
| tools/call | 60-120 秒 | 浏览器自动化、脚本执行等任务天然耗时 |
超时不是越小越好,要给工具调用留足余量,否则体验就是"动不动报错重试"。
4.4 端口暴露在公网后被扫描器盯上
把 8765 端口映射到公网后,我当天就收到一堆来自陌生 IP 的异常连接尝试,几乎都是扫到端口后随便发点数据试试运气。其实这不算漏洞,但提示了一个关键点:不要直接把 Node 进程暴露在公网,前面必须有一层 TLS 终止和访问控制。
我的做法是用 Nginx 做反向代理,由它来挂证书、终止 TLS,再把解密后的流量转发到本机 8765。这样 Node 进程只需要监听回环地址,公网看到的是一个标准的 HTTPS 端口,安全性和隐蔽性都高很多。
5. 安全加固与把 mobile-mcp 用到极致的几种姿势
5.1 哪怕只给自己用,也要加上这三层防护
mobile-mcp是自己的私人通道,但"私人"不代表可以裸奔。我最后落地的方案是三层防护叠在一起。第一层是 token,用openssl rand -hex 32生成一个 64 位十六进制随机串,不要用生日、手机号这种可猜的内容。第二层是网络层,用 Nginx 做反向代理加 IP 白名单,只放行自己的手机 IP 或家庭宽带 IP 段。第三层是应用层限流,在网关里做一个简单的计数器,同一条连接在单位时间内只能发 N 次请求,超出就断开。
以下是我用的 Nginx 配置片段,注意 WebSocket 的 Upgrade 头必须带上:
server { listen 443 ssl; server_name mcp.example.com; ssl_certificate /etc/letsencrypt/live/mcp.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/mcp.example.com/privkey.pem; location /mcp { proxy_pass http://127.0.0.1:8765; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; } }5.2 手机端真正好用的 MCP 工具组合
通道打通后,价值就体现在能接哪些 MCP Server 上。我最常用的组合有三个:Playwright MCP 负责浏览器自动化,让 AI 用手机指挥电脑打开网页、截图、抓取页面结构;Chrome DevTools MCP 负责调试前端项目,改样式不用爬起来跑到电脑前敲 F12;还有一个本地笔记类的 MCP Server,专门用来检索我电脑上的 Markdown 笔记和项目文档。手机现在更像是遥控器,而电脑端保留了所有工具的完整执行能力。
| 工具 | 适用场景 | 手机端体验 |
|---|---|---|
| Playwright MCP | 浏览器操作、网页截图 | 让 AI"打开某个页面截图给我看" |
| Chrome DevTools MCP | 前端调试、DOM/样式修改 | 对页面元素做实时调整 |
| 本地文件/笔记 MCP | 检索本地资料 | 查项目文档、找历史笔记 |
| 自动化脚本 MCP | 执行命令、跑批处理 | 远程触发编译、执行脚本 |
5.3 后续还能怎么扩展:多 Server 聚合、转发、审计
网关结构是天然可扩展的,我现在正在做的是把多个本地 MCP Server 聚合成一个入口。手机端只填一个 wss 地址,网关拿到tools/list请求后,分别向多个上游 Server 询问工具列表,汇总后再返回。这样手机端不用每个工具配一个地址,体验更接近"一个入口、全部工具"。
再往下还可以做按 token 粒度的授权控制:不同的手机配不同的 token,有的 token 只能访问浏览器工具,有的 token 可以访问文件工具,相当于一个极其简单的权限系统。网关日志里还可以记录每一次工具调用的时间、参数、结果大小,哪天想复盘 AI 到底在背后干了什么,一查日志就清楚了。
最后分享一个我在实际使用中的体会:别一上来就追求公网和异地上网,先把 WSS 在局域网里跑通,手机和电脑连同一个路由器,验证完整调用链之后再加公网反向代理。这样能把变量隔离开,出了问题也容易定位。mobile-mcp这套方案本身不复杂,真正复杂的是它接入的生态——你现在有了一个可以随时从口袋里掏出来用的 MCP 远程入口,接下来能玩出什么花样,完全取决于你本地接了多少好东西。