chromex-mcp 1.1.0 → 1.3.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 +178 -83
- package/bin/chromex.mjs +2 -0
- package/package.json +2 -1
- package/plugins/chromex/skills/chromex/scripts/chromex.mjs +3 -1
- package/plugins/chromex/skills/chromex/scripts/lib/commands/snapshot.mjs +58 -3
- package/plugins/chromex/skills/chromex/scripts/lib/daemon.mjs +43 -5
- package/plugins/chromex/skills/chromex/scripts/mcp-server.mjs +31 -14
package/README.md
CHANGED
|
@@ -1,50 +1,87 @@
|
|
|
1
1
|
# Chromex
|
|
2
2
|
|
|
3
|
-
Zero-dependency Chrome DevTools Protocol
|
|
4
|
-
|
|
5
|
-
Built as a [Claude Code](https://claude.ai/code) plugin but works standalone with any AI agent or from the terminal.
|
|
3
|
+
Zero-dependency Chrome DevTools Protocol toolkit for AI agents. 52 typed MCP tools + CLI. Connects directly to Chrome, Brave, Edge, or Chromium via WebSocket. No Puppeteer, no bloat.
|
|
6
4
|
|
|
7
5
|
## Features
|
|
8
6
|
|
|
9
|
-
- **
|
|
7
|
+
- **52 MCP tools** -- typed JSON Schema, annotations (`readOnlyHint`, `destructiveHint`), inline screenshots (base64)
|
|
10
8
|
- **Zero dependencies** -- uses only Node.js 22+ built-in modules (WebSocket, fs, net, crypto)
|
|
11
9
|
- **Ref-based selection** -- `snap --refs` assigns `@e1`, `@e2`... to interactive elements, then `click @e5` or `fill @e3 "value"`. No fragile CSS selectors
|
|
10
|
+
- **Incremental snapshots** -- second snapshot returns only changed nodes (diff), reducing output from thousands of lines to just what changed
|
|
11
|
+
- **Auto-snapshot** -- interactive commands (click, fill, nav, etc.) automatically append an incremental snapshot with refs, so the agent sees the page state in a single round-trip
|
|
12
|
+
- **Scroll detection** -- snapshots report scrollable containers with remaining distance (`[scroll: page: down:1200px | sidebar: up:300px]`)
|
|
12
13
|
- **Per-tab persistent daemons** -- each tab gets a background process connected via Unix socket. Chrome's "Allow debugging" modal fires once, not on every command
|
|
13
14
|
- **Security hardened** -- domain filtering (allow/blocklist), CDP method blocklist, token-authenticated sockets, full audit log
|
|
14
15
|
- **Multi-browser** -- auto-detects Brave, Chrome, Chrome Canary, Chromium, Edge, Vivaldi (macOS + Linux)
|
|
15
16
|
- **Network control** -- throttle to 3G/offline, intercept & mock requests, record HAR files
|
|
16
17
|
- **Form filling** -- fill inputs, select dropdowns, toggle checkboxes, upload files, batch fill entire forms. Works with React/Vue/Angular
|
|
17
18
|
- **Browser launcher** -- launch browser with remote debugging pre-enabled (skips the "Allow debugging" modal entirely)
|
|
19
|
+
- **CLI included** -- same commands available from the terminal for scripts and CI/CD
|
|
18
20
|
|
|
19
21
|
## Requirements
|
|
20
22
|
|
|
21
23
|
- Node.js 22+ (for built-in WebSocket)
|
|
22
24
|
- Any Chromium-based browser
|
|
23
25
|
|
|
24
|
-
##
|
|
25
|
-
|
|
26
|
-
### As a Claude Code plugin
|
|
26
|
+
## Installation
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
# Add
|
|
30
|
-
|
|
29
|
+
# Add to Claude Code (global -- all projects)
|
|
30
|
+
claude mcp add chromex -s user npx chromex-mcp@latest
|
|
31
31
|
|
|
32
|
-
#
|
|
33
|
-
|
|
32
|
+
# Or project-only
|
|
33
|
+
claude mcp add chromex npx chromex-mcp@latest
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
###
|
|
36
|
+
### Auto-Approve (recommended)
|
|
37
|
+
|
|
38
|
+
Add to `~/.claude/settings.json`:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"permissions": {
|
|
43
|
+
"allow": ["mcp__chromex"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This approves all 52 MCP tools at once. For granular control, approve individual tools:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"permissions": {
|
|
53
|
+
"allow": [
|
|
54
|
+
"mcp__chromex__chromex_list",
|
|
55
|
+
"mcp__chromex__chromex_snapshot",
|
|
56
|
+
"mcp__chromex__chromex_screenshot",
|
|
57
|
+
"mcp__chromex__chromex_perf"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Global install (optional)
|
|
37
64
|
|
|
38
65
|
```bash
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
66
|
+
npm install -g chromex-mcp
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This installs three binaries:
|
|
70
|
+
|
|
71
|
+
| Binary | Purpose |
|
|
72
|
+
|--------|---------|
|
|
73
|
+
| `chromex` | CLI -- the main command for terminal usage |
|
|
74
|
+
| `chromex-cli` | Alias for `chromex` |
|
|
75
|
+
| `chromex-mcp` | MCP server (stdio JSON-RPC) -- used by `claude mcp add`, not run directly |
|
|
42
76
|
|
|
43
|
-
|
|
44
|
-
|
|
77
|
+
```bash
|
|
78
|
+
chromex list # List open tabs
|
|
79
|
+
chromex launch --url https://example.com # Launch browser
|
|
80
|
+
chromex snap 6BE8 --refs # Snapshot with refs
|
|
81
|
+
chromex click 6BE8 @e3 # Click by ref
|
|
45
82
|
```
|
|
46
83
|
|
|
47
|
-
|
|
84
|
+
## Connect to Your Browser
|
|
48
85
|
|
|
49
86
|
**Option A: Launch a new browser** (recommended -- no setup needed)
|
|
50
87
|
|
|
@@ -56,8 +93,6 @@ This starts Chrome/Brave/Edge with remote debugging pre-enabled. No manual confi
|
|
|
56
93
|
|
|
57
94
|
**Option B: Connect to an already-running browser**
|
|
58
95
|
|
|
59
|
-
You **must** enable remote debugging first:
|
|
60
|
-
|
|
61
96
|
1. Open your browser (Chrome, Brave, Edge, etc.)
|
|
62
97
|
2. Navigate to `chrome://inspect/#remote-debugging`
|
|
63
98
|
3. **Toggle the switch ON** to enable remote debugging
|
|
@@ -67,11 +102,7 @@ You **must** enable remote debugging first:
|
|
|
67
102
|
|
|
68
103
|
> **Note:** With Option B, Chrome will show an "Allow debugging" dialog the first time you access each tab. Click "Allow" once per tab -- the daemon keeps the session alive after that.
|
|
69
104
|
|
|
70
|
-
|
|
71
|
-
chromex list
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### Your first commands
|
|
105
|
+
## Quick Start
|
|
75
106
|
|
|
76
107
|
```bash
|
|
77
108
|
# List open tabs
|
|
@@ -118,8 +149,10 @@ chromex stop # Stop all daemons
|
|
|
118
149
|
### Inspect
|
|
119
150
|
|
|
120
151
|
```bash
|
|
121
|
-
chromex snap <target> # Accessibility tree (
|
|
152
|
+
chromex snap <target> # Accessibility tree snapshot (compact)
|
|
122
153
|
chromex snap <target> --refs # With interactive refs (@e1, @e2...)
|
|
154
|
+
chromex snap <target> --depth=3 # Limit tree depth
|
|
155
|
+
chromex snap <target> --full # Force full snapshot (skip diff)
|
|
123
156
|
chromex html <target> "#main" # Element HTML by selector
|
|
124
157
|
chromex shot <target> /tmp/page.png # Viewport screenshot
|
|
125
158
|
chromex shot <target> /tmp/full.png --full # Full page screenshot
|
|
@@ -151,8 +184,10 @@ chromex wait <target> load # Wait for page load
|
|
|
151
184
|
chromex wait <target> domready # Wait for DOMContentLoaded
|
|
152
185
|
chromex wait <target> fcp # Wait for First Contentful Paint
|
|
153
186
|
chromex scroll <target> down 500 # Scroll down 500px
|
|
154
|
-
chromex scroll <target>
|
|
187
|
+
chromex scroll <target> up 300 # Scroll up 300px
|
|
155
188
|
chromex scroll <target> top # Scroll to top
|
|
189
|
+
chromex scroll <target> bottom # Scroll to bottom
|
|
190
|
+
chromex scroll <target> to "#footer" # Scroll to element
|
|
156
191
|
```
|
|
157
192
|
|
|
158
193
|
### Interact
|
|
@@ -207,6 +242,7 @@ chromex pdf <target> /tmp/page.pdf # Export as PDF
|
|
|
207
242
|
```bash
|
|
208
243
|
chromex throttle <target> 3g # Throttle to 3G
|
|
209
244
|
chromex throttle <target> slow-3g # Throttle to slow 3G
|
|
245
|
+
chromex throttle <target> 4g # Throttle to 4G
|
|
210
246
|
chromex throttle <target> offline # Go offline
|
|
211
247
|
chromex throttle <target> custom 200 1000 500 # Custom: latency, down, up (kbps)
|
|
212
248
|
chromex throttle <target> reset # Remove throttling
|
|
@@ -224,8 +260,12 @@ chromex har <target> stop /tmp/trace.har # Save HAR file
|
|
|
224
260
|
|
|
225
261
|
```bash
|
|
226
262
|
chromex emulate <target> iphone-14 # 390x844 @3x mobile
|
|
263
|
+
chromex emulate <target> iphone-15-pro # 393x852 @3x mobile
|
|
227
264
|
chromex emulate <target> ipad-pro # 1024x1366 @2x tablet
|
|
228
265
|
chromex emulate <target> pixel-7 # 412x915 @2.625x mobile
|
|
266
|
+
chromex emulate <target> galaxy-s23 # 360x780 @3x mobile
|
|
267
|
+
chromex emulate <target> macbook-air # 1440x900 @2x laptop
|
|
268
|
+
chromex emulate <target> desktop-1080p # 1920x1080 @1x
|
|
229
269
|
chromex emulate <target> desktop-4k # 3840x2160 @1x
|
|
230
270
|
chromex emulate <target> reset # Reset to default
|
|
231
271
|
chromex geo <target> -23.55 -46.63 # Set geolocation (Sao Paulo)
|
|
@@ -236,8 +276,6 @@ chromex cpu <target> 4 # CPU 4x slower
|
|
|
236
276
|
chromex cpu <target> reset # Reset CPU speed
|
|
237
277
|
```
|
|
238
278
|
|
|
239
|
-
Available devices: `iphone-14`, `iphone-15-pro`, `ipad-pro`, `pixel-7`, `galaxy-s23`, `macbook-air`, `desktop-1080p`, `desktop-4k`.
|
|
240
|
-
|
|
241
279
|
### Advanced
|
|
242
280
|
|
|
243
281
|
```bash
|
|
@@ -278,73 +316,127 @@ chromex click <target> @e3
|
|
|
278
316
|
chromex click <target> @e4
|
|
279
317
|
```
|
|
280
318
|
|
|
281
|
-
Refs are assigned to all interactive elements (buttons, links, inputs, checkboxes, radios, dropdowns, tabs, switches). They persist until the next `snap --refs` call.
|
|
319
|
+
Refs are assigned to all interactive elements (buttons, links, inputs, checkboxes, radios, dropdowns, tabs, switches, sliders, search boxes). They persist until the next `snap --refs` call.
|
|
282
320
|
|
|
283
321
|
Supported ref commands: `click @eN`, `fill @eN "value"`, `hover @eN`.
|
|
284
322
|
|
|
285
|
-
##
|
|
323
|
+
## Snapshot Optimizations
|
|
324
|
+
|
|
325
|
+
Chromex snapshots are designed to minimize token usage for AI agents.
|
|
286
326
|
|
|
287
|
-
|
|
327
|
+
### Incremental Diff
|
|
288
328
|
|
|
289
|
-
|
|
329
|
+
The first snapshot returns the full accessibility tree. Subsequent snapshots return only nodes that changed:
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
[incremental: 2 changed, 45 unchanged]
|
|
333
|
+
*[textbox] Email = "user@example.com"
|
|
334
|
+
*[button] Submit
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
- Changed/new nodes are marked with `*`
|
|
338
|
+
- Unchanged subtrees are collapsed entirely
|
|
339
|
+
- Use `--full` to force a full snapshot (skips diff)
|
|
340
|
+
- Navigation (`nav`) resets the diff baseline automatically
|
|
341
|
+
|
|
342
|
+
### Depth Limiting
|
|
343
|
+
|
|
344
|
+
Limit tree depth for large pages:
|
|
290
345
|
|
|
291
346
|
```bash
|
|
292
|
-
#
|
|
293
|
-
|
|
347
|
+
chromex snap <target> --depth=3 # Only 3 levels deep
|
|
348
|
+
```
|
|
294
349
|
|
|
295
|
-
|
|
296
|
-
|
|
350
|
+
Nodes at the depth limit render as leaves (children are not expanded).
|
|
351
|
+
|
|
352
|
+
### Scroll Detection
|
|
353
|
+
|
|
354
|
+
Snapshots automatically detect scrollable containers and report remaining scroll distance:
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
[scroll: page: down:1200px | sidebar: up:300px, down:800px]
|
|
297
358
|
```
|
|
298
359
|
|
|
299
|
-
###
|
|
360
|
+
### Visibility Filtering
|
|
300
361
|
|
|
301
|
-
|
|
362
|
+
- Ignored/hidden accessibility nodes are automatically omitted
|
|
363
|
+
- Disabled interactive elements are shown but not assigned refs (can't be interacted with)
|
|
364
|
+
- Generic wrapper nodes (`div`, `span` with no semantic role) are collapsed -- their children inherit the parent's depth
|
|
365
|
+
- Names longer than 200 characters are truncated with `...`
|
|
302
366
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
367
|
+
## Auto-Snapshot
|
|
368
|
+
|
|
369
|
+
Interactive commands automatically append an incremental snapshot with refs after execution. This lets the AI agent see the updated page state without a separate `snap` call:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
chromex click <target> @e3
|
|
373
|
+
# Output:
|
|
374
|
+
# Clicked @e3 [button] "Submit"
|
|
375
|
+
#
|
|
376
|
+
# [incremental: 5 changed, 40 unchanged]
|
|
377
|
+
# @e1 [heading] Thank you!
|
|
378
|
+
# @e2 [link] Back to home
|
|
379
|
+
# ...
|
|
309
380
|
```
|
|
310
381
|
|
|
311
|
-
|
|
382
|
+
Commands that trigger auto-snapshot: `click`, `clickxy`, `type`, `fill`, `clear`, `select`, `check`, `form`, `nav`, `dialog`, `loadall`, `drag`, `touch`, `upload`.
|
|
312
383
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
"mcp__chromex__chromex_screenshot",
|
|
320
|
-
"mcp__chromex__chromex_perf"
|
|
321
|
-
]
|
|
322
|
-
}
|
|
323
|
-
}
|
|
384
|
+
Suppress with `--no-snap` for scripts doing rapid sequential actions:
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
chromex fill <target> @e1 "user@test.com" --no-snap
|
|
388
|
+
chromex fill <target> @e2 "secret123" --no-snap
|
|
389
|
+
chromex click <target> @e3 # Only this one triggers snapshot
|
|
324
390
|
```
|
|
325
391
|
|
|
326
|
-
|
|
392
|
+
## MCP vs CLI
|
|
327
393
|
|
|
328
|
-
|
|
394
|
+
Both interfaces call the same core, same daemons, same commands. The difference is how they integrate with Claude Code.
|
|
395
|
+
|
|
396
|
+
| | CLI (`chromex-cli`) | MCP Server (`chromex-mcp`) |
|
|
329
397
|
|---|---|---|
|
|
330
|
-
|
|
|
398
|
+
| Token overhead | Zero schema cost | ~500 tokens per tool used (deferred loading) |
|
|
399
|
+
| Auto-approve | Glob pattern in settings | `"mcp__chromex"` -- one line |
|
|
331
400
|
| Permissions | All-or-nothing | Per-tool granularity |
|
|
332
401
|
| Parameters | Positional string args | Typed JSON Schema |
|
|
333
|
-
|
|
|
334
|
-
|
|
|
335
|
-
|
|
402
|
+
| Screenshots | File path (needs `Read` to view) | Inline image (base64, no extra call) |
|
|
403
|
+
| Best for | Terminal, scripts, CI/CD, token-sensitive sessions | Plug-and-play automation, users who want zero-config |
|
|
404
|
+
|
|
405
|
+
### Switching Between MCP and CLI
|
|
336
406
|
|
|
337
|
-
|
|
407
|
+
You can enable and disable the MCP server at any time. The CLI always works regardless.
|
|
338
408
|
|
|
339
|
-
|
|
409
|
+
**Disable MCP** (saves ~3-5k tokens per session):
|
|
340
410
|
|
|
341
411
|
```bash
|
|
342
|
-
|
|
343
|
-
chromex-mcp # Run MCP server
|
|
344
|
-
chromex-cli list # CLI also available
|
|
412
|
+
claude mcp remove chromex
|
|
345
413
|
```
|
|
346
414
|
|
|
347
|
-
|
|
415
|
+
The CLI (`chromex-cli`) continues working normally -- same commands, same daemons, no change.
|
|
416
|
+
|
|
417
|
+
**Re-enable MCP:**
|
|
418
|
+
|
|
419
|
+
```bash
|
|
420
|
+
# Global (all projects)
|
|
421
|
+
claude mcp add chromex -s user npx chromex-mcp@latest
|
|
422
|
+
|
|
423
|
+
# Project-only
|
|
424
|
+
claude mcp add chromex npx chromex-mcp@latest
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
**When to disable MCP:**
|
|
428
|
+
- Token-sensitive sessions where every token counts
|
|
429
|
+
- You only need occasional commands (`list`, `snap`, `shot`)
|
|
430
|
+
- You're already comfortable with the CLI syntax
|
|
431
|
+
- You're using chromex from terminal/scripts, not from Claude Code
|
|
432
|
+
|
|
433
|
+
**When to keep MCP enabled:**
|
|
434
|
+
- Automating multi-step browser workflows (snap -> click -> fill -> snap)
|
|
435
|
+
- You want per-tool auto-approve without glob patterns
|
|
436
|
+
- You want inline screenshots without a separate `Read` call
|
|
437
|
+
- First time using chromex (discovery via tool schema)
|
|
438
|
+
|
|
439
|
+
### CLI Auto-Approve
|
|
348
440
|
|
|
349
441
|
If you prefer the CLI interface, add this to `~/.claude/settings.json`:
|
|
350
442
|
|
|
@@ -352,14 +444,12 @@ If you prefer the CLI interface, add this to `~/.claude/settings.json`:
|
|
|
352
444
|
{
|
|
353
445
|
"permissions": {
|
|
354
446
|
"allow": [
|
|
355
|
-
"Bash(
|
|
447
|
+
"Bash(chromex-cli *)"
|
|
356
448
|
]
|
|
357
449
|
}
|
|
358
450
|
}
|
|
359
451
|
```
|
|
360
452
|
|
|
361
|
-
> **Warning:** This approves all chromex commands without distinction. The security config (`~/.chromex/config.json`) still applies -- domain filtering, CDP blocklist, and audit log remain active.
|
|
362
|
-
|
|
363
453
|
## Security
|
|
364
454
|
|
|
365
455
|
Config at `~/.chromex/config.json` (auto-created on first run):
|
|
@@ -376,11 +466,26 @@ Config at `~/.chromex/config.json` (auto-created on first run):
|
|
|
376
466
|
|
|
377
467
|
- **Domain filtering**: block sensitive sites or restrict to a whitelist
|
|
378
468
|
- **CDP blocklist**: dangerous methods blocked by default in `evalraw`
|
|
379
|
-
- **Socket auth**: 32-byte random token per session
|
|
469
|
+
- **Socket auth**: 32-byte random token per session (mode 0600)
|
|
380
470
|
- **Audit log**: every command logged with timestamp and status
|
|
381
471
|
|
|
382
472
|
See [docs/security.md](docs/security.md) for full details.
|
|
383
473
|
|
|
474
|
+
## How It Works
|
|
475
|
+
|
|
476
|
+
1. **Browser detection** -- scans ~30 paths for `DevToolsActivePort` (or use `CDP_PORT_FILE` env var)
|
|
477
|
+
2. **Daemon spawn** -- first command to a tab spawns a background Node.js process connected via CDP WebSocket
|
|
478
|
+
3. **Session persistence** -- daemon holds the session open; Chrome's "Allow" modal fires once per daemon
|
|
479
|
+
4. **Unix sockets** -- CLI/MCP communicates with daemon via authenticated Unix sockets
|
|
480
|
+
5. **Auto-exit** -- daemons shut down after 20 minutes of inactivity (configurable)
|
|
481
|
+
|
|
482
|
+
```
|
|
483
|
+
CLI Client ──Unix Socket + Auth──> Per-Tab Daemon ──CDP WebSocket──> Chrome
|
|
484
|
+
MCP Server ──Unix Socket + Auth──> (same) ──CDP WebSocket──> Chrome
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
See [docs/architecture.md](docs/architecture.md) for the full deep dive.
|
|
488
|
+
|
|
384
489
|
## Documentation
|
|
385
490
|
|
|
386
491
|
| Guide | Description |
|
|
@@ -396,16 +501,6 @@ See [docs/security.md](docs/security.md) for full details.
|
|
|
396
501
|
| [Advanced](docs/advanced.md) | Script injection, code coverage, tracing, heap snapshots, WebAuthn |
|
|
397
502
|
| [Architecture](docs/architecture.md) | How it works: daemon model, connection modes, file layout |
|
|
398
503
|
|
|
399
|
-
## How It Works
|
|
400
|
-
|
|
401
|
-
1. **Browser detection** -- scans ~30 paths for `DevToolsActivePort` (or use `CDP_PORT_FILE` env var)
|
|
402
|
-
2. **Daemon spawn** -- first command to a tab spawns a background Node.js process connected via CDP WebSocket
|
|
403
|
-
3. **Session persistence** -- daemon holds the session open; Chrome's "Allow" modal fires once per daemon
|
|
404
|
-
4. **Unix sockets** -- CLI communicates with daemon via authenticated Unix sockets
|
|
405
|
-
5. **Auto-exit** -- daemons shut down after 20 minutes of inactivity (configurable)
|
|
406
|
-
|
|
407
|
-
See [docs/architecture.md](docs/architecture.md) for the full deep dive.
|
|
408
|
-
|
|
409
504
|
## License
|
|
410
505
|
|
|
411
506
|
MIT
|
package/bin/chromex.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chromex-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Zero-dependency Chrome DevTools Protocol MCP server for AI agents. 52 typed tools, per-tab daemons, security hardened.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
+
"chromex": "./bin/chromex.mjs",
|
|
7
8
|
"chromex-mcp": "./bin/chromex-mcp.mjs",
|
|
8
9
|
"chromex-cli": "./bin/chromex-cli.mjs"
|
|
9
10
|
},
|
|
@@ -244,7 +244,8 @@ async function main() {
|
|
|
244
244
|
|
|
245
245
|
const conn = await getOrStartTabDaemon(targetId, config);
|
|
246
246
|
|
|
247
|
-
const
|
|
247
|
+
const noSnap = args.includes('--no-snap');
|
|
248
|
+
const cmdArgs = args.slice(1).filter(a => a !== '--no-snap');
|
|
248
249
|
|
|
249
250
|
// Juntar argumentos para comandos que aceitam texto livre
|
|
250
251
|
if (cmd === 'eval') {
|
|
@@ -278,6 +279,7 @@ async function main() {
|
|
|
278
279
|
process.exit(1);
|
|
279
280
|
}
|
|
280
281
|
|
|
282
|
+
if (noSnap) cmdArgs.push('--no-snap');
|
|
281
283
|
const response = await sendCommand(conn, { cmd, args: cmdArgs });
|
|
282
284
|
|
|
283
285
|
if (response.ok) {
|
|
@@ -38,7 +38,7 @@ function truncate(str, max = MAX_NAME_LENGTH) {
|
|
|
38
38
|
return str.slice(0, max) + '...';
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function formatAxNode(node, depth, refIndex, refs) {
|
|
41
|
+
function formatAxNode(node, depth, refIndex, refs, isNew = false) {
|
|
42
42
|
const role = node.role?.value || '';
|
|
43
43
|
const name = truncate(node.name?.value ?? '');
|
|
44
44
|
const value = node.value?.value;
|
|
@@ -50,7 +50,8 @@ function formatAxNode(node, depth, refIndex, refs) {
|
|
|
50
50
|
refIndex.value++;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
const newTag = isNew ? '*' : '';
|
|
54
|
+
let line = `${indent}${newTag}${refTag}[${role}]`;
|
|
54
55
|
if (name !== '') line += ` ${name}`;
|
|
55
56
|
if (!(value === '' || value == null)) line += ` = ${JSON.stringify(truncate(String(value)))}`;
|
|
56
57
|
return line;
|
|
@@ -91,6 +92,54 @@ function buildFingerprints(nodes, nodesById, childrenByParent, compact) {
|
|
|
91
92
|
return fingerprints;
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
// Detect scrollable containers. Returns human-readable summary lines.
|
|
96
|
+
// Only reports containers with overflow:auto|scroll and >50px hidden content.
|
|
97
|
+
async function detectScrollables(cdp, sid) {
|
|
98
|
+
try {
|
|
99
|
+
const { result } = await cdp.send('Runtime.evaluate', {
|
|
100
|
+
expression: `(() => {
|
|
101
|
+
const MIN = 50;
|
|
102
|
+
const out = [];
|
|
103
|
+
const walk = (el, path) => {
|
|
104
|
+
if (el.nodeType !== 1 || out.length >= 10) return;
|
|
105
|
+
const sh = el.scrollHeight, sw = el.scrollWidth;
|
|
106
|
+
const ch = el.clientHeight, cw = el.clientWidth;
|
|
107
|
+
const isRoot = el === document.documentElement || el === document.body;
|
|
108
|
+
const style = isRoot ? null : getComputedStyle(el);
|
|
109
|
+
const oy = isRoot ? 'auto' : style.overflowY;
|
|
110
|
+
const ox = isRoot ? 'auto' : style.overflowX;
|
|
111
|
+
const scrollableY = isRoot || oy === 'auto' || oy === 'scroll';
|
|
112
|
+
const scrollableX = isRoot || ox === 'auto' || ox === 'scroll';
|
|
113
|
+
if (((scrollableY && sh > ch + MIN) || (scrollableX && sw > cw + MIN)) && ch > 0) {
|
|
114
|
+
const dirs = [];
|
|
115
|
+
if (scrollableY && sh > ch + MIN) {
|
|
116
|
+
const down = sh - ch - el.scrollTop;
|
|
117
|
+
const up = el.scrollTop;
|
|
118
|
+
if (up > MIN) dirs.push('up:' + Math.round(up) + 'px');
|
|
119
|
+
if (down > MIN) dirs.push('down:' + Math.round(down) + 'px');
|
|
120
|
+
}
|
|
121
|
+
if (scrollableX && sw > cw + MIN) {
|
|
122
|
+
const right = sw - cw - el.scrollLeft;
|
|
123
|
+
const left = el.scrollLeft;
|
|
124
|
+
if (left > MIN) dirs.push('left:' + Math.round(left) + 'px');
|
|
125
|
+
if (right > MIN) dirs.push('right:' + Math.round(right) + 'px');
|
|
126
|
+
}
|
|
127
|
+
if (dirs.length) {
|
|
128
|
+
const label = isRoot ? 'page' : (el.getAttribute('aria-label') || el.getAttribute('role') || el.id || el.tagName.toLowerCase());
|
|
129
|
+
out.push(label + ': ' + dirs.join(', '));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const c of el.children) walk(c);
|
|
133
|
+
};
|
|
134
|
+
walk(document.documentElement);
|
|
135
|
+
return out;
|
|
136
|
+
})()`,
|
|
137
|
+
returnByValue: true,
|
|
138
|
+
}, sid);
|
|
139
|
+
return result?.value || [];
|
|
140
|
+
} catch { return []; }
|
|
141
|
+
}
|
|
142
|
+
|
|
94
143
|
// refMap is populated when refs=true: { refNumber -> { backendNodeId, role, name } }
|
|
95
144
|
// The caller (daemon) stores this map for later ref resolution.
|
|
96
145
|
// previousFingerprints: Map from prior snapshot for incremental diff.
|
|
@@ -108,6 +157,7 @@ export async function snapshotStr(cdp, sid, compact = true, refs = false, previo
|
|
|
108
157
|
|
|
109
158
|
const currentFingerprints = buildFingerprints(nodes, nodesById, childrenByParent, compact);
|
|
110
159
|
const isDiff = previousFingerprints !== null && previousFingerprints.size > 0;
|
|
160
|
+
const scrollables = await detectScrollables(cdp, sid);
|
|
111
161
|
|
|
112
162
|
const refIndex = { value: 1 };
|
|
113
163
|
const refMap = new Map();
|
|
@@ -157,8 +207,9 @@ export async function snapshotStr(cdp, sid, compact = true, refs = false, previo
|
|
|
157
207
|
|
|
158
208
|
const role = node.role?.value || '';
|
|
159
209
|
const currentRef = refIndex.value;
|
|
210
|
+
const isNew = isDiff && !previousFingerprints.has(node.nodeId);
|
|
160
211
|
|
|
161
|
-
lines.push(formatAxNode(node, depth, refIndex, refs));
|
|
212
|
+
lines.push(formatAxNode(node, depth, refIndex, refs, isNew));
|
|
162
213
|
|
|
163
214
|
if (refs && refIndex.value > currentRef) {
|
|
164
215
|
refMap.set(currentRef, {
|
|
@@ -214,6 +265,10 @@ export async function snapshotStr(cdp, sid, compact = true, refs = false, previo
|
|
|
214
265
|
const changedCount = totalVisible - unchangedCount;
|
|
215
266
|
text = `[incremental: ${changedCount} changed, ${unchangedCount} unchanged]\n${text}`;
|
|
216
267
|
}
|
|
268
|
+
// Append scroll info footer when scrollable containers exist
|
|
269
|
+
if (scrollables.length > 0) {
|
|
270
|
+
text += `\n[scroll: ${scrollables.join(' | ')}]`;
|
|
271
|
+
}
|
|
217
272
|
|
|
218
273
|
return { text, refMap, fingerprints: currentFingerprints };
|
|
219
274
|
}
|
|
@@ -46,6 +46,15 @@ import { touchStr } from './commands/touch.mjs';
|
|
|
46
46
|
import { domsnapshotStr } from './commands/domsnapshot.mjs';
|
|
47
47
|
import { parseRef, clickRefStr, hoverRefStr, fillRefStr } from './commands/refs.mjs';
|
|
48
48
|
import { highlightStr } from './commands/highlight.mjs';
|
|
49
|
+
import { sleep } from './utils.mjs';
|
|
50
|
+
|
|
51
|
+
// Commands that modify visible DOM and should trigger automatic post-action snapshot.
|
|
52
|
+
// After these commands, an incremental snapshot with refs is appended to the result,
|
|
53
|
+
// so the AI agent sees the page state without needing a separate snapshot call.
|
|
54
|
+
const AUTO_SNAP_CMDS = new Set([
|
|
55
|
+
'click', 'clickxy', 'type', 'fill', 'clear', 'select', 'check', 'form',
|
|
56
|
+
'nav', 'navigate', 'dialog', 'loadall', 'drag', 'touch', 'upload',
|
|
57
|
+
]);
|
|
49
58
|
|
|
50
59
|
export function getOrCreateToken(config) {
|
|
51
60
|
if (!config.socketAuth) return null;
|
|
@@ -114,10 +123,18 @@ export async function runDaemon(targetId, config) {
|
|
|
114
123
|
resetIdle();
|
|
115
124
|
const auditResult = { ok: true };
|
|
116
125
|
try {
|
|
126
|
+
// Strip --no-snap before dispatch so it doesn't contaminate command args
|
|
127
|
+
// (e.g. fill would type "--no-snap" into the input field).
|
|
128
|
+
const noSnap = args.includes('--no-snap');
|
|
129
|
+
if (noSnap) args = args.filter(a => a !== '--no-snap');
|
|
130
|
+
|
|
131
|
+
let result;
|
|
132
|
+
let isRefCmd = false;
|
|
133
|
+
|
|
117
134
|
// Ref-based dispatch: click @e5, fill @e3 "value", hover @e12
|
|
118
135
|
if (args[0] && parseRef(args[0]) !== null) {
|
|
136
|
+
isRefCmd = true;
|
|
119
137
|
const refNum = parseRef(args[0]);
|
|
120
|
-
let result;
|
|
121
138
|
if (cmd === 'click') {
|
|
122
139
|
result = await clickRefStr(cdp, sessionId, currentRefMap, refNum);
|
|
123
140
|
} else if (cmd === 'fill') {
|
|
@@ -127,12 +144,9 @@ export async function runDaemon(targetId, config) {
|
|
|
127
144
|
} else {
|
|
128
145
|
throw new Error(`Ref @e${refNum} not supported for command "${cmd}". Use with: click, fill, hover.`);
|
|
129
146
|
}
|
|
130
|
-
audit(cmd, targetId, args, auditResult, config);
|
|
131
|
-
return { ok: true, result };
|
|
132
147
|
}
|
|
133
148
|
|
|
134
|
-
|
|
135
|
-
switch (cmd) {
|
|
149
|
+
if (!isRefCmd) switch (cmd) {
|
|
136
150
|
// --- Comandos originais ---
|
|
137
151
|
case 'list': {
|
|
138
152
|
const pages = await getPages(cdp);
|
|
@@ -172,6 +186,7 @@ export async function runDaemon(targetId, config) {
|
|
|
172
186
|
break;
|
|
173
187
|
case 'nav': case 'navigate':
|
|
174
188
|
result = await navStr(cdp, sessionId, args[0], config);
|
|
189
|
+
previousFingerprints = null; // Reset: new page needs full snapshot
|
|
175
190
|
break;
|
|
176
191
|
case 'net': case 'network':
|
|
177
192
|
result = await netStr(cdp, sessionId);
|
|
@@ -322,6 +337,29 @@ export async function runDaemon(targetId, config) {
|
|
|
322
337
|
}
|
|
323
338
|
}
|
|
324
339
|
audit(cmd, targetId, args, auditResult, config);
|
|
340
|
+
|
|
341
|
+
// Auto-snapshot: append incremental snapshot with refs after DOM-modifying actions.
|
|
342
|
+
// This lets the AI agent see the resulting page state in a single round-trip.
|
|
343
|
+
// Opt-out with --no-snap for scripts doing rapid sequential actions.
|
|
344
|
+
const shouldSnap = isRefCmd
|
|
345
|
+
? (cmd === 'click' || cmd === 'fill') // hover doesn't change DOM
|
|
346
|
+
: AUTO_SNAP_CMDS.has(cmd);
|
|
347
|
+
|
|
348
|
+
if (shouldSnap && !noSnap) {
|
|
349
|
+
try {
|
|
350
|
+
// Navigate already waits for load+readyState; shorter settle for it.
|
|
351
|
+
// Other actions (click, fill) need more time for SPA re-renders.
|
|
352
|
+
const settleMs = (cmd === 'nav' || cmd === 'navigate') ? 100 : 300;
|
|
353
|
+
await sleep(settleMs);
|
|
354
|
+
const snapResult = await snapshotStr(cdp, sessionId, true, true, previousFingerprints);
|
|
355
|
+
previousFingerprints = snapResult.fingerprints;
|
|
356
|
+
if (snapResult.refMap.size > 0) {
|
|
357
|
+
currentRefMap = snapResult.refMap;
|
|
358
|
+
}
|
|
359
|
+
result = (result ?? '') + '\n\n' + snapResult.text;
|
|
360
|
+
} catch (e) { process.stderr.write(`[auto-snap] ${e.message}\n`); }
|
|
361
|
+
}
|
|
362
|
+
|
|
325
363
|
return { ok: true, result: result ?? '' };
|
|
326
364
|
} catch (e) {
|
|
327
365
|
auditResult.ok = false;
|
|
@@ -54,6 +54,7 @@ function tool(name, description, properties, required, annotations) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
const P_TARGET = { type: 'string', description: 'Target ID prefix from chromex_list' };
|
|
57
|
+
const P_NO_SNAP = { type: 'boolean', description: 'Skip auto-snapshot after action' };
|
|
57
58
|
|
|
58
59
|
// ---- Tool definitions (52 tools) ----
|
|
59
60
|
|
|
@@ -167,10 +168,11 @@ const TOOLS = [
|
|
|
167
168
|
|
|
168
169
|
// == NAVIGATE ==
|
|
169
170
|
tool('chromex_navigate',
|
|
170
|
-
'Navigate to URL and wait for page load.',
|
|
171
|
+
'Navigate to URL and wait for page load. Returns full snapshot with refs of the new page.',
|
|
171
172
|
{
|
|
172
173
|
target: P_TARGET,
|
|
173
174
|
url: { type: 'string', description: 'URL to navigate to' },
|
|
175
|
+
noSnap: P_NO_SNAP,
|
|
174
176
|
}, ['target', 'url'], RW),
|
|
175
177
|
|
|
176
178
|
tool('chromex_waitfor',
|
|
@@ -199,25 +201,28 @@ const TOOLS = [
|
|
|
199
201
|
|
|
200
202
|
// == INTERACT ==
|
|
201
203
|
tool('chromex_click',
|
|
202
|
-
'Click element by CSS selector or @eN ref from snapshot.',
|
|
204
|
+
'Click element by CSS selector or @eN ref from snapshot. Returns auto-snapshot with updated refs.',
|
|
203
205
|
{
|
|
204
206
|
target: P_TARGET,
|
|
205
207
|
selector: { type: 'string', description: 'CSS selector or @eN ref' },
|
|
208
|
+
noSnap: P_NO_SNAP,
|
|
206
209
|
}, ['target', 'selector'], RW),
|
|
207
210
|
|
|
208
211
|
tool('chromex_clickxy',
|
|
209
|
-
'Click at CSS pixel coordinates.',
|
|
212
|
+
'Click at CSS pixel coordinates. Returns auto-snapshot with updated refs.',
|
|
210
213
|
{
|
|
211
214
|
target: P_TARGET,
|
|
212
215
|
x: { type: 'number', description: 'X in CSS pixels' },
|
|
213
216
|
y: { type: 'number', description: 'Y in CSS pixels' },
|
|
217
|
+
noSnap: P_NO_SNAP,
|
|
214
218
|
}, ['target', 'x', 'y'], RW),
|
|
215
219
|
|
|
216
220
|
tool('chromex_type',
|
|
217
|
-
'Type text at currently focused element.',
|
|
221
|
+
'Type text at currently focused element. Returns auto-snapshot with updated refs.',
|
|
218
222
|
{
|
|
219
223
|
target: P_TARGET,
|
|
220
224
|
text: { type: 'string', description: 'Text to type' },
|
|
225
|
+
noSnap: P_NO_SNAP,
|
|
221
226
|
}, ['target', 'text'], RW),
|
|
222
227
|
|
|
223
228
|
tool('chromex_hover',
|
|
@@ -228,82 +233,92 @@ const TOOLS = [
|
|
|
228
233
|
}, ['target', 'ref'], RW),
|
|
229
234
|
|
|
230
235
|
tool('chromex_drag',
|
|
231
|
-
'Drag and drop between selectors or coordinate pairs (x1,y1 x2,y2).',
|
|
236
|
+
'Drag and drop between selectors or coordinate pairs (x1,y1 x2,y2). Returns auto-snapshot with updated refs.',
|
|
232
237
|
{
|
|
233
238
|
target: P_TARGET,
|
|
234
239
|
from: { type: 'string', description: 'Source selector or x,y' },
|
|
235
240
|
to: { type: 'string', description: 'Destination selector or x,y' },
|
|
241
|
+
noSnap: P_NO_SNAP,
|
|
236
242
|
}, ['target', 'from', 'to'], RW),
|
|
237
243
|
|
|
238
244
|
tool('chromex_touch',
|
|
239
|
-
'Touch gesture: tap, swipe, pinch, longpress.',
|
|
245
|
+
'Touch gesture: tap, swipe, pinch, longpress. Returns auto-snapshot with updated refs.',
|
|
240
246
|
{
|
|
241
247
|
target: P_TARGET,
|
|
242
248
|
gesture: { type: 'string', enum: ['tap', 'swipe', 'pinch', 'longpress'], description: 'Gesture type' },
|
|
243
249
|
args: { type: 'array', items: { type: 'string' }, description: 'Gesture args: tap(x,y), swipe(x1,y1,x2,y2), pinch(x,y,scale), longpress(x,y,[ms])' },
|
|
250
|
+
noSnap: P_NO_SNAP,
|
|
244
251
|
}, ['target', 'gesture'], RW),
|
|
245
252
|
|
|
246
253
|
tool('chromex_dialog',
|
|
247
|
-
'Handle JS dialogs (alert/confirm/prompt). Use "auto" to auto-accept all.',
|
|
254
|
+
'Handle JS dialogs (alert/confirm/prompt). Use "auto" to auto-accept all. Returns auto-snapshot with updated refs.',
|
|
248
255
|
{
|
|
249
256
|
target: P_TARGET,
|
|
250
257
|
action: { type: 'string', enum: ['accept', 'dismiss', 'auto'], description: 'Dialog action' },
|
|
251
258
|
text: { type: 'string', description: 'Text for prompt (only with accept)' },
|
|
259
|
+
noSnap: P_NO_SNAP,
|
|
252
260
|
}, ['target', 'action'], RW),
|
|
253
261
|
|
|
254
262
|
tool('chromex_loadall',
|
|
255
|
-
'Click "load more" button repeatedly until it disappears.',
|
|
263
|
+
'Click "load more" button repeatedly until it disappears. Returns auto-snapshot with updated refs.',
|
|
256
264
|
{
|
|
257
265
|
target: P_TARGET,
|
|
258
266
|
selector: { type: 'string', description: 'CSS selector of load-more button' },
|
|
259
267
|
interval: { type: 'number', description: 'Interval between clicks in ms (default: 1500)' },
|
|
268
|
+
noSnap: P_NO_SNAP,
|
|
260
269
|
}, ['target', 'selector'], RW),
|
|
261
270
|
|
|
262
271
|
// == FORMS ==
|
|
263
272
|
tool('chromex_fill',
|
|
264
|
-
'Fill input/textarea. Handles React/Vue/Angular controlled inputs. Accepts @eN ref.',
|
|
273
|
+
'Fill input/textarea. Handles React/Vue/Angular controlled inputs. Accepts @eN ref. Returns auto-snapshot with updated refs.',
|
|
265
274
|
{
|
|
266
275
|
target: P_TARGET,
|
|
267
276
|
selector: { type: 'string', description: 'CSS selector or @eN ref' },
|
|
268
277
|
value: { type: 'string', description: 'Value to fill' },
|
|
278
|
+
noSnap: P_NO_SNAP,
|
|
269
279
|
}, ['target', 'selector', 'value'], RW),
|
|
270
280
|
|
|
271
281
|
tool('chromex_clear',
|
|
272
|
-
'Clear input field.',
|
|
282
|
+
'Clear input field. Returns auto-snapshot with updated refs.',
|
|
273
283
|
{
|
|
274
284
|
target: P_TARGET,
|
|
275
285
|
selector: { type: 'string', description: 'CSS selector' },
|
|
286
|
+
noSnap: P_NO_SNAP,
|
|
276
287
|
}, ['target', 'selector'], RW),
|
|
277
288
|
|
|
278
289
|
tool('chromex_select',
|
|
279
|
-
'Select option in dropdown.',
|
|
290
|
+
'Select option in dropdown. Returns auto-snapshot with updated refs.',
|
|
280
291
|
{
|
|
281
292
|
target: P_TARGET,
|
|
282
293
|
selector: { type: 'string', description: 'CSS selector of select element' },
|
|
283
294
|
value: { type: 'string', description: 'Option value or visible text' },
|
|
295
|
+
noSnap: P_NO_SNAP,
|
|
284
296
|
}, ['target', 'selector', 'value'], RW),
|
|
285
297
|
|
|
286
298
|
tool('chromex_check',
|
|
287
|
-
'Toggle checkbox or radio button.',
|
|
299
|
+
'Toggle checkbox or radio button. Returns auto-snapshot with updated refs.',
|
|
288
300
|
{
|
|
289
301
|
target: P_TARGET,
|
|
290
302
|
selector: { type: 'string', description: 'CSS selector' },
|
|
291
303
|
checked: { type: 'boolean', description: 'Desired state (default: true)', default: true },
|
|
304
|
+
noSnap: P_NO_SNAP,
|
|
292
305
|
}, ['target', 'selector'], RW),
|
|
293
306
|
|
|
294
307
|
tool('chromex_form',
|
|
295
|
-
'Batch fill form. JSON maps selectors to values. Booleans toggle checkboxes.',
|
|
308
|
+
'Batch fill form. JSON maps selectors to values. Booleans toggle checkboxes. Returns auto-snapshot with updated refs.',
|
|
296
309
|
{
|
|
297
310
|
target: P_TARGET,
|
|
298
311
|
fields: { type: 'string', description: 'JSON: {"#email":"user@test.com","#terms":true}' },
|
|
312
|
+
noSnap: P_NO_SNAP,
|
|
299
313
|
}, ['target', 'fields'], RW),
|
|
300
314
|
|
|
301
315
|
tool('chromex_upload',
|
|
302
|
-
'Upload file(s) to input[type=file].',
|
|
316
|
+
'Upload file(s) to input[type=file]. Returns auto-snapshot with updated refs.',
|
|
303
317
|
{
|
|
304
318
|
target: P_TARGET,
|
|
305
319
|
selector: { type: 'string', description: 'CSS selector of file input' },
|
|
306
320
|
files: { type: 'array', items: { type: 'string' }, description: 'File path(s)' },
|
|
321
|
+
noSnap: P_NO_SNAP,
|
|
307
322
|
}, ['target', 'selector', 'files'], RW),
|
|
308
323
|
|
|
309
324
|
// == DATA ==
|
|
@@ -631,6 +646,8 @@ async function executeTool(name, params) {
|
|
|
631
646
|
const mapped = toolToCmd(name, params);
|
|
632
647
|
if (!mapped) return fail(`Unknown tool: ${name}`);
|
|
633
648
|
|
|
649
|
+
if (params.noSnap) mapped.args.push('--no-snap');
|
|
650
|
+
|
|
634
651
|
const conn = await getOrStartTabDaemon(targetId, config);
|
|
635
652
|
const response = await sendCommand(conn, { cmd: mapped.cmd, args: mapped.args });
|
|
636
653
|
|