cdp-toolkit 1.9.1 → 1.9.5
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/CHANGELOG.md +434 -0
- package/CONTRACT.md +59 -2
- package/README.md +68 -3
- package/dist/cli.js +2857 -114
- package/dist/index.js +934 -78
- package/dist/mcp.js +1130 -115
- package/package.json +2 -1
- package/src/backend.ts +156 -21
- package/src/bidi/client.ts +48 -10
- package/src/bidi/driver.ts +230 -33
- package/src/bidi/launch.ts +23 -13
- package/src/cdp/driver.ts +594 -9
- package/src/cli.ts +107 -10
- package/src/driver.ts +254 -3
- package/src/manifest.ts +20 -4
- package/src/mcp.ts +21 -12
- package/src/png.ts +809 -0
- package/src/shared-tools.ts +313 -11
package/README.md
CHANGED
|
@@ -119,6 +119,7 @@ bun run mcp:smoke # spawn the server + a real initialize/tools-list/tools-call
|
|
|
119
119
|
- **One target per call, never a broadcast.** Each call opens one WebSocket to the one target it named, with a bounded timeout on every CDP command, lazy domain enabling, and stateless element refs. Drive as many tabs as you like across calls; there is still no broadcast step that can stall on a wedged tab.
|
|
120
120
|
- **Network mocking: build the UI before the backend exists.** `mock_request` arms a persistent per-target fake backend: return canned responses, force errors, or inject latency/fault rates. Mocks survive reloads and navigations until `clear_mocks`.
|
|
121
121
|
- **Full chrome-devtools-mcp parity + extras.** All 29 upstream tools, plus `performance_trace` (a robust single-call trace), Lighthouse audits, heap snapshots, a cookie group that reads, writes, and deletes httpOnly cookies, real HTML5 drag-and-drop, raw scroll/mouse dispatch, download capture, permission grants, and tab-to-video screen recording. 45 single-purpose tools, no discovery overhead, and they coexist with `chrome-devtools-mcp` in a separate namespace.
|
|
122
|
+
- **A whole page is a file, not a wedged tab.** Chrome cannot encode a screenshot past 16384 device px on either side, and past that `Page.captureScreenshot` does not error — it never answers, and leaves the tab resized to the clip it was capturing. On a ratio-2 display that ceiling arrives at about 8192 CSS px: an ordinary long article. `take_screenshot` measures the projection before every capture and, past the cap, takes the page as vertical bands stitched losslessly into one PNG — a 140,982 CSS px page comes back as a 2780×281964 file in 18 bands, in 17 seconds, with the tab still healthy. Also per-capture `scale`, and `renderWidth`/`renderHeight` to shoot one capture at an emulated viewport and restore the tab afterwards.
|
|
122
123
|
- **Many agents, one browser, no stolen tabs.** `claim_page` hands out an opaque lease token for one tab; every other tool checks it at target resolution, so an unqualified call against a leased tab is refused by name rather than silently retargeted to whatever tab a different agent is driving.
|
|
123
124
|
- **Knows when a human is already using the tab you're driving.** An in-page activity beacon distinguishes a person's clicks/keys/scrolls from the toolkit's own dispatched input, so `claim_page` and `list_leases` can report `humanActiveMs` and warn on contention instead of silently fighting someone for the keyboard. See "Staleness: is a human already using this tab?" below.
|
|
124
125
|
- **Out-of-model secret handling.** A read that would return a credential, a JWT in `localStorage` via `evaluate_script` or an httpOnly session cookie via `list_cookies`, takes a `savePath` that writes the value to a file and keeps it out of the tool response entirely, so the secret never lands in the agent transcript. That same per-call, in-process design makes cdp-toolkit a clean substrate for credential-injection tools: a vaulted password can be typed straight into the DOM while only a status crosses back to the model, never the secret.
|
|
@@ -158,6 +159,8 @@ cdp clear_mocks --all true
|
|
|
158
159
|
# Run any tool by its MCP name; args come from --json and/or --key value flags.
|
|
159
160
|
cdp <tool> [--target <sel>] [--json '<obj>'] [--<key> <value> ...]
|
|
160
161
|
cdp --list # list every available tool name
|
|
162
|
+
cdp --help # top-level usage
|
|
163
|
+
cdp take_screenshot --help # that tool's arguments, from its schema — touches no browser
|
|
161
164
|
|
|
162
165
|
cdp list_pages
|
|
163
166
|
cdp navigate_page --target index:0 --url https://example.com
|
|
@@ -176,11 +179,21 @@ cdp set_cookie --json '{"name":"session","value":"abc","url":"https://example.co
|
|
|
176
179
|
# Remove it again. Either url or domain is required on both write tools.
|
|
177
180
|
cdp delete_cookies --json '{"name":"session","url":"https://example.com/"}'
|
|
178
181
|
cdp take_screenshot --target url:example --fullPage true
|
|
182
|
+
# A whole page, however long: past Chrome's 16384-device-px encode limit the capture is
|
|
183
|
+
# taken as vertical bands and stitched into one lossless PNG. No flag needed — this is the
|
|
184
|
+
# default — and the result says so: {"width":2780,"height":281964,"tiled":true,"bands":18}.
|
|
185
|
+
cdp take_screenshot --target url:example --fullPage true --savePath /tmp/whole-page.png
|
|
186
|
+
# 3x output pixels for one capture (Chrome only). The page is not told anything changed.
|
|
187
|
+
cdp take_screenshot --target url:example --scale 3
|
|
188
|
+
# Shoot a responsive page at desktop width from a tab that isn't that size, then restore it.
|
|
189
|
+
cdp take_screenshot --target url:example --renderWidth 1920 --renderHeight 1080
|
|
179
190
|
cdp lighthouse_audit --url https://example.com --json '{"categories":["performance"]}'
|
|
180
191
|
```
|
|
181
192
|
|
|
182
193
|
**Argument parsing:** the first positional token is the tool name. `--json '<obj>'` merges a JSON object into the args (applied first). `--target <sel>` sets `args.target`. Repeated `--key value` pairs become `args.key`, coerced (`true`/`false` → boolean, numeric strings → number, else string); a bare `--flag` is `true`. Explicit flags override keys from `--json`. Output is `JSON.stringify(result, null, 2)` on stdout (exit 0); on any throw, `{"error":"<message>"}` goes to stderr and the process exits 1.
|
|
183
194
|
|
|
195
|
+
**`--help`/`-h`** are recognized anywhere in argv, ahead of every other flag, and are never treated as a tool argument or a tool name — as of 1.9.3, `<tool> --help` used to run the tool instead. With no tool named, `cdp --help` prints the usage above. With a tool named, `cdp <tool> --help` prints that tool's arguments (name, type, required/optional, description) read from its schema, and exits 0 having made no CDP connection, taken no lease, and written no file.
|
|
196
|
+
|
|
184
197
|
### Programmatic use
|
|
185
198
|
|
|
186
199
|
```ts
|
|
@@ -214,11 +227,63 @@ cdp --capabilities --browser firefox # see what's available and
|
|
|
214
227
|
|
|
215
228
|
Backend selection precedence: `--browser chrome|firefox` flag, then `CDP_BROWSER`, then `chrome`. For the MCP server, set `CDP_BROWSER=firefox` (or pass `--browser firefox` in its launch args) in your MCP client config; the backend is fixed for the life of that server process.
|
|
216
229
|
|
|
217
|
-
|
|
230
|
+
Firefox runs in one of **two modes**, and the difference between them is process ownership.
|
|
231
|
+
|
|
232
|
+
**LAUNCH (default): a fresh throwaway-profile Firefox, launched and killed per session.** `--browser firefox` with no `--connect`/`CDP_FIREFOX_ENDPOINT` starts a brand-new Firefox process with an empty profile — a login wall for anything that needs a real, already-authenticated session:
|
|
218
233
|
|
|
219
234
|
- **CLI** (one process per invocation): each command launches Firefox, runs exactly one tool call, and disposes the session and kills the process before exiting, win or lose. State does not carry between separate CLI invocations: there is no running Firefox left afterward for a second command to find.
|
|
220
235
|
- **MCP server** (long-lived): the first Firefox tool call launches one Firefox process and memoizes its BiDi session for the life of the server; every later Firefox call reuses it. The session is torn down on `SIGINT`/`SIGTERM`/stdin close. Multi-step Firefox workflows (navigate, then snapshot, then click) need the MCP server, not the CLI, for exactly this reason.
|
|
221
236
|
|
|
237
|
+
**ATTACH (`--connect <port|host:port|ws-url>` / `CDP_FIREFOX_ENDPOINT`): connect to a Firefox YOU already started.** Launch a Firefox with its debug port open yourself, and cdp-toolkit connects to that process's BiDi endpoint instead of spawning a throwaway one — so tools see your real, logged-in profile, cookies and all. This process never launches or kills that Firefox: dispose only ends the BiDi session (`session.end`), never the browser.
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
# 1. Start YOUR Firefox with the debug port open (a separate, empty --no-remote profile
|
|
241
|
+
# is recommended so it doesn't collide with a Firefox you already have open; drop
|
|
242
|
+
# -profile/-no-remote to attach to your everyday profile instead, once nothing else
|
|
243
|
+
# is holding its BiDi session):
|
|
244
|
+
firefox --remote-debugging-port 9223 --no-remote -profile /tmp/ff-attach-profile &
|
|
245
|
+
|
|
246
|
+
# 2. CLI: --connect implies --browser firefox, so it doesn't need to be passed too
|
|
247
|
+
bun run src/cli.ts --connect 9223 take_snapshot
|
|
248
|
+
cdp --connect 127.0.0.1:9223 take_snapshot # host:port also works
|
|
249
|
+
cdp --connect ws://127.0.0.1:9223/session take_snapshot # or the full ws:// URL
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
3. MCP client config: set the env var instead of a flag (the endpoint doesn't belong in `args`):
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"mcpServers": {
|
|
257
|
+
"cdp-toolkit": {
|
|
258
|
+
"command": "npx",
|
|
259
|
+
"args": ["-y", "cdp-toolkit"],
|
|
260
|
+
"env": { "CDP_BROWSER": "firefox", "CDP_FIREFOX_ENDPOINT": "9223" }
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`<endpoint>` accepts three spellings, all normalized to a ws URL: a bare port (`9223`), `host:port` (`127.0.0.1:9223`), or a full `ws://`/`wss://` URL. `--connect`/`CDP_FIREFOX_ENDPOINT` implies the Firefox backend on its own and errors against an explicit `--browser chrome`.
|
|
267
|
+
|
|
268
|
+
**On Linux, and over SSH to a remote host.** Attach is a plain loopback WebSocket, so it is byte-identical on every OS; only the *launch* half is platform-specific. On Linux the binary is `firefox` on `PATH` (or `/usr/bin/firefox`), and the same `--remote-debugging-port 9223` opens the endpoint. One catch worth stating plainly: if your everyday Firefox is already running, relaunching it with the flag just focuses the open window and opens no port (see "Attaching is not relaunching" below). To attach to your real logged-in profile, quit Firefox first and reopen it with the flag; to run beside your daily browser instead, use the separate `--no-remote -profile` instance shown above.
|
|
269
|
+
|
|
270
|
+
When Firefox runs on a different host than cdp-toolkit (a remote box, a container), its debug port binds to loopback *there*, so forward the port and attach to localhost:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
ssh -L 9223:127.0.0.1:9223 you@the-box # forward the remote debug port
|
|
274
|
+
cdp --connect 9223 take_snapshot # then attach as if it were local
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
For Claude Code, register the MCP server with the endpoint in one line (the env var implies the Firefox backend):
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
claude mcp add cdp-toolkit -e CDP_FIREFOX_ENDPOINT=9223 -- npx -y cdp-toolkit
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Firefox allows only one active WebDriver BiDi session at a time.** A second `session.new` against the same endpoint fails outright while a first session is open. Disposing cleanly (the normal exit path, both CLI and MCP shutdown) sends `session.end` first, which frees the slot for the next attach. A client that dies **without** disposing — a `SIGKILL`, a crash — leaves the slot occupied: Firefox does not reap an abandoned session on its own. A colliding attach against an occupied slot now fails fast with an actionable error naming the endpoint, rather than hanging forever (fixed in 1.9.4); recover by closing the other client or restarting Firefox.
|
|
284
|
+
|
|
285
|
+
**Attaching is not relaunching.** The one thing that is genuinely impossible is handing a debug port to an *already-running* Firefox process after the fact: the `--remote-debugging-port` flag only takes effect on a process's original launch, so relaunching the `firefox` binary against a running instance hands off to it and exits silently, opening no port (verified against Firefox 153.0.3). That is a real, narrow limitation of the Firefox binary itself. It is not the same claim as "Firefox cannot be attached to" — a Firefox that was launched *with* the debug port open, whether by this toolkit or by your own hand, exposes a plain BiDi endpoint that any number of fresh clients can connect to later, which is exactly what `--connect`/`CDP_FIREFOX_ENDPOINT` does.
|
|
286
|
+
|
|
222
287
|
**Tool availability is filtered per backend**, not per call: `tools/list` (MCP) and `--list`/`--capabilities` (CLI) only ever advertise a tool the selected browser can actually run. A tool is never listed and then thrown from at call time. Under Firefox, four tool groups are absent because Firefox 153's BiDi implementation has no equivalent domain:
|
|
223
288
|
|
|
224
289
|
- `performance_start_trace`, `performance_stop_trace`, `performance_analyze_insight`, `performance_trace` (needs `trace.performance`)
|
|
@@ -237,9 +302,9 @@ Everything else, including `mock_request`/`list_mocks`/`clear_mocks` (Firefox's
|
|
|
237
302
|
- **Thin emulation.** Only viewport size/DPR and `userAgent` are applied. CPU throttling, media-feature emulation (e.g. `prefers-color-scheme`), and network-condition throttling are not available: Firefox 153 does not implement the underlying BiDi commands.
|
|
238
303
|
- **No tracing, heap snapshots, or Lighthouse.** See the capability list above; there is no BiDi equivalent for any of the three.
|
|
239
304
|
- **`locate.text` is not available** (Firefox 153's `browsingContext.locateNodes` rejects the `innerText` locator type as unsupported), unlike Chrome, which has it via `DOM.performSearch`.
|
|
240
|
-
- **No `--browser firefox` attach mode.** By design, per the launch model above: every invocation gets its own throwaway Firefox process and profile.
|
|
241
305
|
- **No modifier-key clicks.** `click`'s `modifiers` (Alt/Control/Meta/Shift) is a Chrome-only *parameter* on an otherwise-universal tool: a non-empty `modifiers` array throws under `--browser firefox` rather than being silently dropped. A plain click still works on both backends.
|
|
242
306
|
- **No real HTML5 drag-and-drop mode.** `drag`'s `mode:"html5"` requires capability `input.html5Drag` and is rejected with a clear error under Firefox; `mode:"mouse"` (the default) works on both.
|
|
307
|
+
- **No per-capture screenshot `scale`; band tiling is a Chrome-only workaround Firefox has no use for.** Two *parameters* on an otherwise-universal tool, for two different reasons. `scale` (`screenshot.scale`) is a genuine protocol gap: `browsingContext.captureScreenshot` has no scale parameter at all, and the refusal points at `emulate {deviceScaleFactor}` + a scale-1 capture instead — Firefox captures are always 1x. `tile:true` (`screenshot.tile`) is **not** a Firefox gap: measured against Firefox 153.0.3, `take_screenshot --fullPage true` on a 20,000px-tall page returned one 1366×20000 PNG at scale 1 from a single BiDi `captureScreenshot(origin:"document")` call — verified complete top-to-bottom (a marker at y=0, a marker at y=20000, an unbroken ruler through the middle, no truncation). Chrome's banding exists solely to route around its 16384-device-px encode cap; Firefox has no such cap, so there is nothing for tiling to work around, and auto-tiling correctly never fires there — a Firefox `fullPage` capture is already the whole page in one shot, however long. **`renderWidth`/`renderHeight` (`screenshot.renderSize`) are available on both backends** — that one is not a gap.
|
|
243
308
|
|
|
244
309
|
## The tools (29 parity + 16 superset = 45)
|
|
245
310
|
|
|
@@ -268,7 +333,7 @@ The 29 parity tools are 1:1 with `chrome-devtools-mcp`; the 16 superset tools (`
|
|
|
268
333
|
| `type_text` | `Input.insertText` | Appends (does not clear first); insertText, not per-key. |
|
|
269
334
|
| `press_key` | `Input.dispatchKeyEvent` | Curated named-key table + single chars; not the full Puppeteer KeyInput enum. |
|
|
270
335
|
| `upload_file` | `DOM.setFileInputFiles` | Requires a resolvable `<input type=file>` (uid or selector). |
|
|
271
|
-
| `take_screenshot` | `Page.captureScreenshot` (+
|
|
336
|
+
| `take_screenshot` | `Page.captureScreenshot` (+ `Page.getLayoutMetrics` on **every** capture) | Full-page uses `captureBeyondViewport` + a layout-metrics clip. `scale` (>0, ≤8, **Chrome-only**) multiplies output pixels for one capture: output px = `ceil(css × scale × devicePixelRatio)`, and the page is never told (`devicePixelRatio`/`innerWidth` are unmoved). `renderWidth`+`renderHeight` (**both backends**, required together) emulate a viewport for one capture and restore it after — media queries flip, so this is how you shoot a responsive page at 1920×1080 from a tab that isn't. Chrome cannot encode past **16384 device px per side** and does not refuse politely there, so past the cap the capture is taken as vertical bands and stitched losslessly into one PNG (`tile`, auto by default; `tiled`/`bands` on the result): a 140,982 CSS px page returns 2780×281964 px in 18 bands instead of hanging and wedging the tab. Banding is vertical only (an over-wide projection is refused, not split), PNG-only, and never with `returnBase64`; content the page loads only on real scroll renders blank past the first viewport. `width`/`height` are decoded from the encoded bytes and omitted when undecodable. |
|
|
272
337
|
| `start_screen_recording` *(superset)* | `Page.startScreencast` / `Page.screencastFrame` / `Page.screencastFrameAck` | **Toolkit addition; chrome-devtools-mcp has no screen-recording tool.** Opens a persistent per-target connection and spools frames to a ledger on disk; pairs with `stop_screen_recording` in the SAME process, for the same cross-process reason as `performance_start_trace`. ffmpeg is probed here so a missing encoder fails before a recording is captured. Chrome only: absent from `tools/list` under `--browser firefox` (needs `capture.screencast`, which BiDi has no primitive for). |
|
|
273
338
|
| `stop_screen_recording` *(superset)* | `Page.stopScreencast` (+ ffmpeg encode) | Assembles the spooled frames into an H.265 (`hevc_videotoolbox`, falling back to `h264_videotoolbox` → `libx265` → `libx264`) MP4 using per-frame durations from the capture ledger, coalesced onto ffmpeg's 40ms concat-demuxer grid. Returns `{path,bytes,durationMs,frameCount,encodedFrames,codec,encoder,width,height,droppedFrames,target}` (`frameCount` captured vs. `encodedFrames` in the video). On an ffmpeg failure the spool is kept and the exact re-run command is named in the error. |
|
|
274
339
|
| `emulate` | `Emulation.*` / `Network.emulateNetworkConditions` | Stateless: UA/CPU/media/network overrides reset when the per-call connection closes. No named device presets. |
|