@pyrokine/mcp-chrome 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/README_zh.md +570 -0
- package/dist/cdp/launcher.d.ts.map +1 -1
- package/dist/cdp/launcher.js +41 -19
- package/dist/cdp/launcher.js.map +1 -1
- package/dist/core/types.d.ts +7 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +20 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/unified-session.d.ts +9 -0
- package/dist/core/unified-session.d.ts.map +1 -1
- package/dist/core/unified-session.js +99 -34
- package/dist/core/unified-session.js.map +1 -1
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core/utils.js +21 -8
- package/dist/core/utils.js.map +1 -1
- package/dist/extension/http-server.d.ts +6 -0
- package/dist/extension/http-server.d.ts.map +1 -1
- package/dist/extension/http-server.js +86 -4
- package/dist/extension/http-server.js.map +1 -1
- package/dist/tools/manage.js +1 -1
- package/dist/tools/manage.js.map +1 -1
- package/package.json +9 -6
package/README.md
CHANGED
|
@@ -95,6 +95,15 @@ browse(action="open", url="https://example.com")
|
|
|
95
95
|
extract(type="screenshot")
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
**Optional pairing token**
|
|
99
|
+
|
|
100
|
+
By default, local Extension mode does not require a token. To restrict the Extension connection, start the MCP server
|
|
101
|
+
with `MCP_CHROME_PAIRING_TOKEN`, then enter the same token in the Extension popup:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
MCP_CHROME_PAIRING_TOKEN="your-token" node /path/to/mcp-chrome/dist/index.js
|
|
105
|
+
```
|
|
106
|
+
|
|
98
107
|
### Mode 2: CDP Mode (Fallback)
|
|
99
108
|
|
|
100
109
|
CDP mode launches or connects to a dedicated Chrome instance. Used when the Extension is not installed, or for
|
package/README_zh.md
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
# MCP-Chrome
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
Chrome 浏览器自动化 MCP Server,双模式架构:**Extension 模式**(推荐)操控现有浏览器,**CDP 模式**(回退)启动独立实例
|
|
6
|
+
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://nodejs.org/)
|
|
9
|
+
[](https://modelcontextprotocol.io/)
|
|
10
|
+
|
|
11
|
+
## 功能特性
|
|
12
|
+
|
|
13
|
+
- **双模式**:Extension 模式共享登录状态;CDP 模式用于无头/隔离场景
|
|
14
|
+
- **8 个统一工具**:基于 action 的设计,覆盖浏览、输入、提取、等待、执行、管理、Cookie、日志
|
|
15
|
+
- **多 Tab 并行**:`tabId` 参数支持对任意 Tab 操作,无需切换焦点
|
|
16
|
+
- **iframe 穿透**:`frame` 参数支持操作 iframe 内元素(CSS 选择器或索引,Extension 模式)
|
|
17
|
+
- **语义化定位**:11 种元素定位方式(role、text、label、css、css+文本组合、xpath、坐标等)
|
|
18
|
+
- **自动等待**:内置可点击性、可输入性检测,基于 deadline 的超时预算机制
|
|
19
|
+
- **双输入模式**:`precise`(debugger API,可绕过 CSP)或 `stealth`(JS 注入,无调试横幅)
|
|
20
|
+
- **智能输出**:裸 `return` 语句自动 IIFE 包裹;大结果(>100KB)自动写入文件;`output` 对字符串写入原始文本
|
|
21
|
+
- **多服务器**:Extension 自动发现并同时连接多个 MCP Server 实例
|
|
22
|
+
- **反检测**:可选的指纹伪装和行为模拟
|
|
23
|
+
- **结构化错误**:每个错误包含 code、message、suggestion、context
|
|
24
|
+
|
|
25
|
+
## 兼容客户端
|
|
26
|
+
|
|
27
|
+
| 客户端 | 状态 |
|
|
28
|
+
|----------------|----|
|
|
29
|
+
| Claude Code | ✅ |
|
|
30
|
+
| Claude Desktop | ✅ |
|
|
31
|
+
| Cursor | ✅ |
|
|
32
|
+
| Windsurf | ✅ |
|
|
33
|
+
| 其他 MCP 兼容客户端 | ✅ |
|
|
34
|
+
|
|
35
|
+
## 安装
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install -g @pyrokine/mcp-chrome
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
或从源码安装:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://github.com/Pyrokine/claude-tools.git
|
|
45
|
+
cd claude-tools/mcp-chrome
|
|
46
|
+
npm install
|
|
47
|
+
npm run build
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 快速开始
|
|
51
|
+
|
|
52
|
+
### 模式一:Extension 模式(推荐)
|
|
53
|
+
|
|
54
|
+
Extension 模式操控你现有的 Chrome——共享登录状态、Cookie 和浏览上下文,
|
|
55
|
+
|
|
56
|
+
**第一步:安装 Chrome Extension**
|
|
57
|
+
|
|
58
|
+
1. 在 Chrome 中打开 `chrome://extensions/`
|
|
59
|
+
2. 开启右上角"开发者模式"
|
|
60
|
+
3. 点击"加载已解压的扩展程序" → 选择 `mcp-chrome/extension/dist/` 目录
|
|
61
|
+
4. 工具栏出现 MCP Chrome 图标
|
|
62
|
+
|
|
63
|
+
**第二步:配置 MCP 客户端**
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Claude Code
|
|
67
|
+
claude mcp add chrome -- node /path/to/mcp-chrome/dist/index.js
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Claude Desktop / 其他客户端:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"mcpServers": {
|
|
75
|
+
"chrome": {
|
|
76
|
+
"command": "node",
|
|
77
|
+
"args": [
|
|
78
|
+
"/path/to/mcp-chrome/dist/index.js"
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**第三步:连接**
|
|
86
|
+
|
|
87
|
+
Extension 通过 HTTP/WebSocket 自动连接 MCP Server(端口 19222-19299),点击工具栏图标可查看连接状态,
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
browse(action="list") // 列出所有 Tab
|
|
91
|
+
browse(action="open", url="https://example.com")
|
|
92
|
+
extract(type="screenshot")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**可选配对 token**
|
|
96
|
+
|
|
97
|
+
默认本地 Extension 模式不需要 token,如需限制 Extension 连接,启动 MCP server 时设置 `MCP_CHROME_PAIRING_TOKEN`,再在
|
|
98
|
+
Extension popup 中输入同一个 token:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
MCP_CHROME_PAIRING_TOKEN="your-token" node /path/to/mcp-chrome/dist/index.js
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 模式二:CDP 模式(回退)
|
|
105
|
+
|
|
106
|
+
CDP 模式启动或连接独立的 Chrome 实例,适用于未安装 Extension 或需要无头/隔离的场景,
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# 启动带远程调试的 Chrome
|
|
110
|
+
google-chrome --remote-debugging-port=9222
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
browse(action="connect", port=9222)
|
|
115
|
+
browse(action="open", url="https://example.com")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
> Extension 已连接时,所有工具自动使用 Extension 模式,仅当 Extension 不可用时才激活 CDP 模式,
|
|
119
|
+
|
|
120
|
+
## 可用工具(8 个)
|
|
121
|
+
|
|
122
|
+
### browse - 浏览器管理与导航
|
|
123
|
+
|
|
124
|
+
| Action | 描述 |
|
|
125
|
+
|-----------|-----------------------|
|
|
126
|
+
| `launch` | 启动新 Chrome 实例(CDP 模式) |
|
|
127
|
+
| `connect` | 连接已运行的 Chrome(CDP 模式) |
|
|
128
|
+
| `list` | 列出所有页面/Tab |
|
|
129
|
+
| `attach` | 附加到指定页面/Tab |
|
|
130
|
+
| `open` | 导航到 URL |
|
|
131
|
+
| `back` | 后退 |
|
|
132
|
+
| `forward` | 前进 |
|
|
133
|
+
| `refresh` | 刷新 |
|
|
134
|
+
| `close` | 关闭浏览器连接 |
|
|
135
|
+
|
|
136
|
+
Extension 特有:`list` 返回 flat `targets` 数组和树状 `windows`,每个 target 含 `managed`(Tab 是否由 MCP Chrome 受控)、
|
|
137
|
+
`isActive`(是否为当前操作目标)、
|
|
138
|
+
`windowId`、`index`、`pinned`、`incognito`、`status`(`loading`/`complete`),树状结构含 `windowCount`、`focusedWindowId`、
|
|
139
|
+
`activeTargetId` 和每个窗口按顺序排列的 `tabs`,可区分每个窗口内的 active tab 和当前人眼看到的 focused window 页面,`open`
|
|
140
|
+
自动创建 Tab 分组(cyan 色),`open`、`back`、`forward`、`refresh` 支持 `diagnostics=true`,返回动作期间新增的 console
|
|
141
|
+
warning/error 和失败网络请求摘要,也覆盖首次 `open` 自动创建页面的场景,
|
|
142
|
+
|
|
143
|
+
### input - 键鼠输入
|
|
144
|
+
|
|
145
|
+
事件序列模型,支持任意组合:
|
|
146
|
+
|
|
147
|
+
| 事件类型 | 描述 |
|
|
148
|
+
|-----------------------------------------|----------------------------------------------------|
|
|
149
|
+
| `keydown` / `keyup` | 按键按下/释放 |
|
|
150
|
+
| `click` | 点击(含可操作性检查:可见性、是否启用、遮挡检测、自动滚动) |
|
|
151
|
+
| `mousedown` / `mouseup` | 鼠标按下/释放 |
|
|
152
|
+
| `mousemove` | 鼠标移动 |
|
|
153
|
+
| `wheel` | 滚轮滚动 |
|
|
154
|
+
| `touchstart` / `touchmove` / `touchend` | 触摸事件 |
|
|
155
|
+
| `type` | 输入文本 |
|
|
156
|
+
| `wait` | 事件间暂停 |
|
|
157
|
+
| `select` | 按内容选中文本(鼠标模拟) |
|
|
158
|
+
| `replace` | 查找并替换文本 |
|
|
159
|
+
| `drag` | HTML5 拖放(MAIN 世界 DragEvent,需 `target` 源 + `to` 目标) |
|
|
160
|
+
| `editorContext` | 读取当前编辑器和选区上下文 |
|
|
161
|
+
| `editorInsert` | 在当前编辑器选区插入文本 |
|
|
162
|
+
| `editorCommand` | 执行 `bold`、`insertOrderedList` 等浏览器编辑命令 |
|
|
163
|
+
|
|
164
|
+
参数:`humanize` 启用贝塞尔曲线移动和随机延迟,`diagnostics=true` 返回动作后新增 console warning/error 和失败网络请求,
|
|
165
|
+
`tabId` 指定目标 Tab,`frame` 指定目标 iframe(CSS 选择器或索引),均限 Extension 模式,
|
|
166
|
+
|
|
167
|
+
**`click` 专属参数**:`force: true` 跳过可操作性检查(适用于测试隐藏元素等场景),可操作性失败返回 `ACTIONABILITY_FAILED`,包含
|
|
168
|
+
`rect`、`clickPoint`、遮挡元素、候选遮挡物和修复建议,
|
|
169
|
+
**`type` 专属参数**:`mode="controlled"` 或 `dispatch: true` 直接设置 `.value` 并触发 `input`/`change` 事件,兼容
|
|
170
|
+
React/Vue 等框架的受控组件(键盘事件无法触发状态更新时使用),需要非坐标型 `target`,仅限 Extension 模式,受控输入和目标查找失败时返回结构化
|
|
171
|
+
context,包含 `target`、`matchCount`、`nth`、`activeElement`、`selection` 和候选控件,
|
|
172
|
+
|
|
173
|
+
**`keydown` 专属参数**:
|
|
174
|
+
|
|
175
|
+
- `commands`:触发浏览器原生编辑命令(如 `["selectAll"]`、`["copy"]`、`["paste"]`、`["cut"]`、`["undo"]`、`["redo"]`),仅
|
|
176
|
+
precise 模式可用,stealth 模式下抛错(CDP commands API 没有 JS 事件等价物)
|
|
177
|
+
- 连续 `keydown` 同一 key 自动切换为 `rawKeyDown` + `autoRepeat`,模拟长按重复(与 Puppeteer 一致)
|
|
178
|
+
|
|
179
|
+
### extract - 内容提取
|
|
180
|
+
|
|
181
|
+
| Type | 描述 |
|
|
182
|
+
|--------------|--------------------------|
|
|
183
|
+
| `text` | 提取文本内容 |
|
|
184
|
+
| `html` | 提取 HTML 源码 |
|
|
185
|
+
| `frameHtml` | 提取指定 iframe 的 HTML |
|
|
186
|
+
| `attribute` | 提取元素属性 |
|
|
187
|
+
| `screenshot` | 截图(支持 `target` 元素裁剪) |
|
|
188
|
+
| `state` | 获取页面状态(URL、标题、可交互元素) |
|
|
189
|
+
| `metadata` | 提取页面元信息(标题、OG、JSON-LD 等) |
|
|
190
|
+
|
|
191
|
+
参数:`output` 将结果保存到文件(`images=data` 时为输出目录),`images`(`info`/`data`)提取 HTML 中的图片元信息或数据,
|
|
192
|
+
`frameHtml` 在 `frame` 路由后提取当前 iframe 文档,截图支持 `clip` 坐标区域、`compareWith` PNG 基准对比和 `diffOutput`
|
|
193
|
+
差异图输出,截图响应包含 `metadata.format`、`width`、`height`、`dimensionSource`、`byteSize`、`fullPage`、`scale`、`clip` 和
|
|
194
|
+
`capabilities`,PNG 对比在解码前限制为单个 PNG 25 MiB 和 12,000,000 像素,超出时使用 `clip` 或 `scale` 缩小截图,Extension
|
|
195
|
+
hidden tab 截图返回 `HIDDEN_TAB_SCREENSHOT`,不会自动切前台,`state` 返回 `interactiveElements`,Extension 和 CDP 模式下
|
|
196
|
+
`metadata` 都返回 `frames`,`tabId` 指定目标 Tab,`frame` 指定目标 iframe,均限 Extension 模式,
|
|
197
|
+
|
|
198
|
+
**`attribute` 特殊前缀**:`computed:<属性名>` 返回 computed CSS 样式值(如 `computed:color`、`computed:font-size`),
|
|
199
|
+
`computed:*` 返回全部计算样式(300+ 属性,建议配合 `output` 写文件),
|
|
200
|
+
|
|
201
|
+
**`state` 专属参数**:`depth` 控制 DOM 遍历深度(默认 15),减小可降低大页面的返回数据量,
|
|
202
|
+
`mode` 选择数据源 — `accessibility`(默认,从无障碍树派生)或 `domsnapshot`(CDP `DOMSnapshot.captureSnapshot`,仅 CDP 模式,返回带
|
|
203
|
+
computed style 的扁平节点数组,便于二次处理),
|
|
204
|
+
|
|
205
|
+
### wait - 等待条件
|
|
206
|
+
|
|
207
|
+
| For | 描述 |
|
|
208
|
+
|--------------|----------------------------------------|
|
|
209
|
+
| `element` | 等待元素(visible/hidden/attached/detached) |
|
|
210
|
+
| `navigation` | 等待导航完成 |
|
|
211
|
+
| `time` | 固定延迟 |
|
|
212
|
+
| `idle` | 等待页面加载完成 + DOM mutation 静默期 |
|
|
213
|
+
|
|
214
|
+
参数:`tabId` 指定目标 Tab,`frame` 指定目标 iframe,均限 Extension 模式,
|
|
215
|
+
|
|
216
|
+
**`idle` 说明**:`readyState === 'complete'` 后注入 `MutationObserver`,等待 `ms` 毫秒内无 DOM 变更(默认 500ms),返回
|
|
217
|
+
`domStable: true` 表示 DOM 已稳定,`domStable: false` 表示预算用完时 DOM 仍在变化,
|
|
218
|
+
|
|
219
|
+
### evaluate - JavaScript 执行
|
|
220
|
+
|
|
221
|
+
在页面上下文执行 JavaScript,
|
|
222
|
+
|
|
223
|
+
| 参数 | 描述 |
|
|
224
|
+
|---------------|-------------------------------------------------------------|
|
|
225
|
+
| `script` | JavaScript 代码,裸 `return` 语句自动包裹 IIFE |
|
|
226
|
+
| `scriptFile` | 从本地文件读取脚本(与 `script` 二选一,相对路径默认走受控临时目录,仓库内文件请显式写 `cwd:`) |
|
|
227
|
+
| `args` | 传递给脚本的参数(script 须为函数表达式) |
|
|
228
|
+
| `mode` | `precise`(默认,debugger API)或 `stealth`(JS 注入) |
|
|
229
|
+
| `output` | 将结果保存到文件(相对路径默认走受控临时目录,写入仓库请显式写 `cwd:`,字符串写原始文本,其他类型写 JSON) |
|
|
230
|
+
| `tabId` | 指定目标 Tab(Extension 模式) |
|
|
231
|
+
| `frame` | 指定目标 iframe(CSS 选择器或索引,Extension 模式) |
|
|
232
|
+
| `timeout` | 端到端超时预算(毫秒) |
|
|
233
|
+
| `diagnostics` | 执行后返回新增 console warning/error 和失败网络请求摘要 |
|
|
234
|
+
|
|
235
|
+
`script` 和 `scriptFile` 至少提供一个,互斥使用,相对 `scriptFile` 和 `output` 路径默认写到 `mcp-chrome`
|
|
236
|
+
管理的系统临时目录,需要把文件留在当前工作目录时,用 `cwd:relative/path` 显式指定,相对路径会拒绝 `..`,Windows 下也会拒绝
|
|
237
|
+
`:`,避免落到 NTFS alternate data stream,
|
|
238
|
+
|
|
239
|
+
结果超过 100KB 时自动写入文件到受控的系统临时目录,返回文件路径和大小,返回 DOM 节点、`NodeList` 或 `HTMLCollection` 时返回
|
|
240
|
+
`NON_SERIALIZABLE_EVALUATE_RESULT`,并提示改为返回 `textContent`、`outerHTML` 等简单字段,
|
|
241
|
+
|
|
242
|
+
### manage - 页面与环境管理
|
|
243
|
+
|
|
244
|
+
| Action | 描述 |
|
|
245
|
+
|----------------|--------------------------------------|
|
|
246
|
+
| `newPage` | 新建受控页面/Tab |
|
|
247
|
+
| `closePage` | 关闭受控页面并返回 `affected.before/after` |
|
|
248
|
+
| `adoptPage` | 将已有 Tab 标记为受控页,不切前台 |
|
|
249
|
+
| `releasePage` | 将受控页移出管理,不关闭页面 |
|
|
250
|
+
| `movePage` | 将受控 Tab 移到指定窗口或 index |
|
|
251
|
+
| `reorderPage` | 调整受控 Tab 在当前窗口内的顺序 |
|
|
252
|
+
| `pinPage` | 固定受控 Tab |
|
|
253
|
+
| `unpinPage` | 取消固定受控 Tab |
|
|
254
|
+
| `activatePage` | 激活受控 Tab 并聚焦所在窗口 |
|
|
255
|
+
| `focusWindow` | 聚焦显式指定的窗口 |
|
|
256
|
+
| `resizeWindow` | 调整显式指定窗口的尺寸或状态 |
|
|
257
|
+
| `newWindow` | 新建受控窗口 |
|
|
258
|
+
| `closeWindow` | 仅当窗口内全是 managed tab 时关闭窗口 |
|
|
259
|
+
| `clearCache` | 清除缓存/存储(清除 Cookie 请用 `cookies` 工具) |
|
|
260
|
+
| `viewport` | 设置视口大小 |
|
|
261
|
+
| `userAgent` | 设置 User-Agent |
|
|
262
|
+
| `emulate` | 设备模拟(iPhone、iPad 等) |
|
|
263
|
+
| `inputMode` | 查询或设置输入模式(`precise` / `stealth`) |
|
|
264
|
+
| `stealth` | 注入反检测脚本 |
|
|
265
|
+
| `cdp` | 发送原始 CDP 命令(高级,如 `Runtime.evaluate`) |
|
|
266
|
+
|
|
267
|
+
Tab/window 管理动作仅支持 Extension 模式,改变浏览器可见状态的动作必须显式传 `targetId` 或 `windowId`,并返回
|
|
268
|
+
`affected.before/after`,`closeWindow` 遇到混有非托管 tab 的窗口会返回 `WINDOW_HAS_UNMANAGED_TABS`
|
|
269
|
+
|
|
270
|
+
**Stealth 模式档位**(CDP launch 参数,通过 `browse action=launch stealth=...` 设置):
|
|
271
|
+
|
|
272
|
+
- `off` — 关闭反检测(纯净模式,适合测试/CI)
|
|
273
|
+
- `safe`(默认)— 最小改动(移除 `navigator.webdriver`、清理 CDP 痕迹)
|
|
274
|
+
- `aggressive` — 增加少量 WebGL/插件/语言等指纹修补(不等于完整伪装)
|
|
275
|
+
|
|
276
|
+
### logs - 浏览器日志
|
|
277
|
+
|
|
278
|
+
| Type | 描述 |
|
|
279
|
+
|-----------|-------------------|
|
|
280
|
+
| `console` | 控制台日志(支持级别过滤) |
|
|
281
|
+
| `network` | 网络请求日志(支持 URL 过滤) |
|
|
282
|
+
|
|
283
|
+
参数:`output` 将结果保存到文件,network 日志包含已完成请求、HTTP 4xx/5xx 响应和加载失败请求,尽量返回 `errorText`、`method`、
|
|
284
|
+
`url`、`status`、`timestamp`、`duration`,`tabId` 指定目标 Tab(Extension 模式),`frame` 不适用于日志,
|
|
285
|
+
|
|
286
|
+
### cookies - Cookie 管理
|
|
287
|
+
|
|
288
|
+
| Action | 描述 |
|
|
289
|
+
|----------|-----------------------------------------------|
|
|
290
|
+
| `get` | 获取 Cookie |
|
|
291
|
+
| `set` | 设置 Cookie |
|
|
292
|
+
| `delete` | 删除 Cookie |
|
|
293
|
+
| `clear` | 按过滤参数删除 Cookie(`name`/`domain`/`url`,必须 ≥1 个) |
|
|
294
|
+
|
|
295
|
+
**说明**:`clear` 必须指定 `name`、`domain`、`url` 中至少一个过滤参数,否则拒绝调用以避免误清用户登录态
|
|
296
|
+
|
|
297
|
+
## Target:统一元素定位器
|
|
298
|
+
|
|
299
|
+
所有工具使用统一的 `Target` 类型定位元素:
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// 按可访问性(推荐 - 最稳定)
|
|
303
|
+
{ role: "button", name: "提交" }
|
|
304
|
+
|
|
305
|
+
// 按可访问性精确匹配名称
|
|
306
|
+
{ role: "button", name: "提交", exact: true }
|
|
307
|
+
|
|
308
|
+
// 按文本内容
|
|
309
|
+
{ text: "点击这里", exact: true }
|
|
310
|
+
|
|
311
|
+
// 按表单 label
|
|
312
|
+
{
|
|
313
|
+
label: "邮箱"
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 按 placeholder
|
|
317
|
+
{
|
|
318
|
+
placeholder: "请输入姓名"
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 按 title 属性
|
|
322
|
+
{
|
|
323
|
+
title: "关闭对话框"
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 按 alt 文本(图片)
|
|
327
|
+
{
|
|
328
|
+
alt: "头像"
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 按 test ID
|
|
332
|
+
{
|
|
333
|
+
testId: "submit-button"
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 按 CSS 选择器
|
|
337
|
+
{
|
|
338
|
+
css: "#login-form .submit-btn"
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 多匹配消歧(从 0 开始)
|
|
342
|
+
{ css: ".ant-select-input", nth: 1 }
|
|
343
|
+
|
|
344
|
+
// 按 CSS + 文本(按文本内容过滤)
|
|
345
|
+
{ css: "button", text: "提交", exact: true }
|
|
346
|
+
|
|
347
|
+
// 按 XPath
|
|
348
|
+
{
|
|
349
|
+
xpath: "//button[@type='submit']"
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// 按坐标
|
|
353
|
+
{ x: 100, y: 200 }
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## 使用示例
|
|
357
|
+
|
|
358
|
+
### 基础:列出 Tab 并导航
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
browse(action="list")
|
|
362
|
+
browse(action="open", url="https://example.com")
|
|
363
|
+
extract(type="state")
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### 点击按钮
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
input(events=[
|
|
370
|
+
{ type: "mousedown", target: { role: "button", name: "提交" } },
|
|
371
|
+
{ type: "mouseup" }
|
|
372
|
+
])
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### 在输入框输入
|
|
376
|
+
|
|
377
|
+
```
|
|
378
|
+
input(events=[
|
|
379
|
+
{ type: "mousedown", target: { label: "邮箱" } },
|
|
380
|
+
{ type: "mouseup" },
|
|
381
|
+
{ type: "type", text: "user@example.com" }
|
|
382
|
+
])
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### 多 Tab 操作(Extension 模式)
|
|
386
|
+
|
|
387
|
+
```
|
|
388
|
+
// 对指定 Tab 操作,不切换焦点
|
|
389
|
+
extract(type="screenshot", tabId="12345")
|
|
390
|
+
evaluate(script="document.title", tabId="12345")
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### 截图
|
|
394
|
+
|
|
395
|
+
```
|
|
396
|
+
// 全页面
|
|
397
|
+
extract(type="screenshot", fullPage=true)
|
|
398
|
+
|
|
399
|
+
// 元素截图(支持所有 target 类型)
|
|
400
|
+
extract(type="screenshot", target={ role: "button", name: "提交" })
|
|
401
|
+
|
|
402
|
+
// JPEG + quality(更小体积)
|
|
403
|
+
extract(type="screenshot", format="jpeg", quality=80, output="tmp:screenshot.jpg")
|
|
404
|
+
|
|
405
|
+
// 保存到文件
|
|
406
|
+
extract(type="screenshot", output="tmp:screenshot.png")
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### 提取 HTML 及图片
|
|
410
|
+
|
|
411
|
+
```
|
|
412
|
+
// 获取 HTML + 图片元信息(src、alt、尺寸)
|
|
413
|
+
extract(type="html", target={ css: ".article" }, images="info")
|
|
414
|
+
|
|
415
|
+
// 获取 HTML + 图片数据,保存到目录
|
|
416
|
+
extract(type="html", images="data", output="tmp:page")
|
|
417
|
+
// 生成:<系统临时目录>/claude-tools/mcp-chrome/page/content.html, images/*, index.json
|
|
418
|
+
|
|
419
|
+
// 获取 HTML + 图片数据(内联返回,最多 20 张)
|
|
420
|
+
extract(type="html", target={ css: ".card" }, images="data")
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### 页面元信息
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
// 提取标题、OG 标签、JSON-LD、RSS 等
|
|
427
|
+
extract(type="metadata")
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### iframe 操作(Extension 模式)
|
|
431
|
+
|
|
432
|
+
```
|
|
433
|
+
// 通过 CSS 选择器指定 iframe
|
|
434
|
+
evaluate(script="document.title", frame="iframe#main")
|
|
435
|
+
|
|
436
|
+
// 通过索引指定 iframe
|
|
437
|
+
extract(type="text", frame=0)
|
|
438
|
+
|
|
439
|
+
// 在 iframe 内输入
|
|
440
|
+
input(events=[
|
|
441
|
+
{ type: "mousedown", target: { label: "用户名" } },
|
|
442
|
+
{ type: "mouseup" },
|
|
443
|
+
{ type: "type", text: "admin" }
|
|
444
|
+
], frame="iframe.login-frame")
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### 等待元素
|
|
448
|
+
|
|
449
|
+
```
|
|
450
|
+
wait(for="element", target={ text: "加载完成" }, state="visible")
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
## 架构
|
|
454
|
+
|
|
455
|
+
```
|
|
456
|
+
┌───────────────────┐
|
|
457
|
+
│ MCP 客户端 │
|
|
458
|
+
│ (Claude 等) │
|
|
459
|
+
└─────────┬─────────┘
|
|
460
|
+
│ stdio (JSON-RPC)
|
|
461
|
+
▼
|
|
462
|
+
┌───────────────────┐
|
|
463
|
+
│ MCP-Chrome │
|
|
464
|
+
│ (8 个工具) │
|
|
465
|
+
│ ├─ core/ │ UnifiedSession, Locator, AutoWait
|
|
466
|
+
│ ├─ cdp/ │ 原生 CDP 客户端
|
|
467
|
+
│ ├─ extension/ │ Extension 桥接(HTTP + WebSocket)
|
|
468
|
+
│ └─ tools/ │ 工具实现
|
|
469
|
+
└────┬─────────┬────┘
|
|
470
|
+
│ │
|
|
471
|
+
│ HTTP/WS │ WebSocket (CDP)
|
|
472
|
+
│ │
|
|
473
|
+
▼ ▼
|
|
474
|
+
┌──────────┐ ┌──────────────────┐
|
|
475
|
+
│ Extension│ │ Chrome (CDP) │
|
|
476
|
+
│ (19222+) │ │ (端口 9222) │
|
|
477
|
+
│ │ │ 独立浏览器实例 │
|
|
478
|
+
│ 操控用户 │ └──────────────────┘
|
|
479
|
+
│ 现有浏览 │
|
|
480
|
+
│ 器 │
|
|
481
|
+
└──────────┘
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
## 项目结构
|
|
485
|
+
|
|
486
|
+
```
|
|
487
|
+
mcp-chrome/
|
|
488
|
+
├── src/
|
|
489
|
+
│ ├── index.ts # MCP Server 入口
|
|
490
|
+
│ ├── tools/ # 8 个 MCP 工具
|
|
491
|
+
│ │ ├── browse.ts
|
|
492
|
+
│ │ ├── input.ts
|
|
493
|
+
│ │ ├── extract.ts
|
|
494
|
+
│ │ ├── wait.ts
|
|
495
|
+
│ │ ├── manage.ts
|
|
496
|
+
│ │ ├── logs.ts
|
|
497
|
+
│ │ ├── cookies.ts
|
|
498
|
+
│ │ ├── evaluate.ts
|
|
499
|
+
│ │ └── schema.ts # 公共 JSON Schema(Target oneOf)
|
|
500
|
+
│ ├── core/ # 核心抽象
|
|
501
|
+
│ │ ├── unified-session.ts # 双模式会话(Extension + CDP)
|
|
502
|
+
│ │ ├── browser-driver.ts # IBrowserDriver 抽象
|
|
503
|
+
│ │ ├── session.ts # CDP 会话管理
|
|
504
|
+
│ │ ├── locator.ts # 元素定位器(deadline 超时预算)
|
|
505
|
+
│ │ ├── auto-wait.ts # 自动等待机制
|
|
506
|
+
│ │ ├── retry.ts # 重试逻辑
|
|
507
|
+
│ │ ├── types.ts # 类型定义
|
|
508
|
+
│ │ ├── utils.ts # 公共工具
|
|
509
|
+
│ │ ├── errors.ts # 错误类型
|
|
510
|
+
│ │ └── index.ts # 模块入口
|
|
511
|
+
│ ├── extension/ # Extension 桥接
|
|
512
|
+
│ │ ├── bridge.ts # 高层 Extension API
|
|
513
|
+
│ │ └── http-server.ts # HTTP + WebSocket 服务器
|
|
514
|
+
│ ├── cdp/ # CDP 层
|
|
515
|
+
│ │ ├── client.ts # WebSocket CDP 客户端
|
|
516
|
+
│ │ └── launcher.ts # Chrome 启动器
|
|
517
|
+
│ └── anti-detection/ # 反检测(可选)
|
|
518
|
+
│ ├── injection.ts
|
|
519
|
+
│ └── behavior.ts
|
|
520
|
+
├── extension/ # Chrome Extension(Manifest V3)
|
|
521
|
+
│ ├── manifest.json
|
|
522
|
+
│ ├── src/
|
|
523
|
+
│ │ ├── background/ # Service Worker
|
|
524
|
+
│ │ ├── content/ # Content Scripts
|
|
525
|
+
│ │ └── popup/ # Popup UI
|
|
526
|
+
│ └── dist/ # 构建产物(在 Chrome 中加载此目录)
|
|
527
|
+
├── scripts/
|
|
528
|
+
│ ├── start-chrome.sh
|
|
529
|
+
│ └── start-chrome-headless.sh
|
|
530
|
+
└── package.json
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
## 安全说明
|
|
534
|
+
|
|
535
|
+
- **信任边界**:本服务器无应用层认证,仅依赖 `127.0.0.1` 绑定 + 同 UID 信任,与 Playwright、Puppeteer、chrome-launcher
|
|
536
|
+
采用同一信任模型,禁止部署在多用户系统、CI runner、`--net=host` 容器等不可信代码可访问
|
|
537
|
+
`127.0.0.1:19222-19299` 的环境中,WebSocket 握手要求 `chrome-extension://` Origin 头,可挡掉浏览器页面和 curl,
|
|
538
|
+
但拦不住同 UID 的本地恶意进程
|
|
539
|
+
- Extension 模式:共享浏览器会话,请仅在受信任的机器上使用
|
|
540
|
+
- CDP 模式:通过 DevTools Protocol 提供完整浏览器控制能力
|
|
541
|
+
- 默认端口仅绑定到 127.0.0.1(本地访问)
|
|
542
|
+
- `evaluate` 工具可执行任意 JavaScript
|
|
543
|
+
- `manage cdp` 操作可发送任意 CDP 命令
|
|
544
|
+
- 网络日志可能包含敏感信息
|
|
545
|
+
|
|
546
|
+
### 反检测(stealth)—— 实际能力
|
|
547
|
+
|
|
548
|
+
`stealth` 模式(`safe` / `aggressive`)仅覆盖少量指纹面:
|
|
549
|
+
|
|
550
|
+
- **覆盖**:`navigator.webdriver`、`cdc_*` 属性、User-Agent 字符串、若干 WebGL vendor/renderer 值、Chrome runtime 属性
|
|
551
|
+
- **不覆盖**:Canvas 指纹、AudioContext 指纹、Font 枚举、TLS 层指纹(JA3/JA4)、CDP attach 横幅("Chrome 正受自动化软件控制"
|
|
552
|
+
)、扩展自身存在的探测
|
|
553
|
+
- **警告**:禁止使用本功能绕过商业 anti-bot 服务(Cloudflare Turnstile、Akamai、DataDome、PerimeterX),真实的 bot
|
|
554
|
+
检测发生在多个我们无法在页面内部修补的层级
|
|
555
|
+
|
|
556
|
+
## 已知限制
|
|
557
|
+
|
|
558
|
+
- **仅 Chrome**:仅支持 Chrome/Chromium 浏览器(不支持 Firefox/Safari)
|
|
559
|
+
- **CDP 单会话**:CDP 模式仅支持一个浏览器会话
|
|
560
|
+
- **Extension 需要 Chrome**:Extension 为 Manifest V3,仅限 Chrome
|
|
561
|
+
- **iframe**:仅支持单层 iframe 定位(不支持嵌套 `>>` 语法);仅限 Extension 模式
|
|
562
|
+
|
|
563
|
+
## 许可证
|
|
564
|
+
|
|
565
|
+
MIT 许可证 - 详见 [LICENSE](LICENSE)
|
|
566
|
+
|
|
567
|
+
## 相关项目
|
|
568
|
+
|
|
569
|
+
- [Model Context Protocol](https://modelcontextprotocol.io/) - MCP 规范
|
|
570
|
+
- [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) - CDP 文档
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"launcher.d.ts","sourceRoot":"","sources":["../../src/cdp/launcher.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAA;AA6BtE;;GAEG;AACH,wBAAgB,UAAU,IAAI,MAAM,GAAG,IAAI,CAU1C;AAED;;GAEG;AACH,qBAAa,eAAe;IACxB,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,KAAK,CAAY;IAEzB,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;OAEG;IACG,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,MAAM,CAAC;IAwD1D;;OAEG;IACH,KAAK,IAAI,IAAI;IAOb;;OAEG;IACH,OAAO,CAAC,SAAS;IA0DjB;;OAEG;IACH,OAAO,CAAC,eAAe;
|
|
1
|
+
{"version":3,"file":"launcher.d.ts","sourceRoot":"","sources":["../../src/cdp/launcher.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAA;AA6BtE;;GAEG;AACH,wBAAgB,UAAU,IAAI,MAAM,GAAG,IAAI,CAU1C;AAED;;GAEG;AACH,qBAAa,eAAe;IACxB,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,KAAK,CAAY;IAEzB,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;OAEG;IACG,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,MAAM,CAAC;IAwD1D;;OAEG;IACH,KAAK,IAAI,IAAI;IAOb;;OAEG;IACH,OAAO,CAAC,SAAS;IA0DjB;;OAEG;IACH,OAAO,CAAC,eAAe;CA6E1B"}
|
package/dist/cdp/launcher.js
CHANGED
|
@@ -164,46 +164,68 @@ export class BrowserLauncher {
|
|
|
164
164
|
reject(new Error('浏览器进程未启动'));
|
|
165
165
|
return;
|
|
166
166
|
}
|
|
167
|
-
|
|
168
|
-
reject(new TimeoutError(`等待浏览器启动超时 (${timeout}ms)`));
|
|
169
|
-
}, timeout);
|
|
167
|
+
let settled = false;
|
|
170
168
|
let stderr = '';
|
|
171
169
|
const stderrStream = this.process.stderr;
|
|
170
|
+
const timer = setTimeout(() => {
|
|
171
|
+
fail(new TimeoutError(`等待浏览器启动超时 (${timeout}ms)`), true);
|
|
172
|
+
}, timeout);
|
|
173
|
+
const cleanup = () => {
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
stderrStream?.removeListener('data', onStderr);
|
|
176
|
+
this.process?.removeListener('error', onError);
|
|
177
|
+
this.process?.removeListener('exit', onExit);
|
|
178
|
+
};
|
|
179
|
+
const finish = (url) => {
|
|
180
|
+
if (settled) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
settled = true;
|
|
184
|
+
cleanup();
|
|
185
|
+
resolve(url);
|
|
186
|
+
};
|
|
187
|
+
const fail = (error, killProcess = false) => {
|
|
188
|
+
if (settled) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
settled = true;
|
|
192
|
+
cleanup();
|
|
193
|
+
if (killProcess && this.process && !this.process.killed) {
|
|
194
|
+
this.process.kill();
|
|
195
|
+
this.process = null;
|
|
196
|
+
}
|
|
197
|
+
reject(error);
|
|
198
|
+
};
|
|
172
199
|
const onStderr = (data) => {
|
|
173
200
|
stderr += data.toString();
|
|
174
201
|
// 解析 DevTools listening on ws://...
|
|
175
202
|
const match = stderr.match(/DevTools listening on (?<url>ws:\/\/\S+)/);
|
|
176
203
|
if (match) {
|
|
177
|
-
clearTimeout(timer);
|
|
178
204
|
// 解析端口
|
|
179
205
|
const portMatch = match.groups.url.match(/:(?<port>\d+)\//);
|
|
180
206
|
if (portMatch) {
|
|
181
207
|
this._port = parseInt(portMatch.groups.port, 10);
|
|
182
208
|
}
|
|
183
|
-
|
|
184
|
-
stderrStream?.removeListener('data', onStderr);
|
|
185
|
-
resolve(match.groups.url);
|
|
209
|
+
finish(match.groups.url);
|
|
186
210
|
}
|
|
187
211
|
};
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
reject(error);
|
|
193
|
-
});
|
|
194
|
-
this.process.on('exit', (code) => {
|
|
195
|
-
clearTimeout(timer);
|
|
196
|
-
stderrStream?.removeListener('data', onStderr);
|
|
212
|
+
const onError = (error) => {
|
|
213
|
+
fail(error);
|
|
214
|
+
};
|
|
215
|
+
const onExit = (code) => {
|
|
197
216
|
if (code === 0) {
|
|
198
217
|
// Chrome 退出码 0:通常是把请求委托给了已运行的实例后自行退出,
|
|
199
218
|
// 此时没有 DevTools 端点可连接
|
|
200
|
-
|
|
219
|
+
fail(new Error('浏览器进程已退出(code 0),可能已有相同 profile 的 Chrome 实例在运行\n' +
|
|
201
220
|
'请关闭已运行的 Chrome 或指定不同的 userDataDir'));
|
|
202
221
|
}
|
|
203
222
|
else if (code !== null) {
|
|
204
|
-
|
|
223
|
+
fail(new Error(`浏览器进程退出,代码: ${code}\n${stderr}`));
|
|
205
224
|
}
|
|
206
|
-
}
|
|
225
|
+
};
|
|
226
|
+
stderrStream?.on('data', onStderr);
|
|
227
|
+
this.process.on('error', onError);
|
|
228
|
+
this.process.on('exit', onExit);
|
|
207
229
|
});
|
|
208
230
|
}
|
|
209
231
|
}
|