weapp-driver-mcp
by Gao12123
README.md
# weapp-driver-mcp
让 Claude、Codex 等 AI agent 像用 Playwright 那样,对**微信小程序**做端到端测试的 MCP server。
底层是微信官方的 [`miniprogram-automator`](https://www.npmjs.com/package/miniprogram-automator)。本项目的价值不在"再包一层 SDK",而在**把 SDK 的坑在适配层修掉**,给 agent 一个可预测、可诊断、可自愈的工具面。
仓库:<https://github.com/Gao12123/weapp-driver-mcp>(尚未发布到 npm,安装见下)
---
## 为什么不用现成的
读 `miniprogram-automator@0.13.0` 的源码可以确认几件事(不是猜测,是 `out/*.js` 里的事实):
| SDK 行为 | 直接暴露给 agent 的后果 | 本项目的处理 |
| --------------------------------------------------------------------- | -------------------------------------- | --------------------------------------------------- |
| `Page.waitFor(selector)` 内部 timeout 默认 `0` | 服务端**永久挂死** | 自己实现有界轮询,超时带回最后观察值 |
| `Page.$()` try/catch 吞掉所有错误返回 `null` | "不存在" 和 "连接断了" 无法区分 | 一律走 `$$()` |
| `changeRoute()` 硬编码 `sleep(3000)`;`launch()` 硬编码 `sleep(5000)` | 每次导航固定等 3 秒 | 自己轮询 `currentPage()`,快时几十毫秒返回 |
| `Launcher.launch()` 的 `spawn` 句柄不外泄 | MCP 退出后 DevTools 变孤儿进程、占端口 | 自己 spawn 并持有 PID,关闭/信号时清理 |
| `Connection.send` 无超时;断连只报 `Connection closed` | 一次调用可能挂到天荒地老 | 每命令 15s 上限,断连归类为 `devtools_disconnected` |
| `Element.attribute()` 键不存在时返回 `undefined` | "属性为空" 和 "属性不存在" 混淆 | 读取结果区分 `present` / `value` |
## 工具清单(13 个)
| 工具 | 作用 |
| ------------------- | ----------------------------------------------------------------------------------------- |
| `mp_launch` | 拉起开发者工具并连接(需要 `project_path`) |
| `mp_close` | 断开并关闭本次拉起的开发者工具(幂等) |
| `mp_status` | 会话状态、当前页、页面栈、视口、活跃 mock、日志游标 |
| `mp_page` | 观察当前页:路由、页面数据、视口、滚动位置、页面栈、WXML |
| `mp_query` | 按选择器列出匹配元素(tag / class / 截断文本 / index) |
| `mp_element_action` | tap / longpress / input / trigger / 触摸 / 滚动 / swipe / slide / setData / 组件方法 |
| `mp_element_read` | 一次读多个字段:text / value / size / offset / wxml / data / attribute / property / style |
| `mp_wait` | 有界等待:元素出现/消失、文本包含、路由、页面数据等于某值 |
| `mp_navigate` | navigateTo / redirectTo / reLaunch / switchTab / navigateBack,等真正落地才返回 |
| `mp_screenshot` | 截图,**内联返回图片**,可选落盘 |
| `mp_console` | 增量读取 console 与未捕获异常(有界环形缓冲 + 丢弃计数) |
| `mp_wx` | 调用或 mock `wx.*` API(这是 automator 唯一提供的请求控制手段) |
| `mp_expect` | 断言,返回 `pass` + 实际值;断言失败**不是**工具错误 |
设计要点:
- **选择器无状态**。不返回不透明 handle(重渲染即失效、agent 也读不懂)。每次动作重新解析选择器,失败时附上**同层候选元素**(含 BEM 基类降级),帮 agent 自我纠正而不是盲目重试。
- **`mp_expect` 失败返回 `pass: false`**,附截图,不当工具错误。一次运行能暴露所有失败的断言。
- **任何工具失败自动取证**:当前页路由与数据摘要、最近 20 条 console/异常、一张截图。取证有 5 秒硬上限,取证失败不会掩盖原错误。
- **严格类型比较**。`"false"` 不等于 `false`,`1` 不等于 `"1"`。agent 从页面数据里读出来的值经常类型不一致,静默转换会把真失败变成假通过。
## 安装
包尚未发布到 npm,先从仓库安装:
```bash
git clone https://github.com/Gao12123/weapp-driver-mcp.git
cd weapp-driver-mcp && npm ci && npm run build
```
然后在 MCP 客户端里指向构建产物 `dist/main.js`。
**Claude Code:**
```bash
claude mcp add weapp -- node /absolute/path/to/weapp-driver-mcp/dist/main.js
```
**Codex:**
```bash
codex mcp add weapp -- node /absolute/path/to/weapp-driver-mcp/dist/main.js
```
**通用 MCP 配置(Claude Desktop 等):**
```json
{
"mcpServers": {
"weapp": {
"command": "node",
"args": ["/absolute/path/to/weapp-driver-mcp/dist/main.js"],
"env": {
"WEAPP_OUTPUT_DIR": "/Users/you/weapp-output"
}
}
}
}
```
> 发布到 npm 之后,上面几处的 `node <path>/dist/main.js` 都可以换成 `npx -y weapp-driver-mcp`。
### 环境变量
| 变量 | 默认值 | 说明 |
| ------------------ | -------------------- | ------------------------------------- |
| `WEAPP_OUTPUT_DIR` | `<cwd>/weapp-output` | `mp_screenshot` 的 `save_to` 落盘目录 |
| `WEAPP_LOG_BUFFER` | `2000` | console/异常环形缓冲条数 |
## 前置条件
1. 安装微信开发者工具(本机验证版本 36.6.0)
2. 登录一个能打开该小程序的微信账号
3. 目标项目目录包含 `project.config.json`,且已构建出可运行产物
**服务端口不需要手动开。** 本工具拉起 CLI 时会带上 `--enable-service-port`,开发者工具收到这个参数会同时做两件事:本次启动开启 CLI 服务端口,并把 `security.enableServicePort` 永久置为 `true`。所以第一次运行就够,之后不用再碰设置界面。
如果端口仍然报错(例如你用的版本不认这个参数),`mp_launch` 会把 CLI 的输出模式匹配成可执行提示:
```
Error (launch_failed): ...
Hint: Enable the service port: WeChat DevTools → 设置 → 安全设置 → 服务端口.
```
这条提示对应界面里的 **设置 → 安全设置 → 服务端口** 开关(开启/关闭)。
## 一个典型流程
```
mp_launch { project_path: "/path/to/miniprogram" }
mp_page { include: ["data"] }
mp_query { selector: ".submit" }
mp_element_action{ selector: "#name-input", action: "input", value: "Ada" }
mp_element_action{ selector: ".submit", action: "tap" }
mp_wait { condition: "page_path", path: "pages/result/index" }
mp_expect { target: "page_data", path: "form.submitted", expected: true }
mp_expect { target: "element_text", selector: ".result-name", expected: "Ada" }
mp_console { since_seq: 0 }
mp_screenshot { save_to: "result.png" }
mp_close {}
```
## 开发
```bash
npm ci
npm run check # tsc + oxlint + oxfmt --check + vitest
npm run build
npm run verify:devtools # 真实拉起开发者工具,跑内置 fixture
```
### 真机验证
`npm run verify:devtools` 会跑完整流程:launch → 页面观察 → 查询 → 点击 → 输入 → 导航 → mock wx API → 通过/失败断言 → 失败取证 → 读 console → 截图 → close。
默认使用仓库内的最小 fixture(`fixtures/mini-app`,3 个页面 + 1 个自定义组件 + input/scroll-view)。也可以指向真实项目:
```bash
# 真实项目:只做与项目无关的检查
node scripts/verify-devtools.mjs --project /path/to/your/miniprogram
# 完整 fixture 场景(默认只对内置 fixture 启用)
node scripts/verify-devtools.mjs --scenario fixture
# 换 CLI 或端口
node scripts/verify-devtools.mjs --project /path/to/project --cli /Applications/wechatwebdevtools.app/Contents/MacOS/cli --port 9421
```
脚本的退出码是有意义的:
| 退出码 | 含义 |
| ------ | -------------------------------------------------- |
| `0` | 全部通过 |
| `1` | 有检查项失败(真实的功能问题) |
| `2` | **未验证**:环境无法支撑这次运行,不是通过(见下) |
退出码 `2` 的三种情况:开发者工具缺失或连不上、服务端口未开、以及**连上了但项目在预算内没有渲染出页面**。最后一种在大型项目上真的会碰到——工具会明确说 `rendered: false`,而不是假装页面是空的。
本机实测(开发者工具 36.6.0):
- 内置 fixture:**23/23 通过**,连续三次
- 真实项目 `helper-xcx`(`pages/index/login` 首屏):**11/11 通过**——launch、页面数据、视口、路由、选择器查询命中 9 个真实元素、console 抓到 20 条、截图、状态、失败归类、close
两种模式是分开的:`--project` 指向别的项目时只跑与项目无关的检查,因为 fixture 的选择器和数据断言对真实项目没有意义。要给某个项目跑完整场景,加 `--scenario fixture`。
真机验证过程中暴露并修掉的两个 SDK 问题,值得单独记一笔:
1. **`MiniProgram.on('console')` 会浮空 `App.enableLog` 的 promise**。源码里是 `on(t,e){return"console"===t&&this.send("App.enableLog"),super.on(t,e)}`——返回值被丢弃。大型项目编译慢时它会 reject,没人接,Node 直接以未处理拒绝终止进程,MCP server 在工具调用中途猝死。现在 `lateRejections.ts` 只吞这一类 SDK 传输错误,其他未处理拒绝仍然照常抛。
2. **`Automator.connect()` 成功 ≠ 项目就绪**。socket 一连上就返回,但此时项目还在编译,任何 app 级调用都会挂住。现在 launch 会等到「读到一个页面、隔 800ms 再读一次仍然成功」才算就绪;连接中途被换掉的窗口会被识别为重连而不是交还一个死会话。
### 架构
```
src/
domain/ 纯类型:Result、标签化错误、选择器解析、匹配器、日志环形缓冲、轮询
ports/ MiniProgramSession / SessionLauncher 接口
adapters/
automator/ 唯一接触 SDK 的地方:超时、错误归一化、$$ 绕过、事件订阅、mock 记账
devtools/ spawn CLI、端口探测、CLI 输出诊断、进程组清理
application/ 会话状态机、导航、等待、元素解析、动作、断言、失败取证
mcp/ 13 个工具注册 + zod 入参 + text/image 渲染
```
依赖只向内。SDK 的全部问题收敛在 `adapters/automator`,上层只看到 `Result<T, ToolError>`。
### 已知边界(v1 不做)
- 多会话并行:一次只驱动一个小程序
- CDP 网络抓包:automator 不提供网络拦截,`mp_wx` 只能 mock `wx.*` 的结果
- `native()` 原生弹窗(授权、分享、支付)
- `evaluate()` / `exposeFunction()` 任意代码执行
- Windows 实机验证:代码路径已写,但未在 Windows 上跑过
- 大型项目首屏渲染:验证脚本会把 `rendered: false` 报成未验证,不会自己加长等待
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues