内容
# 语义 DOM MCP
本地 MCP 服务器(stdio,Node.js + TypeScript)通过 Playwright 驱动真正的 Chromium 浏览器,将实时页面的 DOM 提取到紧凑、事实的**语义 JSON**中,并使用**Playwright 原生定位器**,从而使 AI 生成的 Playwright 测试在整个 QA 团队中保持一致,而不仅仅是准确。
同一页面 → 相同的提取 → 相同的约定 → 相同的测试风格,无论谁运行它。
**证据:** [benchmark/RESULTS.md](benchmark/RESULTS.md) - 在真实页面上,语义 JSON 比原始 DOM 小 **92–97%**,每个定位器都由 Playwright 引擎验证唯一性,并且在运行过程中输出的字节相同。
**文档:** [团队指南(设置 + 连接您的代理)](docs/GUIDE.md) · [基准测试方法](benchmark/README.md) · [路线图](docs/ROADMAP.md)
## 快速入门
无需克隆,无需构建 - 该软件包已在 npm 上发布。一次性浏览器设置(安装与软件包捆绑的 Playwright 匹配的 Chromium 版本):
```bash
npx -y -p semantic-dom-mcp playwright install chromium
```
然后将服务器添加到您的 MCP 客户端:
```json
{
"mcpServers": {
"semantic-dom": {
"command": "npx",
"args": ["-y", "semantic-dom-mcp"],
"env": {
"QA_MCP_ALLOWED_HOSTS": "staging.yourapp.internal,staging.admin.internal",
"QA_MCP_STORAGE_STATE": "./.auth/staging.json"
}
}
}
}
```
这就是整个设置过程。验证方法是通过询问您的代理列出其 MCP 工具 - 您应该看到 `extract_semantic_dom`。有关每个客户端的配置位置(Claude Code、Claude Desktop、Cursor、Windsurf)、身份验证暂存和故障排除,请参见 [docs/GUIDE.md](docs/GUIDE.md)。要从克隆开始运行(贡献者),请参见下面的开发部分。
## 工作流程
1. 询问您的代理: *"提取结账页面并编写一个成功的测试。"*
2. 代理调用 **`extract_semantic_dom({ url })`** - 服务器导航到一个真实的 Chromium 页面,在页面内运行提取器,并返回语义 JSON:每个交互节点都有一个即插即用的 Playwright 定位器,其唯一性由 Playwright 引擎验证。
3. 代理使用 **`write_playwright_test`** 提示(场景 + JSON),该提示注入团队约定。
4. 结果是一个 Playwright 测试,符合团队风格,基于真实的定位器 - 而不是猜测的定位器。
## MCP 接口
| 类型 | 名称 | 用途 |
| --- | --- | --- |
| 工具 | `extract_semantic_dom` | 将 URL 提取到语义 JSON(`url`、`wait_for`、`wait_selector`、`include_hidden`、`max_nodes`)。只读,永不触摸页面。 |
| 工具 | `extract_semantic_dom_after` | 相同,但首先在主框架中运行一个简短的**声明**操作列表(填充/点击/按下/等待,最大 20),并快照结果状态 - 用于 toast、验证错误、打开的对话框。如果操作导航离开允许列表,则拒绝提取。 |
| 工具 | `check_auth` | 诊断:使用配置的 storageState 进行导航,并报告会话是否跳转到类似登录的页面(过期的身份验证显示为答案,而不是谜团)。 |
| 工具 | `list_frames` | 诊断帧树,分类为同源/可达性。 |
| 提示 | `write_playwright_test` | 团队标准测试编写提示(`scenario`、`extract_json`、`team_name?`、`framework_note?`)。 |
| 资源 | `conventions://playwright` | 只读文本的相同团队约定。 |
错误(导航失败、拒绝的主机、缺少的选择器)以结构化的 JSON 返回工具结果 - 代理可以反应而不是崩溃。
## 配置(环境变量)
| 变量 | 含义 |
| --- | --- |
| `QA_MCP_ALLOWED_HOSTS` | **必填。** 服务器可以导航到的主机名的逗号分隔列表。默认情况下拒绝导航。支持 `host`、`host:port` 和 `*.domain` 条目。 |
| `QA_MCP_STORAGE_STATE` | Playwright `storageState` JSON 的可选路径,用于预认证的暂存会话。**此文件保存活动会话 - 它被 gitignored; 永远不要提交它。** |
| `QA_MCP_TEAM_NAME` | `write_playwright_test` 提示中使用的可选团队名称(默认为 `QA`)。 |
## 安全姿态
- 工具输入不受信任(它们通过 LLM 到达):严格的模式(`additionalProperties: false`)、仅 http/https、主机允许列表在导航前强制执行。
- `extract_semantic_dom` 仅**读取** DOM - 它永远不会点击、提交或改变页面。唯一的例外是 `extract_semantic_dom_after`,它只执行显式、有界限、模式验证的操作列表,从不记录填充值,如果页面离开允许列表,则在不提取的情况下中止。
- 没有超出目标 URL 导航的网络出口。没有遥测。页面内容永远不会被记录(stderr 只携带高级别事件),并且不会在当前调用之外存储。
## 值得了解的语义
- **快照诚实度:** JSON 是一个单一的时刻。禁用的提交按钮被报告为 `is_disabled: true`,并附有注释 - 约定指示模型编写改变状态的交互,而不是假设它保持禁用状态。
- **隐藏节点被包含**并标记为 `is_visible: false`(测试通常断言隐藏性);传递 `include_hidden: false` 以删除它们(删除的计数被记录,而不是沉默的)。
- **开放的影子 DOM** 被遍历并标记为 `in_shadow` - 定位器原生地刺穿它,因此永远不会发出 `>>>`/`::shadow` CSS。**封闭的影子根** 显示为 `shadow_boundary` 标记节点(通过预导航 `attachShadow` 检测仪器检测;由*声明性影子 DOM* 创建的封闭根在脚本运行之前解析,不能被检测到)。
- **同源 iframe** 按帧提取,设置 `frame_path`(按顺序链接 `frameLocator()`)。**跨源 iframe** 被记录为不透明的 `cross_origin_frame` 节点,仅包含 URL/名称 - 它们的 DOM 永远不会被触摸。
- **通知和对话框表面**(`role="alert"`、`role="status"`、对话框)像交互节点一样被提取。当 toast 库保持活动区域为空并在 sibling 中渲染消息时(一种跨 UI 库的常见模式),消息文本从封闭容器中提取并标记。对于 UI,在交互后呈现迟缓,`wait_selector_after` 在 `extract_semantic_dom_after` 上确定性地等待,而不是猜测 `settle_ms`。由于这些 ARIA 角色从作者(不是内容)那里获取名称,因此它们的角色定位器为 `getByRole('alert')` - 或者当存在 `aria-label` 名称时,使用 `aria-label` 名称。对于仅在交互后出现的 UI(登录成功 toast),使用 `extract_semantic_dom_after`。
- **JS 点击卡**(具有无锚点/角色/测试 ID 的产品图块)根据设计对事实规则不可见 - 传递 `include_click_targets: true` 以包含具有内容的 cursor-pointer 边界元素,标记为启发式并通过其标题文本定位。
- **链接携带 `href`**(模式 1.1),因此代理可以发现下一个要提取的页面,而无需进行爬取。**框架生成的 ID**(`rc_select_*`、React `useId`、Radix、MUI...)被检测并降级为最后的手段,并附有注释 - 它们在构建之间更改,因此永远不应该是主要的。
- **`viewport: "mobile"`**(375×812,触摸)快照响应式状态;可见性标志反映活动的媒体查询。
- **截断是响亮的:** `max_nodes` / 深度上限设置 `truncated: true` 加上注释。非唯一定位器携带 `is_unique: false` 和 `disambiguation` 指导。
## 开发
```bash
git clone https://github.com/helmif/semantic-dom-mcp.git && cd semantic-dom-mcp
npm install
npx playwright install chromium
npm run dev # 通过 tsx 在 stdio 上运行服务器
npm run typecheck # tsc --noEmit(严格)
npm test # Vitest 套件针对 headless Chromium 中的真实fixture页面
npm run build # 编译到 dist/(客户端可以使用“command”:“node”,“args”:["<path>/dist/index.js")]
```
存储库布局:`src/index.ts`(引导)· `src/server.ts`(MCP 接口)· `src/browser.ts`(Playwright 层 + 协调)· `src/extractor/`(页面内引擎 + 定位器解析)· `src/types.ts`(冻结 v1 合同)· `src/conventions.ts`(团队约定的单一来源)。