@mallocfeng/chromedev 0.1.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/LICENSE +21 -0
- package/README.md +337 -0
- package/bin/chromedev.mjs +243 -0
- package/launchd/com.local.chrome-mcp-daemon.plist +46 -0
- package/package.json +48 -0
- package/server/chrome-mcp-daemon.mjs +159 -0
- package/skills/chromedev/SKILL.md +117 -0
- package/skills/chromedev/agents/openai.yaml +15 -0
- package/skills/chromedev/scripts/http_mcp_call.mjs +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mallocfeng
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# ChromeDev
|
|
2
|
+
|
|
3
|
+
`ChromeDev` 是一个本地命令行工具,用来把 `chrome-devtools-mcp` 作为后台中间件运行,并通过 `--autoConnect` 连接你当前正在使用的 Chrome。
|
|
4
|
+
|
|
5
|
+
启动后,本地 MCP 地址固定为:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
http://127.0.0.1:8787/mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 快速开始
|
|
12
|
+
|
|
13
|
+
整个流程只有两步:
|
|
14
|
+
|
|
15
|
+
1. 在 Chrome 中开启远程调试
|
|
16
|
+
2. 安装并启动 `chromedev` 服务
|
|
17
|
+
|
|
18
|
+
### 1. 在 Chrome 中开启远程调试
|
|
19
|
+
|
|
20
|
+
在 Chrome(版本 `>= 144`)中,执行以下操作来设置远程调试:
|
|
21
|
+
|
|
22
|
+
前往 `chrome://inspect/#remote-debugging` 以启用远程调试。
|
|
23
|
+
|
|
24
|
+
建议操作:
|
|
25
|
+
|
|
26
|
+
1. 打开 Chrome。
|
|
27
|
+
2. 在地址栏输入 `chrome://inspect/#remote-debugging`。
|
|
28
|
+
3. 开启远程调试相关选项。
|
|
29
|
+
4. 保持这个 Chrome 实例继续运行。
|
|
30
|
+
|
|
31
|
+
第一次连接时,Chrome 可能会弹出授权确认框。默认会等待 30 秒。
|
|
32
|
+
|
|
33
|
+
### 2. 安装并启动服务
|
|
34
|
+
|
|
35
|
+
推荐直接用 npm 全局安装:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install -g @mallocfeng/chromedev
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
安装完成后,后台启动:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
chromedev run
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
如果你是在项目源码目录里本地运行,也可以用:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
node ./bin/chromedev.mjs run
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
成功后会看到:
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
chromedev started in background (pid 12345)
|
|
57
|
+
endpoint: http://127.0.0.1:8787/mcp
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
这表示服务已经在后台运行,其他 AI 或程序现在可以访问 `http://127.0.0.1:8787/mcp`。
|
|
61
|
+
|
|
62
|
+
## npm 安装
|
|
63
|
+
|
|
64
|
+
推荐安装方式:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm install -g @mallocfeng/chromedev
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
安装完成后可直接使用:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
chromedev run
|
|
74
|
+
chromedev status
|
|
75
|
+
chromedev stop
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
如果你不想全局安装,也可以临时执行:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx @mallocfeng/chromedev run
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
如果用户把这个 GitHub 仓库地址直接发给 AI,AI 应该按下面的步骤理解和执行:
|
|
85
|
+
|
|
86
|
+
1. 先安装命令行工具:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm install -g @mallocfeng/chromedev
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
2. 然后启动服务:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
chromedev run
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
3. 如果还需要安装 OpenClaw skill,再从 GitHub 下载项目源码,并把 `skills/chromedev` 拷贝到 OpenClaw 的 `skills` 目录。
|
|
99
|
+
|
|
100
|
+
## 常用命令
|
|
101
|
+
|
|
102
|
+
后台启动:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
chromedev run
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
前台调试:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
chromedev run --foreground
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
查看状态:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
chromedev status
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
停止服务:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
chromedev stop
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
查看日志路径:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
chromedev logs
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
如果你是在源码目录里本地运行,把 `chromedev` 换成:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
node ./bin/chromedev.mjs
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## 如何验证服务正常
|
|
139
|
+
|
|
140
|
+
看状态:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
chromedev status
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
看端点是否存在:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
curl -i http://127.0.0.1:8787/mcp
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
如果返回 `400 Bad Request` 且内容类似 `No sessionId`,这是正常的,说明 MCP 端点已经存在。
|
|
153
|
+
|
|
154
|
+
## 命令行访问网页示例
|
|
155
|
+
|
|
156
|
+
列出当前页面:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
node /Volumes/MacMiniDisk/project/chromeDev/skills/chromedev/scripts/http_mcp_call.mjs list_pages
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
打开 `163.com`:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
node /Volumes/MacMiniDisk/project/chromeDev/skills/chromedev/scripts/http_mcp_call.mjs new_page '{"url":"https://www.163.com/","timeout":30000}'
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
读取当前页面快照:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
node /Volumes/MacMiniDisk/project/chromeDev/skills/chromedev/scripts/http_mcp_call.mjs take_snapshot
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## 在 OpenClaw 中安装 skill
|
|
175
|
+
|
|
176
|
+
这个项目里已经包含了一份 skill:
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
skills/chromedev
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
如果你是通过 GitHub 下载项目源码:
|
|
183
|
+
|
|
184
|
+
1. 打开仓库:`https://github.com/mallocfeng/chromeDev`
|
|
185
|
+
2. 下载项目源码,或者直接 clone:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
git clone https://github.com/mallocfeng/chromeDev.git
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
3. 你需要用到的 skill 目录就是:
|
|
192
|
+
|
|
193
|
+
```text
|
|
194
|
+
chromeDev/skills/chromedev
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
如果用户把 GitHub 地址直接发给 AI,AI 可以按这套指令执行:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
git clone https://github.com/mallocfeng/chromeDev.git
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
然后取出:
|
|
204
|
+
|
|
205
|
+
```text
|
|
206
|
+
chromeDev/skills/chromedev
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
再把它复制到 OpenClaw 的 `skills` 目录下。
|
|
210
|
+
|
|
211
|
+
安装到 OpenClaw 的方法很简单:
|
|
212
|
+
|
|
213
|
+
1. 把项目目录中的 `skills/chromedev` 目录拷贝到你 OpenClaw 工作目录的 `skills` 文件夹下。
|
|
214
|
+
2. 如果你不清楚这个文件夹在哪里,可以先直接问 OpenClaw,它会告诉你当前 agent 工作目录在哪里。
|
|
215
|
+
3. 拷贝进去以后,运行:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
openclaw gateway restart
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
完成后就可以在 OpenClaw 里调用这个 skill。
|
|
222
|
+
|
|
223
|
+
给 AI 的可执行描述可以直接写成这样:
|
|
224
|
+
|
|
225
|
+
```text
|
|
226
|
+
从 https://github.com/mallocfeng/chromeDev 下载项目,把其中的 skills/chromedev 目录拷贝到当前 OpenClaw agent 工作目录下的 skills 文件夹,然后执行 openclaw gateway restart。
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
目录结构应该像这样:
|
|
230
|
+
|
|
231
|
+
```text
|
|
232
|
+
<openclaw-agent-workdir>/
|
|
233
|
+
skills/
|
|
234
|
+
chromedev/
|
|
235
|
+
SKILL.md
|
|
236
|
+
agents/openai.yaml
|
|
237
|
+
scripts/http_mcp_call.mjs
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## 在 OpenClaw 中使用
|
|
241
|
+
|
|
242
|
+
在聊天框中可以直接这样输入:
|
|
243
|
+
|
|
244
|
+
```text
|
|
245
|
+
/chromedev 调用这个 skills 访问当前打开的纽约时报标签文章,并摘要
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
或者:
|
|
249
|
+
|
|
250
|
+
```text
|
|
251
|
+
调用 chromedev 这个 skills,访问纽约时报,并且摘要新闻
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
这个 skill 会通过本地 `ChromeDev` MCP 服务访问你当前打开的 Chrome,并读取网页内容。
|
|
255
|
+
|
|
256
|
+
注意:
|
|
257
|
+
|
|
258
|
+
- OpenClaw 里的 skill 只负责“告诉 AI 怎么调用本地 ChromeDev 服务”
|
|
259
|
+
- 真正提供浏览器访问能力的,还是你本机运行中的 `chromedev run`
|
|
260
|
+
- 所以使用 skill 前,先确认本地 `chromedev` 服务已经启动
|
|
261
|
+
|
|
262
|
+
## 日志与运行文件
|
|
263
|
+
|
|
264
|
+
默认文件位置:
|
|
265
|
+
|
|
266
|
+
- `~/.chromedev/run/chromedev.pid`
|
|
267
|
+
- `~/.chromedev/run/chromedev.out.log`
|
|
268
|
+
- `~/.chromedev/run/chromedev.err.log`
|
|
269
|
+
|
|
270
|
+
直接查看日志:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
tail -f ~/.chromedev/run/chromedev.out.log
|
|
274
|
+
tail -f ~/.chromedev/run/chromedev.err.log
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## 默认配置
|
|
278
|
+
|
|
279
|
+
默认参数:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
CHROME_AUTO_CONNECT=1
|
|
283
|
+
CHROME_CHANNEL=stable
|
|
284
|
+
MCP_HOST=127.0.0.1
|
|
285
|
+
MCP_PORT=8787
|
|
286
|
+
MCP_CONNECTION_TIMEOUT=30000
|
|
287
|
+
MCP_REQUEST_TIMEOUT=30000
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
如果你要改端口:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
MCP_PORT=8788 chromedev run
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## 常见问题
|
|
297
|
+
|
|
298
|
+
### 1. `EADDRINUSE: address already in use 127.0.0.1:8787`
|
|
299
|
+
|
|
300
|
+
说明 `8787` 已经被已有实例占用了。
|
|
301
|
+
|
|
302
|
+
处理方式:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
chromedev status
|
|
306
|
+
chromedev stop
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
或者换端口:
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
MCP_PORT=8788 chromedev run
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### 2. Chrome 没有响应
|
|
316
|
+
|
|
317
|
+
检查这几项:
|
|
318
|
+
|
|
319
|
+
1. Chrome 是否已经打开
|
|
320
|
+
2. 是否已访问 `chrome://inspect/#remote-debugging`
|
|
321
|
+
3. 远程调试是否已经启用
|
|
322
|
+
4. Chrome 是否弹出了授权确认框但还没有点
|
|
323
|
+
|
|
324
|
+
### 3. 第一次连接会卡几秒
|
|
325
|
+
|
|
326
|
+
通常是正常的,可能是:
|
|
327
|
+
|
|
328
|
+
- Chrome 正在等待授权确认
|
|
329
|
+
- `autoConnect` 正在连接浏览器
|
|
330
|
+
- 页面本身还在加载
|
|
331
|
+
|
|
332
|
+
## 安全说明
|
|
333
|
+
|
|
334
|
+
- 服务只应监听本地地址 `127.0.0.1`
|
|
335
|
+
- 不要把 `http://127.0.0.1:8787/mcp` 暴露到公网
|
|
336
|
+
- 不要让不可信程序访问这个端点
|
|
337
|
+
- 这个中间件拥有你当前 Chrome 会话的高权限访问能力
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import { mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
5
|
+
import { homedir } from 'node:os'
|
|
6
|
+
import { dirname, join } from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import process from 'node:process'
|
|
9
|
+
import net from 'node:net'
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const projectRoot = dirname(__dirname)
|
|
13
|
+
const appHome = process.env.CHROMEDEV_HOME || join(homedir(), '.chromedev')
|
|
14
|
+
const runDir = join(appHome, 'run')
|
|
15
|
+
const pidFile = join(runDir, 'chromedev.pid')
|
|
16
|
+
const metaFile = join(runDir, 'chromedev.json')
|
|
17
|
+
const outLog = join(runDir, 'chromedev.out.log')
|
|
18
|
+
const errLog = join(runDir, 'chromedev.err.log')
|
|
19
|
+
const daemonScript = join(projectRoot, 'server', 'chrome-mcp-daemon.mjs')
|
|
20
|
+
|
|
21
|
+
const argv = process.argv.slice(2)
|
|
22
|
+
const command = argv[0] || 'help'
|
|
23
|
+
|
|
24
|
+
main().catch((error) => {
|
|
25
|
+
console.error(error.message || String(error))
|
|
26
|
+
process.exit(1)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
switch (command) {
|
|
31
|
+
case 'run':
|
|
32
|
+
case 'start':
|
|
33
|
+
await runCommand(argv.slice(1))
|
|
34
|
+
return
|
|
35
|
+
case 'stop':
|
|
36
|
+
await stopCommand()
|
|
37
|
+
return
|
|
38
|
+
case 'status':
|
|
39
|
+
await statusCommand()
|
|
40
|
+
return
|
|
41
|
+
case 'logs':
|
|
42
|
+
logsCommand()
|
|
43
|
+
return
|
|
44
|
+
case 'help':
|
|
45
|
+
case '--help':
|
|
46
|
+
case '-h':
|
|
47
|
+
printHelp()
|
|
48
|
+
return
|
|
49
|
+
default:
|
|
50
|
+
throw new Error(`Unknown command: ${command}`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function runCommand(args) {
|
|
55
|
+
const foreground = args.includes('--foreground') || args.includes('-f')
|
|
56
|
+
const meta = readMeta()
|
|
57
|
+
const pid = readPid()
|
|
58
|
+
const port = Number(process.env.MCP_PORT || meta?.port || '8787')
|
|
59
|
+
|
|
60
|
+
if (pid && isProcessRunning(pid)) {
|
|
61
|
+
console.log(`chromedev is already running (pid ${pid})`)
|
|
62
|
+
console.log(`endpoint: http://127.0.0.1:${port}/mcp`)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
ensureRunDir()
|
|
67
|
+
cleanupState()
|
|
68
|
+
|
|
69
|
+
if (await isPortListening(port)) {
|
|
70
|
+
throw new Error(`port ${port} is already in use; stop the existing service or set MCP_PORT`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (foreground) {
|
|
74
|
+
console.log('starting chromedev in foreground')
|
|
75
|
+
process.stdout.write(`endpoint: http://127.0.0.1:${port}/mcp\n`)
|
|
76
|
+
const child = spawn(process.execPath, [daemonScript], {
|
|
77
|
+
cwd: projectRoot,
|
|
78
|
+
stdio: 'inherit',
|
|
79
|
+
env: {
|
|
80
|
+
...process.env,
|
|
81
|
+
CHROMEDEV_HOME: appHome,
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
child.on('exit', (code) => {
|
|
85
|
+
process.exit(code ?? 0)
|
|
86
|
+
})
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const stdoutFd = openSync(outLog, 'a')
|
|
91
|
+
const stderrFd = openSync(errLog, 'a')
|
|
92
|
+
const child = spawn(process.execPath, [daemonScript], {
|
|
93
|
+
cwd: projectRoot,
|
|
94
|
+
detached: true,
|
|
95
|
+
stdio: ['ignore', stdoutFd, stderrFd],
|
|
96
|
+
env: {
|
|
97
|
+
...process.env,
|
|
98
|
+
CHROMEDEV_HOME: appHome,
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
child.unref()
|
|
103
|
+
|
|
104
|
+
writeFileSync(pidFile, `${child.pid}\n`)
|
|
105
|
+
writeMeta(port)
|
|
106
|
+
|
|
107
|
+
console.log(`chromedev started in background (pid ${child.pid})`)
|
|
108
|
+
console.log(`endpoint: http://127.0.0.1:${port}/mcp`)
|
|
109
|
+
console.log(`stdout: ${outLog}`)
|
|
110
|
+
console.log(`stderr: ${errLog}`)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function stopCommand() {
|
|
114
|
+
const pid = readPid()
|
|
115
|
+
|
|
116
|
+
if (!pid) {
|
|
117
|
+
console.log('chromedev is not running')
|
|
118
|
+
cleanupState()
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!isProcessRunning(pid)) {
|
|
123
|
+
console.log(`stale pid file found for pid ${pid}; cleaning up`)
|
|
124
|
+
cleanupState()
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
process.kill(pid, 'SIGTERM')
|
|
129
|
+
|
|
130
|
+
const deadline = Date.now() + 5000
|
|
131
|
+
while (Date.now() < deadline) {
|
|
132
|
+
if (!isProcessRunning(pid)) {
|
|
133
|
+
cleanupState()
|
|
134
|
+
console.log(`chromedev stopped (pid ${pid})`)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
await sleep(200)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
process.kill(pid, 'SIGKILL')
|
|
141
|
+
cleanupState()
|
|
142
|
+
console.log(`chromedev force-stopped (pid ${pid})`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function statusCommand() {
|
|
146
|
+
const meta = readMeta()
|
|
147
|
+
const pid = readPid()
|
|
148
|
+
const port = Number(process.env.MCP_PORT || meta?.port || '8787')
|
|
149
|
+
const endpoint = `http://127.0.0.1:${port}/mcp`
|
|
150
|
+
|
|
151
|
+
if (pid && isProcessRunning(pid)) {
|
|
152
|
+
console.log(`status: running`)
|
|
153
|
+
console.log(`pid: ${pid}`)
|
|
154
|
+
console.log(`endpoint: ${endpoint}`)
|
|
155
|
+
console.log(`stdout: ${outLog}`)
|
|
156
|
+
console.log(`stderr: ${errLog}`)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (await isPortListening(port)) {
|
|
161
|
+
console.log('status: port in use by another process')
|
|
162
|
+
console.log(`endpoint: ${endpoint}`)
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log('status: stopped')
|
|
167
|
+
console.log(`endpoint: ${endpoint}`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function logsCommand() {
|
|
171
|
+
console.log(`stdout: ${outLog}`)
|
|
172
|
+
console.log(`stderr: ${errLog}`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function ensureRunDir() {
|
|
176
|
+
mkdirSync(runDir, { recursive: true })
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function cleanupState() {
|
|
180
|
+
rmSync(pidFile, { force: true })
|
|
181
|
+
rmSync(metaFile, { force: true })
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function readPid() {
|
|
185
|
+
try {
|
|
186
|
+
return Number(readFileSync(pidFile, 'utf8').trim())
|
|
187
|
+
} catch {
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function writeMeta(port) {
|
|
193
|
+
const payload = {
|
|
194
|
+
port,
|
|
195
|
+
updatedAt: new Date().toISOString(),
|
|
196
|
+
}
|
|
197
|
+
writeFileSync(metaFile, JSON.stringify(payload, null, 2))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function readMeta() {
|
|
201
|
+
try {
|
|
202
|
+
return JSON.parse(readFileSync(metaFile, 'utf8'))
|
|
203
|
+
} catch {
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isProcessRunning(pid) {
|
|
209
|
+
try {
|
|
210
|
+
process.kill(pid, 0)
|
|
211
|
+
return true
|
|
212
|
+
} catch {
|
|
213
|
+
return false
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isPortListening(port) {
|
|
218
|
+
return new Promise((resolve) => {
|
|
219
|
+
const socket = net.createConnection({ host: '127.0.0.1', port })
|
|
220
|
+
socket.once('connect', () => {
|
|
221
|
+
socket.destroy()
|
|
222
|
+
resolve(true)
|
|
223
|
+
})
|
|
224
|
+
socket.once('error', () => {
|
|
225
|
+
resolve(false)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function sleep(ms) {
|
|
231
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function printHelp() {
|
|
235
|
+
console.log('chromedev <command>')
|
|
236
|
+
console.log('')
|
|
237
|
+
console.log('Commands:')
|
|
238
|
+
console.log(' run, start Start the Chrome MCP daemon in background')
|
|
239
|
+
console.log(' run --foreground Start in foreground')
|
|
240
|
+
console.log(' stop Stop the background daemon')
|
|
241
|
+
console.log(' status Show daemon status')
|
|
242
|
+
console.log(' logs Print log file locations')
|
|
243
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key>
|
|
6
|
+
<string>com.local.chrome-mcp-daemon</string>
|
|
7
|
+
|
|
8
|
+
<key>ProgramArguments</key>
|
|
9
|
+
<array>
|
|
10
|
+
<string>/usr/bin/env</string>
|
|
11
|
+
<string>npm</string>
|
|
12
|
+
<string>start</string>
|
|
13
|
+
</array>
|
|
14
|
+
|
|
15
|
+
<key>WorkingDirectory</key>
|
|
16
|
+
<string>/Volumes/MacMiniDisk/project/chromeDev</string>
|
|
17
|
+
|
|
18
|
+
<key>EnvironmentVariables</key>
|
|
19
|
+
<dict>
|
|
20
|
+
<key>CHROME_AUTO_CONNECT</key>
|
|
21
|
+
<string>1</string>
|
|
22
|
+
<key>CHROME_CHANNEL</key>
|
|
23
|
+
<string>stable</string>
|
|
24
|
+
<key>MCP_HOST</key>
|
|
25
|
+
<string>127.0.0.1</string>
|
|
26
|
+
<key>MCP_PORT</key>
|
|
27
|
+
<string>8787</string>
|
|
28
|
+
<key>MCP_CONNECTION_TIMEOUT</key>
|
|
29
|
+
<string>30000</string>
|
|
30
|
+
<key>MCP_REQUEST_TIMEOUT</key>
|
|
31
|
+
<string>30000</string>
|
|
32
|
+
</dict>
|
|
33
|
+
|
|
34
|
+
<key>RunAtLoad</key>
|
|
35
|
+
<true/>
|
|
36
|
+
|
|
37
|
+
<key>KeepAlive</key>
|
|
38
|
+
<true/>
|
|
39
|
+
|
|
40
|
+
<key>StandardOutPath</key>
|
|
41
|
+
<string>/tmp/chrome-mcp-daemon.out.log</string>
|
|
42
|
+
|
|
43
|
+
<key>StandardErrorPath</key>
|
|
44
|
+
<string>/tmp/chrome-mcp-daemon.err.log</string>
|
|
45
|
+
</dict>
|
|
46
|
+
</plist>
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mallocfeng/chromedev",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CLI that exposes chrome-devtools-mcp as a local background service connected to your live Chrome browser.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "mallocfeng",
|
|
8
|
+
"homepage": "https://github.com/mallocfeng/chromeDev#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/mallocfeng/chromeDev.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/mallocfeng/chromeDev/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"chrome",
|
|
18
|
+
"mcp",
|
|
19
|
+
"devtools",
|
|
20
|
+
"cli",
|
|
21
|
+
"browser",
|
|
22
|
+
"automation"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"bin",
|
|
29
|
+
"server",
|
|
30
|
+
"skills",
|
|
31
|
+
"launchd",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"bin": {
|
|
36
|
+
"chromedev": "./bin/chromedev.mjs"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"start": "node ./server/chrome-mcp-daemon.mjs",
|
|
40
|
+
"start:headless": "CHROME_HEADLESS=1 node ./server/chrome-mcp-daemon.mjs",
|
|
41
|
+
"cli": "node ./bin/chromedev.mjs"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
45
|
+
"chrome-devtools-mcp": "^0.20.1",
|
|
46
|
+
"mcp-proxy": "^6.4.4"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { existsSync, mkdirSync } from 'node:fs'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import process from 'node:process'
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const projectRoot = dirname(__dirname)
|
|
10
|
+
const appHome = process.env.CHROMEDEV_HOME || join(homedir(), '.chromedev')
|
|
11
|
+
|
|
12
|
+
const HOST = process.env.MCP_HOST || '127.0.0.1'
|
|
13
|
+
const MCP_PORT = Number(process.env.MCP_PORT || '8787')
|
|
14
|
+
const DEBUG_PORT = Number(process.env.CHROME_DEBUG_PORT || '9222')
|
|
15
|
+
const PROFILE_DIR = process.env.CHROME_USER_DATA_DIR || join(appHome, 'chrome-profile')
|
|
16
|
+
const HEADLESS = ['1', 'true', 'yes'].includes(String(process.env.CHROME_HEADLESS || '').toLowerCase())
|
|
17
|
+
const AUTO_CONNECT = !['0', 'false', 'no'].includes(String(process.env.CHROME_AUTO_CONNECT || '1').toLowerCase())
|
|
18
|
+
const MCP_CONNECTION_TIMEOUT = Number(process.env.MCP_CONNECTION_TIMEOUT || '30000')
|
|
19
|
+
const MCP_REQUEST_TIMEOUT = Number(process.env.MCP_REQUEST_TIMEOUT || '30000')
|
|
20
|
+
const CHROME_PATH = process.env.CHROME_PATH || detectChromePath()
|
|
21
|
+
|
|
22
|
+
if (!AUTO_CONNECT && !CHROME_PATH) {
|
|
23
|
+
console.error('Chrome executable not found. Set CHROME_PATH and retry.')
|
|
24
|
+
process.exit(1)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!AUTO_CONNECT) {
|
|
28
|
+
mkdirSync(PROFILE_DIR, { recursive: true })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let chromeProcess
|
|
32
|
+
let proxyProcess
|
|
33
|
+
|
|
34
|
+
main().catch((error) => {
|
|
35
|
+
console.error('[daemon] fatal error:', error)
|
|
36
|
+
shutdown(1)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
async function main() {
|
|
40
|
+
const args = [
|
|
41
|
+
'--host',
|
|
42
|
+
HOST,
|
|
43
|
+
'--port',
|
|
44
|
+
String(MCP_PORT),
|
|
45
|
+
'--server',
|
|
46
|
+
'stream',
|
|
47
|
+
'--connectionTimeout',
|
|
48
|
+
String(MCP_CONNECTION_TIMEOUT),
|
|
49
|
+
'--requestTimeout',
|
|
50
|
+
String(MCP_REQUEST_TIMEOUT),
|
|
51
|
+
'--',
|
|
52
|
+
localBin('chrome-devtools-mcp'),
|
|
53
|
+
'--no-usage-statistics',
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
if (AUTO_CONNECT) {
|
|
57
|
+
console.log('[daemon] using Chrome autoConnect mode against the running browser...')
|
|
58
|
+
console.log(`[daemon] exposing MCP on http://${HOST}:${MCP_PORT}/mcp`)
|
|
59
|
+
args.push('--autoConnect')
|
|
60
|
+
if (process.env.CHROME_CHANNEL) {
|
|
61
|
+
args.push('--channel', process.env.CHROME_CHANNEL)
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
console.log('[daemon] launching Chrome with persistent remote debugging...')
|
|
65
|
+
chromeProcess = spawn(CHROME_PATH, chromeArgs(), {
|
|
66
|
+
stdio: 'inherit',
|
|
67
|
+
})
|
|
68
|
+
chromeProcess.once('exit', (code, signal) => {
|
|
69
|
+
console.error(`[daemon] Chrome exited (code=${code ?? 'null'}, signal=${signal ?? 'null'})`)
|
|
70
|
+
shutdown(code ?? 1)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
await waitForChrome(`http://127.0.0.1:${DEBUG_PORT}/json/version`, 30_000)
|
|
74
|
+
|
|
75
|
+
console.log(`[daemon] Chrome debugger ready on http://127.0.0.1:${DEBUG_PORT}`)
|
|
76
|
+
console.log(`[daemon] exposing MCP on http://${HOST}:${MCP_PORT}/mcp`)
|
|
77
|
+
args.push('--browserUrl', `http://127.0.0.1:${DEBUG_PORT}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
proxyProcess = spawn(localBin('mcp-proxy'), args, {
|
|
81
|
+
stdio: 'inherit',
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
proxyProcess.once('exit', (code, signal) => {
|
|
85
|
+
console.error(`[daemon] mcp-proxy exited (code=${code ?? 'null'}, signal=${signal ?? 'null'})`)
|
|
86
|
+
shutdown(code ?? 1)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
process.on('SIGINT', () => shutdown(0))
|
|
90
|
+
process.on('SIGTERM', () => shutdown(0))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function chromeArgs() {
|
|
94
|
+
const args = [
|
|
95
|
+
`--remote-debugging-port=${DEBUG_PORT}`,
|
|
96
|
+
`--user-data-dir=${PROFILE_DIR}`,
|
|
97
|
+
'--no-first-run',
|
|
98
|
+
'--no-default-browser-check',
|
|
99
|
+
'--disable-background-networking',
|
|
100
|
+
'--disable-sync',
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
if (HEADLESS) {
|
|
104
|
+
args.push('--headless=new')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return args
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function waitForChrome(url, timeoutMs) {
|
|
111
|
+
const start = Date.now()
|
|
112
|
+
let lastError
|
|
113
|
+
|
|
114
|
+
while (Date.now() - start < timeoutMs) {
|
|
115
|
+
try {
|
|
116
|
+
const response = await fetch(url)
|
|
117
|
+
if (response.ok) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
lastError = new Error(`HTTP ${response.status}`)
|
|
121
|
+
} catch (error) {
|
|
122
|
+
lastError = error
|
|
123
|
+
}
|
|
124
|
+
await sleep(500)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw new Error(`Chrome debugger did not become ready within ${timeoutMs}ms: ${String(lastError)}`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function detectChromePath() {
|
|
131
|
+
const candidates = [
|
|
132
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
133
|
+
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
|
134
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
return candidates.find((candidate) => existsSync(candidate))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function localBin(name) {
|
|
141
|
+
const suffix = process.platform === 'win32' ? '.cmd' : ''
|
|
142
|
+
return join(projectRoot, 'node_modules', '.bin', `${name}${suffix}`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sleep(ms) {
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
setTimeout(resolve, ms)
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function shutdown(code) {
|
|
152
|
+
if (proxyProcess && !proxyProcess.killed) {
|
|
153
|
+
proxyProcess.kill('SIGTERM')
|
|
154
|
+
}
|
|
155
|
+
if (chromeProcess && !chromeProcess.killed) {
|
|
156
|
+
chromeProcess.kill('SIGTERM')
|
|
157
|
+
}
|
|
158
|
+
process.exit(code)
|
|
159
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: chromedev
|
|
3
|
+
description: Use this skill when you need to access or control a live Chrome browser through the local Chrome DevTools MCP middleware at http://127.0.0.1:8787/mcp, especially for opening pages, extracting rendered content, interacting with DOM elements, taking snapshots, or collecting data from websites.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# chromedev
|
|
7
|
+
|
|
8
|
+
Use this skill when the user wants browser-backed data from real web pages through the local `chrome-devtools-mcp` middleware running at `http://127.0.0.1:8787/mcp`.
|
|
9
|
+
|
|
10
|
+
This skill is for cases where rendered browser state matters: JavaScript-heavy sites, login state in the user's Chrome, interaction flows, screenshots, DOM snapshots, or page data that should come from the live browser instead of plain HTTP fetches.
|
|
11
|
+
|
|
12
|
+
## Preconditions
|
|
13
|
+
|
|
14
|
+
- The local middleware must already be running on `127.0.0.1:8787`.
|
|
15
|
+
- The middleware usually runs via the user's `chromeDev` project and uses Chrome `--autoConnect`.
|
|
16
|
+
- On first connection, Chrome may show a remote-debugging authorization prompt. Wait up to 30 seconds for the user to approve it.
|
|
17
|
+
|
|
18
|
+
Quick endpoint check:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
curl -i http://127.0.0.1:8787/mcp
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
A response like `400 Bad Request` with `No sessionId` means the endpoint exists and is healthy.
|
|
25
|
+
|
|
26
|
+
## Workflow
|
|
27
|
+
|
|
28
|
+
1. Confirm the middleware is reachable.
|
|
29
|
+
2. Connect to `http://127.0.0.1:8787/mcp`.
|
|
30
|
+
3. Use MCP browser tools to open or select a page.
|
|
31
|
+
4. Prefer `take_snapshot` or `evaluate_script` for structured extraction.
|
|
32
|
+
5. Use `wait_for` when content depends on async rendering.
|
|
33
|
+
6. Return the extracted data, not raw protocol noise.
|
|
34
|
+
|
|
35
|
+
## Preferred tool order
|
|
36
|
+
|
|
37
|
+
- `list_pages`: inspect current browser pages before changing state.
|
|
38
|
+
- `new_page`: open a URL when the user wants navigation.
|
|
39
|
+
- `select_page`: switch to the relevant tab before reading or interacting.
|
|
40
|
+
- `take_snapshot`: get accessible text/structure from the current page.
|
|
41
|
+
- `evaluate_script`: extract structured values from the live DOM.
|
|
42
|
+
- `wait_for`: wait for a known string when the page is still rendering.
|
|
43
|
+
- `take_screenshot`: use only when the visual result matters.
|
|
44
|
+
|
|
45
|
+
## Command-line client
|
|
46
|
+
|
|
47
|
+
This skill includes a reusable script:
|
|
48
|
+
|
|
49
|
+
- [`scripts/http_mcp_call.mjs`](/Volumes/MacMiniDisk/project/chromeDev/skills/chromedev/scripts/http_mcp_call.mjs)
|
|
50
|
+
|
|
51
|
+
It connects to the local HTTP MCP endpoint and calls one tool.
|
|
52
|
+
|
|
53
|
+
If `@modelcontextprotocol/sdk` is missing in the current workspace, install it in that workspace first:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install @modelcontextprotocol/sdk
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Common commands
|
|
60
|
+
|
|
61
|
+
List current pages:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
node /Volumes/MacMiniDisk/project/chromeDev/skills/chromedev/scripts/http_mcp_call.mjs list_pages
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Open `163.com`:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
node /Volumes/MacMiniDisk/project/chromeDev/skills/chromedev/scripts/http_mcp_call.mjs new_page '{"url":"https://www.163.com/","timeout":30000}'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Get the current page snapshot:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
node /Volumes/MacMiniDisk/project/chromeDev/skills/chromedev/scripts/http_mcp_call.mjs take_snapshot
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Extract page title and URL:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
node /Volumes/MacMiniDisk/project/chromeDev/skills/chromedev/scripts/http_mcp_call.mjs evaluate_script '{"function":"() => ({ title: document.title, url: location.href })"}'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Wait for specific text:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
node /Volumes/MacMiniDisk/project/chromeDev/skills/chromedev/scripts/http_mcp_call.mjs wait_for '{"text":["网易","163"],"timeout":30000}'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Extraction guidance
|
|
92
|
+
|
|
93
|
+
- Use `take_snapshot` for readable page summaries, navigation labels, and visible content.
|
|
94
|
+
- Use `evaluate_script` for precise fields, arrays, links, prices, tables, or JSON-shaped output.
|
|
95
|
+
- Use `wait_for` before reading if the site is client-rendered or slow.
|
|
96
|
+
- Use `list_network_requests` and `get_network_request` only when DOM extraction is insufficient.
|
|
97
|
+
|
|
98
|
+
## Example patterns
|
|
99
|
+
|
|
100
|
+
For article text:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
node /Volumes/MacMiniDisk/project/chromeDev/skills/chromedev/scripts/http_mcp_call.mjs evaluate_script '{"function":"() => ({ title: document.title, text: document.body.innerText.slice(0, 4000) })"}'
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For links on the page:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
node /Volumes/MacMiniDisk/project/chromeDev/skills/chromedev/scripts/http_mcp_call.mjs evaluate_script '{"function":"() => Array.from(document.querySelectorAll(\"a\")).slice(0,50).map(a => ({ text: (a.innerText || a.textContent || \"\").trim(), href: a.href })).filter(x => x.href)"}'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Operational notes
|
|
113
|
+
|
|
114
|
+
- Keep the browser state intact unless the user asked for navigation or interaction.
|
|
115
|
+
- If a tool call hangs near connection start, assume Chrome may be waiting for the authorization dialog.
|
|
116
|
+
- Prefer returning concise extracted data over full raw snapshots unless the user asked for raw output.
|
|
117
|
+
- Do not expose the local MCP endpoint outside `127.0.0.1`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "ChromeDev"
|
|
3
|
+
short_description: "Use local Chrome MCP for live browsing"
|
|
4
|
+
default_prompt: "Use $chromedev to access the user's live Chrome through the local MCP middleware and extract data from webpages."
|
|
5
|
+
|
|
6
|
+
dependencies:
|
|
7
|
+
tools:
|
|
8
|
+
- type: "mcp"
|
|
9
|
+
value: "chromedev-local"
|
|
10
|
+
description: "Local chrome-devtools-mcp middleware for browser access"
|
|
11
|
+
transport: "streamable_http"
|
|
12
|
+
url: "http://127.0.0.1:8787/mcp"
|
|
13
|
+
|
|
14
|
+
policy:
|
|
15
|
+
allow_implicit_invocation: true
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
const [toolName, rawArgs] = process.argv.slice(2)
|
|
9
|
+
|
|
10
|
+
if (!toolName) {
|
|
11
|
+
printUsage()
|
|
12
|
+
process.exit(1)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let args = {}
|
|
16
|
+
if (rawArgs) {
|
|
17
|
+
try {
|
|
18
|
+
args = JSON.parse(rawArgs)
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error(`Invalid JSON args: ${error.message}`)
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let Client
|
|
26
|
+
let StreamableHTTPClientTransport
|
|
27
|
+
try {
|
|
28
|
+
const require = createRequire(import.meta.url)
|
|
29
|
+
const clientModule = require.resolve('@modelcontextprotocol/sdk/client/index.js', {
|
|
30
|
+
paths: [process.cwd()],
|
|
31
|
+
})
|
|
32
|
+
const transportModule = require.resolve('@modelcontextprotocol/sdk/client/streamableHttp.js', {
|
|
33
|
+
paths: [process.cwd()],
|
|
34
|
+
})
|
|
35
|
+
;({ Client } = await import(pathToFileURL(clientModule).href))
|
|
36
|
+
;({ StreamableHTTPClientTransport } = await import(pathToFileURL(transportModule).href))
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Missing dependency: @modelcontextprotocol/sdk')
|
|
39
|
+
console.error('Install it in the current workspace with: npm install @modelcontextprotocol/sdk')
|
|
40
|
+
console.error(String(error))
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const endpoint = process.env.CHROMEDEV_MCP_URL || 'http://127.0.0.1:8787/mcp'
|
|
45
|
+
const client = new Client({ name: 'chromedev-skill-client', version: '1.0.0' }, { capabilities: {} })
|
|
46
|
+
const transport = new StreamableHTTPClientTransport(new URL(endpoint))
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await client.connect(transport)
|
|
50
|
+
const result = await client.callTool({ name: toolName, arguments: args })
|
|
51
|
+
console.log(JSON.stringify(result, null, 2))
|
|
52
|
+
} finally {
|
|
53
|
+
await transport.close().catch(() => {})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function printUsage() {
|
|
58
|
+
console.error('Usage: http_mcp_call.mjs <tool_name> [json_args]')
|
|
59
|
+
console.error("Example: http_mcp_call.mjs new_page '{\"url\":\"https://www.163.com/\",\"timeout\":30000}'")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main().catch((error) => {
|
|
63
|
+
console.error(error)
|
|
64
|
+
process.exit(1)
|
|
65
|
+
})
|