@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 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
+ })