Whistle 远程控制桥接 MCP Server
概述
通过 whistle 代理向手机页面注入 JS 脚本,让电脑端编码助手(Qoder IDE)能够远程操控手机上的 Web 页面。提供 take_snapshot、click、fill、scroll、screenshot 等工具,类似 chrome-devtools-mcp 操控桌面浏览器的体验。
项目路径 :/Users/alsc/Documents/mobile-bridge-mcp/
系统架构图
stdio 模式] BS[Bridge Server
HTTP + WebSocket :9300] W[Whistle 代理
:8899] Q -->|调用 MCP 工具| MCP MCP -->|内嵌启动| BS end subgraph 手机端 BR[手机浏览器] JS[bridge-client.js
注入的脚本] DOM[页面 DOM] BR -->|执行| JS JS -->|操作| DOM end BR -->|WiFi 代理| W W -->|jsAppend 注入脚本| BR JS -->|HTTPS: 同源路径长轮询| W W -->|路径匹配 /__mb__/ 转发| BS JS -->|HTTP: WebSocket 直连| BS
核心设计:双传输模式
问题背景
手机通过 whistle 代理访问 HTTPS 页面时,存在以下限制:
- Mixed Content :HTTPS 页面无法加载
http://资源或建立ws://连接 - 假域名不可达 :
mobile-bridge.local等假域名在 iOS 上会触发 mDNS 解析,绕过 HTTP 代理 - WebSocket 不走代理:手机浏览器的 WebSocket 连接可能不经过 HTTP 代理
最终方案:同源路径 + Whistle 路径转发
bash
HTTPS 页面请求流程:
手机脚本 → fetch('https://当前域名/__mb__/register') → whistle 代理拦截
→ 匹配路径规则 /__mb__/ → 转发到 127.0.0.1:9300 → bridge-server 处理
关键原理:
- 脚本向页面自身域名 发起请求(
location.origin + '/__mb__/...'),属于同源请求 - 同源 HTTPS 请求一定经过 whistle 代理(页面本身就是通过代理加载的)
- Whistle 已对该域名做 HTTPS 拦截(jsAppend 需要),所以能看到请求路径
- Whistle 按路径
/__mb__/匹配规则,将请求转发到本机 bridge-server(HTTP :9300) - 不存在 Mixed Content 问题,不需要假域名,不需要 DNS 解析
交互时序图
MCP 工具一览
列出已连接的手机] MCP --> T2[take_snapshot
获取 DOM 文本快照] MCP --> T3[click
点击元素] MCP --> T4[fill
输入文本] MCP --> T5[scroll
滚动页面] MCP --> T6[evaluate_script
执行任意 JS] MCP --> T7[get_url
获取当前 URL] MCP --> T8[screenshot
截图 base64] MCP --> T9[get_text
获取元素文本]
| 工具名 | 参数 | 说明 |
|---|---|---|
list_clients |
无 | 列出当前连接的手机设备 |
take_snapshot |
clientId? | 获取页面 DOM 文本快照(含 uid 标记) |
click |
uid, clientId? | 点击指定元素(触发 touch + click) |
fill |
uid, value, clientId? | 向输入框填入文本并触发 input/change |
scroll |
uid?, x?, y?, clientId? | 滚动到元素或坐标 |
evaluate_script |
code, clientId? | 执行任意 JS 代码并返回结果 |
get_url |
clientId? | 获取当前页面 URL 和标题 |
screenshot |
uid?, clientId? | 截图返回 base64(依赖 html2canvas) |
get_text |
uid, clientId? | 获取指定元素的文本内容 |
当只有一个客户端连接时,clientId 可省略。
项目结构
bash
/Users/alsc/Documents/mobile-bridge-mcp/
├── src/
│ ├── bridge-client.js # 注入手机页面的客户端脚本
│ ├── bridge-server.ts # HTTP + WebSocket 桥接服务
│ └── mcp-server.ts # MCP Server 入口(内嵌 bridge-server)
├── package.json
└── tsconfig.json
核心模块详解
1. bridge-client.js(手机端注入脚本)
通过 whistle jsAppend 注入到手机页面,核心逻辑:
传输层自动选择:
- HTTPS 页面 → 同源路径长轮询(
fetch(location.origin + '/__mb__/...')) - HTTP 页面 → WebSocket 直连(
ws://电脑IP:9300)
长轮询通信协议:
POST /__mb__/register--- 注册客户端,获取 clientIdGET /__mb__/poll?clientId=xxx--- 长轮询拉取命令(25秒超时返回 204)POST /__mb__/response--- 回传命令执行结果
DOM 快照:
- 使用
data-mb-uid属性动态标记可见且可交互的 DOM 元素 - 输出格式模仿 chrome-devtools-mcp 的 snapshot,便于编码助手理解
诊断标记:
- 脚本执行后会修改页面标题为
[MB] 原标题 - 注册成功后标题变为
[MB:clientId] 原标题
2. bridge-server.ts(电脑端桥接服务)
Node.js 服务,监听 9300 端口,同时处理:
- HTTP 路由 :
/register、/poll、/response、/status、/bridge-client.js - WebSocket:用于 HTTP 页面的直连模式
- 路径兼容 :自动去掉
/__mb__前缀,兼容 whistle 转发的同源请求
核心特性:
- 支持多客户端(通过 clientId 区分)
- 指令通过
msgId匹配请求和响应 - 命令超时机制(默认 15 秒)
- 长轮询挂起等待(25 秒超时)
- 轮询客户端 30 秒无活动自动清理
3. mcp-server.ts(MCP Server 入口)
基于 @modelcontextprotocol/sdk 实现 stdio 模式的 MCP Server:
- 内嵌启动 BridgeServer(同进程)
- 注册 9 个 MCP 工具
- IDE 打开项目时自动启动
Whistle 配置
在 whistle Rules 中添加两条规则:
csharp
# 1. 注入 bridge-client.js 到目标页面
*.ele.me jsAppend://{bridge-client.js}
# 2. 路径匹配: /__mb__/ 开头的请求转发到本机 bridge-server
/\/__mb__\// 127.0.0.1:9300
规则说明:
- 第 1 条:whistle 对
*.ele.me的 HTTPS 响应做拦截,在 HTML 末尾追加 bridge-client.js 脚本内容 - 第 2 条:正则匹配所有包含
/__mb__/路径的请求,转发到本机 9300 端口的 bridge-server
前提条件:
- whistle 已安装根证书到手机(HTTPS 拦截需要)
- 手机 WiFi 代理指向电脑 whistle(默认 :8899)
whistle Values 配置:
- 在 Values 中创建
bridge-client.js,内容为/Users/alsc/Documents/mobile-bridge-mcp/src/bridge-client.js的完整代码
IDE 注册
.vscode/mcp.json:
json
{
"servers": {
"mobile-bridge": {
"type": "stdio",
"command": "npx",
"args": ["tsx", "/Users/alsc/Documents/mobile-bridge-mcp/src/mcp-server.ts"]
}
}
}
IDE 打开项目时自动拉起 MCP 进程并监听 9300 端口。
踩坑记录
1. .local 域名不可用
iOS/macOS 上 .local TLD 触发 mDNS (Bonjour) 解析,绕过 HTTP 代理。mobile-bridge.local 的请求永远不会到达 whistle。
2. WebSocket 不走 HTTP 代理
手机浏览器建立 WebSocket 时可能不经过 HTTP CONNECT 代理,导致 wss://假域名 连接失败。
3. Mixed Content 限制
HTTPS 页面无法发起 http:// 或 ws:// 请求。所有通信必须是 HTTPS/WSS。
4. 最终解决方案
使用同源路径长轮询 :请求页面自身域名 + /__mb__/ 路径前缀,whistle 按路径匹配转发。彻底避免了 DNS、代理、Mixed Content 三大问题。
5. 端口冲突导致 MCP 启动失败
手动 kill 9300 端口进程后,需要 Reload IDE Window 让 IDE 重新拉起 MCP 进程。避免手动启动导致端口冲突。
验证通过的功能
-
get_url--- 获取页面标题和 URL -
evaluate_script--- 远程执行 JS(修改页面标题) - 长轮询通信稳定(register 200、poll 204/200、response 200)
- IDE 自动启动 MCP Server
- HTTPS 页面(h5.ele.me)正常工作