cdp-toolkit 1.10.0 → 2.0.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/CHANGELOG.md +60 -0
- package/README.md +61 -12
- package/dist/cli.js +1233 -312
- package/dist/index.js +11 -11
- package/dist/mcp.js +1314 -320
- package/package.json +4 -2
- package/src/bidi/driver.ts +219 -29
- package/src/bidi/launch.ts +51 -1
- package/src/bidi/marionette.ts +276 -0
- package/src/bidi/session-lease.ts +265 -0
- package/src/cli.ts +10 -0
- package/src/install/browser.ts +211 -0
- package/src/install/harness.ts +224 -0
- package/src/install/wizard.ts +412 -0
- package/src/manifest.ts +297 -297
- package/src/mcp.ts +123 -11
- package/src/toolDocs.ts +468 -0
- package/src/toolGroups.ts +49 -0
- package/src/tools/navigation.ts +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,66 @@ All notable changes to cdp-toolkit are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.0.0] - 2026-08-29
|
|
9
|
+
|
|
10
|
+
**Progressive disclosure: the default `tools/list` an agent loads shrinks ~88% (≈20,200 → ≈2,500 tokens) to 12 core tools plus two meta-tools, with the full 45-tool surface, and the full per-tool documentation, one `browser_tools` call away.** This is the first npm publication since 1.11.0, so it also **carries the [1.11.1] changes below** (the two Firefox BiDi navigation fixes) that landed on main but were never released. Tool names, parameters, and schema shapes are unchanged — a strip-descriptions deep-equal of the manifest gates the change, the same check 1.10.0 used.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Tool profiles + on-demand group activation.** The server now starts on the `core` profile (`list_pages`, `new_page`, `close_page`, `select_page`, `navigate_page`, `wait_for`, `take_snapshot`, `click`, `fill`, `type_text`, `evaluate_script`, `take_screenshot`) and holds the other 33 tools in 12 activatable groups (`src/toolGroups.ts`). `CDP_TOOL_PROFILE=full` advertises everything at startup — the exact prior behavior. Profiles are MCP-only; the CLI still exposes all 45 tools.
|
|
15
|
+
- **`browser_tools` meta-tool** — `list_groups` (every group, its tools, active state), `activate`/`deactivate` (toggle one group), `set_profile` (core|full wholesale). Any change fires `notifications/tools/list_changed` so the client re-fetches `tools/list`; the server now declares `tools.listChanged: true`.
|
|
16
|
+
- **`describe_tool` meta-tool** — full description and per-parameter docs for any tool by name, *including tools not currently listed* (`src/toolDocs.ts`, generated verbatim from the pre-2.0 manifest prose). Call it before using an unfamiliar tool; the live listing carries only terse summaries.
|
|
17
|
+
- Environment knob: `CDP_TOOL_PROFILE` (`core` default, `full` = prior behavior).
|
|
18
|
+
- The server `instructions` block now front-loads the disclosure model: core profile at startup, `browser_tools` to activate, `describe_tool` for full docs, hidden tools still callable by name.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **The manifest advertised over `tools/list` now carries terse one-line descriptions; the full prose moved to `describe_tool`.** Description text drops 61,002 → 14,077 chars; the full 45-tool payload would be ~8.5k tokens (−58% vs 1.x) and the default surface ~2.5k (−88%). Schema shapes are untouched. Hidden does not mean disabled: a hidden-but-available tool called by name still executes — only discovery is gated.
|
|
23
|
+
- **MCP SDK 1.29 → 2.0.** The runtime dependency is now `@modelcontextprotocol/server@^2.0.0` (the SDK 2.0 package split), with `@modelcontextprotocol/client@^2.0.0` as a devDependency powering the in-process client tests; `scripts/build.ts` keeps the SDK external under the new package name.
|
|
24
|
+
- **Tests re-pointed, guarantees preserved.** Every description assertion in `test/` now reads `TOOL_DOCS` instead of the manifest (worker-capture traps, HTML5 drag modes, lease semantics, the 500ms probe bound — all still asserted, relocated), and `test/origins.test.ts` now proves the "origin is never 'human'" vocabulary is reachable end-to-end through `describe_tool` over a real stdio MCP connection.
|
|
25
|
+
- Repo hygiene: the private `.cbm-atlas`/`.cbmignore` indexing artifacts and one internal design plan doc are no longer tracked.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Seven one-line manifest descriptions that a mechanical compression pass had corrupted with copy-paste text from unrelated tools (`fill.value`, `fill_form.fields[].value`, `emulate.mediaFeatures` item name/value, `mock_request.action`, `lighthouse_audit.categories`, `clear_mocks.all`, `list_mocks.target`) now carry their correct terse wording, derived from the preserved `TOOL_DOCS` prose.
|
|
30
|
+
- The runtime version constant (`src/mcp.ts` `VERSION`) had drifted from `package.json` since 1.11.0; both now read from the same release.
|
|
31
|
+
|
|
32
|
+
## [1.11.1] - 2026-08-25
|
|
33
|
+
|
|
34
|
+
**Two Firefox BiDi navigation fixes: every plain `reload:true` no longer fails, and `file://` navigation gets automation-pref parity plus an actionable error when the OS still refuses.** Chrome behavior is unchanged. The tool surface is unchanged (45 tools).
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **Every plain `reload:true` on Firefox failed with `Argument "ignoreCache" is not supported yet`.** `browsingContext.reload` was sent `ignoreCache: false` unconditionally, and Firefox's BiDi server rejects the KEY regardless of value — so the default reload path, not just hard reloads, was broken on the Firefox backend. The key is now omitted from the params entirely on plain reloads, and an explicit `ignoreCache: true` fails fast with a clear backend-capability error (the same doctrine as `click`'s modifiers) instead of an opaque mid-flight protocol rejection. The `navigate_page` manifest and docs now state the Firefox limitation up front.
|
|
39
|
+
- **#6 — `file://` navigation on the Firefox backend.** Two parts, with their evidentiary limits stated:
|
|
40
|
+
- Launched-Firefox profiles now carry Mozilla's automation prefs. Firefox's Remote Agent has its own recommended automation bundle, but it is gated on `remote.prefs.recommended`, which DEFAULTS TO FALSE (verified in mozilla-central's `remote/shared/RecommendedPreferences.sys.mjs`) — so a bare `--remote-debugging-port` launch ran with consumer prefs while every geckodriver/Marionette-class setup applies the bundle. `launch.ts` now seeds `user.js` into every throwaway profile with `remote.prefs.recommended: true` plus `security.fileuri.strict_origin_policy: false` and `dom.file.createInChild: true` (the latter is the pref Marionette's own `geckoinstance.py` sets). An explicit `profilePath` is deliberately NOT mutated — it belongs to its owner.
|
|
41
|
+
- `NS_ERROR_FILE_ACCESS_DENIED` now maps to an actionable error naming snap/AppArmor confinement (an Ubuntu snap Firefox cannot read host paths outside its mount namespace, and no preference fixes that) and the localhost-server workaround, instead of relaying the bare nsresult.
|
|
42
|
+
- Live-verified on macOS (Firefox 153, launch mode): top-level `file://` navigation, sibling `<script src>` loading, and same-directory `fetch()` all pass end-to-end (`test/firefox-smoke.ts`, 24/24). The Linux snap-confined case cannot be reproduced on macOS and is covered by the error mapping instead; a negative control confirmed sibling-script/fetch behavior does not discriminate the seeded prefs on Firefox 153/macOS, so those smoke checks are regression guards, not causal proof.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **`test/firefox-smoke.ts`** gains the file:// + reload section (check 7b2) with its evidentiary limits documented inline, and no longer claims to avoid `file://` categorically.
|
|
47
|
+
|
|
48
|
+
## [1.11.0] - 2026-08-18
|
|
49
|
+
|
|
50
|
+
**Firefox multi-process session coordination + orphan auto-recovery, and a new `cdp install` one-command setup.** Attaching several cdp-toolkit processes to one user Firefox no longer hard-wedges on Firefox's single-BiDi-session limit, and a session orphaned by a killed client is now auto-cleared over the Marionette side channel instead of requiring a Firefox restart. Chrome behavior is unchanged. The tool surface is still 45 tools — the installer is a CLI subcommand, not a new tool.
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- **`cdp install`** — an interactive installer that registers the MCP server into a harness you pick (Claude Code, Codex, or opencode), asks for a debug port and a browser (Arc, Chrome, or Firefox), and appends an idempotent shell alias that launches that browser with its remote-debugging port open. The Firefox alias includes `--marionette` (required for orphan auto-recovery, below). Runs non-interactively too: `--harness`/`--browser`/`--port`/`--name`/`--no-alias`/`--yes`. It is a CLI-only subcommand; `cdp --list` / `tools/list` still advertise exactly 45 tools.
|
|
55
|
+
- **Cross-process Firefox session coordination** (`src/bidi/session-lease.ts`). A file-based lease — a sibling of the existing tab-lease group — serializes Firefox's single WebDriver BiDi session slot across processes attaching to the same endpoint. When the slot is held by a genuinely live process, a second process now waits up to `CDP_FIREFOX_SESSION_WAIT_MS` (default `10000`) and then returns a fast, distinguishable error naming the live holder, instead of hanging or emitting the raw "Maximum number of active sessions" wedge. (#5)
|
|
56
|
+
- **Firefox orphan-session auto-recovery** (`src/bidi/marionette.ts`). A session stranded by a client that died without `session.end` — the classic wedge Firefox never reaps — is force-cleared over the Marionette side channel (a blind `WebDriver:DeleteSession` on `CDP_FIREFOX_MARIONETTE_PORT`, default `2828`) without killing or restarting Firefox, then the connection is retried. Requires the attached Firefox to have been launched with `--marionette`; when it wasn't, the toolkit degrades to a clear, actionable error rather than a permanent dead-end. (#4)
|
|
57
|
+
- Environment knobs: `CDP_FIREFOX_MARIONETTE_PORT` (default `2828`) and `CDP_FIREFOX_SESSION_WAIT_MS` (default `10000`).
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- **#4 — an orphaned Firefox BiDi session no longer permanently wedges the next server process.** A client killed without `session.end` left Firefox's single session slot occupied (Firefox does not reap it), so every subsequent process's first `session.new` failed with no recovery short of restarting Firefox. cdp-toolkit now detects the dead holder, force-clears the orphan over Marionette, and retries — verified end-to-end against Firefox 153 (the browser stays the same pid, never restarted).
|
|
62
|
+
- **#5 — two concurrent live processes contending for Firefox's one BiDi session no longer both hard-wedge identically.** The loser now gets a fast, distinguishable, retryable signal naming the live holder, instead of an ambiguous, unrecoverable error indistinguishable from the orphan case.
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- **README:** Firefox is documented as a supported backend (only WebKit/Safari now routes to `playwright-mcp`); the attach examples add `--marionette`; the "many agents, one browser" section states Firefox's one-session-per-browser ceiling explicitly; the `cdp install` on-ramp and the two new env knobs are documented; the stale "restart Firefox to recover" guidance is replaced by the new coordinate-and-recover behavior.
|
|
67
|
+
|
|
8
68
|
## [1.10.0] - 2026-08-18
|
|
9
69
|
|
|
10
70
|
**MCP context-rot reduction: the `tools/list` payload every consuming agent loads is cut ~24% (≈26,600 → ≈20,150 tokens), with NO change to tool behavior, names, parameters, or schema shapes** — the trim touched `description` strings only and was proven shape-identical to the prior manifest (a strip-descriptions deep-equal check gates the regeneration).
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# cdp-toolkit
|
|
2
2
|
|
|
3
|
-
**A lightweight, drop-in alternative to [`chrome-devtools-mcp`](https://github.com/ChromeDevTools/chrome-devtools-mcp) that won't wedge your agent.** It drives the Chrome tabs you point it at over the raw DevTools Protocol: any number of tabs, one explicitly named target per call over one direct socket, with a bounded timeout on every call, so a stuck page returns a clean error instead of hanging your agent and forcing a `/mcp` restart. Same idea, no all-target fan-out, plus tab leases so several agents can work one browser (and know when a human is using it too), plus a built-in network-mocking fake backend. **45 tools
|
|
3
|
+
**A lightweight, drop-in alternative to [`chrome-devtools-mcp`](https://github.com/ChromeDevTools/chrome-devtools-mcp) that won't wedge your agent.** It drives the Chrome tabs you point it at over the raw DevTools Protocol: any number of tabs, one explicitly named target per call over one direct socket, with a bounded timeout on every call, so a stuck page returns a clean error instead of hanging your agent and forcing a `/mcp` restart. Same idea, no all-target fan-out, plus tab leases so several agents can work one browser (and know when a human is using it too), plus a built-in network-mocking fake backend. **45 tools — 12 of them on the MCP server's default listing**, the rest revealed by one `browser_tools` call (see [Progressive disclosure](#progressive-disclosure)). Chrome is the flagship default; a second backend drives Firefox over WebDriver BiDi behind the same tool surface (see below).
|
|
4
4
|
|
|
5
5
|
> For AI-agent developers and Claude Code / Cursor users who need **the tabs they name driven reliably**, not a Puppeteer-managed browser.
|
|
6
6
|
|
|
@@ -49,12 +49,22 @@ If [`chrome-devtools-mcp`](https://github.com/ChromeDevTools/chrome-devtools-mcp
|
|
|
49
49
|
- run **multiple agents against one Chrome** and need them to stop stealing each other's tabs.
|
|
50
50
|
|
|
51
51
|
**Probably not, if you:**
|
|
52
|
-
- need
|
|
52
|
+
- need **WebKit / Safari**, or a browser that is neither Chromium nor Firefox — use [`playwright-mcp`](https://github.com/microsoft/playwright-mcp). Firefox *is* supported here, as a first-class backend (see [Firefox (WebDriver BiDi)](#firefox-webdriver-bidi));
|
|
53
53
|
- need Puppeteer's auto-wait/retry envelope for an unknown, changing page;
|
|
54
54
|
- want one server to fan out across *all* your open tabs at once.
|
|
55
55
|
|
|
56
56
|
## Quickstart (about 30 seconds)
|
|
57
57
|
|
|
58
|
+
**Fastest path — the interactive installer.** `cdp install` registers the MCP server into a harness you pick and appends a shell alias that launches your browser with its debug port open:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx -y --package cdp-toolkit cdp install # or `cdp install` from a clone / after a global install
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
It asks for a harness (`claude` | `codex` | `opencode`), a debug port, and a browser (`arc` | `chrome` | `firefox`), then writes an idempotent marker block into your `~/.zshrc`/`~/.bashrc`. The same flags drive it non-interactively: `--harness --browser --port --name --no-alias --yes`. The Firefox alias it writes includes `--marionette` (required for cdp-toolkit's orphaned-session auto-recovery — see [Firefox](#firefox-webdriver-bidi)). It is a CLI subcommand, **not** a 46th tool: `cdp --list` still shows 45.
|
|
65
|
+
|
|
66
|
+
Prefer to wire it by hand? The manual path is three steps:
|
|
67
|
+
|
|
58
68
|
```bash
|
|
59
69
|
# 1. Start Chrome/Chromium with the DevTools port open
|
|
60
70
|
open -a "Google Chrome" --args --remote-debugging-port=9222
|
|
@@ -84,6 +94,7 @@ npx -y --package cdp-toolkit cdp navigate_page --target index:0 --url https://ex
|
|
|
84
94
|
<details>
|
|
85
95
|
<summary><b>Claude Code</b></summary>
|
|
86
96
|
|
|
97
|
+
Either let `cdp install` register it for you (pick `claude` at the harness prompt), or wire it by hand:
|
|
87
98
|
```bash
|
|
88
99
|
claude mcp add cdp-toolkit --scope user -- npx -y cdp-toolkit # or: bunx -y cdp-toolkit
|
|
89
100
|
claude mcp get cdp-toolkit # status (should show ✓ Connected)
|
|
@@ -118,7 +129,7 @@ bun run mcp:smoke # spawn the server + a real initialize/tools-list/tools-call
|
|
|
118
129
|
|
|
119
130
|
- **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
131
|
- **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
|
-
- **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.
|
|
132
|
+
- **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. The MCP server lists a lean 12-tool core by default and activates the rest per group via `browser_tools`, so the discovery payload stays small; the CLI exposes all 45. They coexist with `chrome-devtools-mcp` in a separate namespace.
|
|
122
133
|
- **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.
|
|
123
134
|
- **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.
|
|
124
135
|
- **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.
|
|
@@ -213,11 +224,36 @@ await TOOLS.navigate_page({ target: "index:0", url: "https://example.com" });
|
|
|
213
224
|
| `CDP_STATE_DIR` | `/tmp/cdp-toolkit` | `select_page` selected-target file, in-flight trace state. |
|
|
214
225
|
| `CDP_LEASE_TTL_MS` | `900000` | How long a tab lease survives without use before another agent can reclaim it. Refreshed on every checked call. |
|
|
215
226
|
| `CDP_REQUIRE_LEASE` | off | Strict mode, MCP server only (inert under the CLI regardless of value). Turns leasing from optional into mandatory: a call against an unheld tab acquires a lease instead of driving it lease-free, and `list_pages`/`list_leases` close tabs an abandoned agent left behind. See "Parallel tabs" below. |
|
|
227
|
+
| `CDP_TOOL_PROFILE` | `core` | MCP server only. Startup tool profile: `core` lists the 12 everyday tools plus the `browser_tools`/`describe_tool` meta-tools; `full` lists everything at startup (exact 1.x behavior). Groups toggle at runtime via `browser_tools` either way. See "Progressive disclosure" below. |
|
|
216
228
|
| `CDP_REAP_GRACE_MS` | `2700000` | Extra grace (on top of `CDP_LEASE_TTL_MS`) before an `expired` lease's tab is actually destroyed by reap; `dead-pid` tabs are reaped immediately regardless. See "Reap" below. |
|
|
229
|
+
| `CDP_FIREFOX_MARIONETTE_PORT` | `2828` | Firefox backend only. The Marionette side-channel port used to force-clear Firefox's orphaned BiDi session during orphan-session recovery (a blind `WebDriver:DeleteSession`). Only effective when that Firefox was launched with `--marionette`. See "Firefox" below. |
|
|
230
|
+
| `CDP_FIREFOX_SESSION_WAIT_MS` | `10000` | Firefox backend only. How long a second process waits for a live holder to release Firefox's one BiDi session before returning the distinguishable "held by a live process" error. See "Firefox" below. |
|
|
231
|
+
|
|
232
|
+
## Progressive disclosure
|
|
233
|
+
|
|
234
|
+
By default the MCP server starts on the `core` profile: 12 everyday tools (`list_pages`, `new_page`, `close_page`, `select_page`, `navigate_page`, `wait_for`, `take_snapshot`, `click`, `fill`, `type_text`, `evaluate_script`, `take_screenshot`) plus two always-listed meta-tools, `browser_tools` and `describe_tool`. The other 33 tools stay hidden inside 12 activatable groups (`input`, `cookies`, `network`, `console`, `mocking`, `emulation`, `performance`, `recording`, `leases`, `permissions`, `dialogs`, `downloads`) until one `browser_tools` call reveals them. `CDP_TOOL_PROFILE=full` in the server's env restores 1.x behavior: everything advertised at startup.
|
|
235
|
+
|
|
236
|
+
The default listing costs under 2.5k tokens and the full profile about 8.5k, down from about 20.2k in 1.x (−88% and −58%). Tool names, parameters, and schema shapes are unchanged; only what a client sees at discovery time moves.
|
|
237
|
+
|
|
238
|
+
`browser_tools` drives the disclosure; `describe_tool` serves the full docs:
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{"tool": "browser_tools", "arguments": {"action": "list_groups"}}
|
|
242
|
+
{"tool": "browser_tools", "arguments": {"action": "activate", "group": "mocking"}}
|
|
243
|
+
{"tool": "describe_tool", "arguments": {"name": "wait_for_download"}}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Every `activate`/`deactivate`/`set_profile` that changes the listing fires `notifications/tools/list_changed` (the server declares `listChanged: true`), so a compliant client re-fetches `tools/list` and sees the new surface. Clients that cache their tool list should honor that notification rather than keep serving the stale one.
|
|
247
|
+
|
|
248
|
+
Three rules worth knowing:
|
|
249
|
+
|
|
250
|
+
- **Hidden does not mean blocked.** Only discovery is gated. `tools/call` checks backend availability, not group membership, so an agent that names a hidden-but-available tool still executes it; only a tool the selected browser cannot run at all is refused by name.
|
|
251
|
+
- **`describe_tool` works for any tool by name, hidden ones included** — the full description and per-parameter docs behind the terse one-liner `tools/list` now carries.
|
|
252
|
+
- **Meta-tools are MCP-only.** Profiles are an MCP-server concept: `cdp browser_tools` fails with `unknown tool`, and the CLI keeps exposing all 45 tools no matter how `CDP_TOOL_PROFILE` is set.
|
|
217
253
|
|
|
218
254
|
## Firefox (WebDriver BiDi)
|
|
219
255
|
|
|
220
|
-
cdp-toolkit ships a second backend, Firefox over [WebDriver BiDi](https://w3c.github.io/webdriver-bidi/), behind the same
|
|
256
|
+
cdp-toolkit ships a second backend, Firefox over [WebDriver BiDi](https://w3c.github.io/webdriver-bidi/), behind the same tool surface. Chrome stays the default and its behavior is unchanged, opt in explicitly to reach Firefox:
|
|
221
257
|
|
|
222
258
|
```bash
|
|
223
259
|
cdp --browser firefox take_snapshot # CLI: explicit flag
|
|
@@ -240,8 +276,10 @@ Firefox runs in one of **two modes**, and the difference between them is process
|
|
|
240
276
|
# 1. Start YOUR Firefox with the debug port open (a separate, empty --no-remote profile
|
|
241
277
|
# is recommended so it doesn't collide with a Firefox you already have open; drop
|
|
242
278
|
# -profile/-no-remote to attach to your everyday profile instead, once nothing else
|
|
243
|
-
# is holding its BiDi session)
|
|
244
|
-
|
|
279
|
+
# is holding its BiDi session). --marionette enables cdp-toolkit's orphaned-session
|
|
280
|
+
# auto-recovery over the Marionette side channel; without it, a killed client's
|
|
281
|
+
# wedged session can only be cleared by restarting Firefox (see "one session" below):
|
|
282
|
+
firefox --remote-debugging-port 9223 --marionette --no-remote -profile /tmp/ff-attach-profile &
|
|
245
283
|
|
|
246
284
|
# 2. CLI: --connect implies --browser firefox, so it doesn't need to be passed too
|
|
247
285
|
bun run src/cli.ts --connect 9223 take_snapshot
|
|
@@ -265,7 +303,7 @@ cdp --connect ws://127.0.0.1:9223/session take_snapshot # or the full ws:// UR
|
|
|
265
303
|
|
|
266
304
|
`<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
305
|
|
|
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.
|
|
306
|
+
**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 --marionette` opens the endpoint (keep `--marionette` for the orphan-recovery reason above). 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
307
|
|
|
270
308
|
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
309
|
|
|
@@ -280,11 +318,16 @@ For Claude Code, register the MCP server with the endpoint in one line (the env
|
|
|
280
318
|
claude mcp add cdp-toolkit -e CDP_FIREFOX_ENDPOINT=9223 -- npx -y cdp-toolkit
|
|
281
319
|
```
|
|
282
320
|
|
|
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
|
|
321
|
+
**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 — normal exit, `SIGINT`/`SIGTERM`, stdin close) sends `session.end` first, which frees the slot for the next process cross-process. When several cdp-toolkit processes attach to the **same** user Firefox endpoint, they now coordinate that single session slot through a file-based lease (a sibling of the tab-lease mechanism), so a collision no longer hard-wedges both sides:
|
|
322
|
+
|
|
323
|
+
- **Two genuinely live processes** are serialized rather than both wedging: the second waits up to `CDP_FIREFOX_SESSION_WAIT_MS` (default `10000`ms) for the slot to free, then returns a distinguishable, fast error naming the live holder — "Firefox's single WebDriver BiDi session on `<endpoint>` is held by a LIVE process `<label>` (pid N) … wait & retry, or point this server at a different endpoint/browser" — instead of hanging.
|
|
324
|
+
- **An orphaned session from a dead holder** — the classic wedge, a client `SIGKILL`ed or crashed without `session.end`, which Firefox does not reap on its own — is now **auto-recovered**: cdp-toolkit force-clears the orphaned session over the **Marionette side channel** (a blind `WebDriver:DeleteSession` on port `2828`, `CDP_FIREFOX_MARIONETTE_PORT`) **without** killing or restarting Firefox, then retries. This works **only** if that Firefox was launched with `--marionette` (verified against FF153: `--remote-debugging-port` alone does **not** start Marionette). When Marionette is absent it degrades to a clear, actionable error ("orphaned … could not be auto-cleared. Marionette recovery needs Firefox launched with `--marionette` …; otherwise restart Firefox") rather than the old dead-end.
|
|
325
|
+
|
|
326
|
+
This coordinates and recovers **around** Firefox's one-session limit; it does not remove it. Two live holders still cannot share the slot at once — one is served, the other waits or is told, by name, who holds it.
|
|
284
327
|
|
|
285
328
|
**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
329
|
|
|
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,
|
|
330
|
+
**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. On the MCP server a second filter composes on top: a tool is listed only when the backend can run it *and* its group is active — backend availability first, profile second (see [Progressive disclosure](#progressive-disclosure)). Under Firefox, six capability areas are absent because Firefox 153's BiDi implementation has no equivalent domain:
|
|
288
331
|
|
|
289
332
|
- `performance_start_trace`, `performance_stop_trace`, `performance_analyze_insight`, `performance_trace` (needs `trace.performance`)
|
|
290
333
|
- `take_heapsnapshot` (needs `heap.snapshot`)
|
|
@@ -293,7 +336,7 @@ claude mcp add cdp-toolkit -e CDP_FIREFOX_ENDPOINT=9223 -- npx -y cdp-toolkit
|
|
|
293
336
|
- `dispatch_mouse` (needs `input.raw`): the raw move/down/up primitive is a direct `Input.dispatchMouseEvent` wrapper with no BiDi analogue.
|
|
294
337
|
- `wait_for_download`, `grant_permissions` (need `browser.downloads` / `browser.permissions`): both drive Chrome's `Browser.*` domain; WebDriver BiDi has no command to redirect a download or pre-grant a permission.
|
|
295
338
|
|
|
296
|
-
Everything else, including `mock_request`/`list_mocks`/`clear_mocks` (Firefox's `network.addIntercept` covers the same fake-backend use case as Chrome's `Fetch` domain), the `claim_page`/`release_page`/`list_leases` lease group, and the new `scroll` tool (Chrome dispatches `Input.dispatchMouseEvent{type:'mouseWheel'}`, Firefox uses BiDi's `wheel` input source — both live-verified), is available under both backends: 34 of the 45 tools.
|
|
339
|
+
Everything else, including `mock_request`/`list_mocks`/`clear_mocks` (Firefox's `network.addIntercept` covers the same fake-backend use case as Chrome's `Fetch` domain), the `claim_page`/`release_page`/`list_leases` lease group, and the new `scroll` tool (Chrome dispatches `Input.dispatchMouseEvent{type:'mouseWheel'}`, Firefox uses BiDi's `wheel` input source — both live-verified), is available under both backends: 34 of the 45 tools. Under the MCP server's default profile only the core subset of those is listed until their groups are activated — the profile filter applies after this backend filter. One asymmetry to know before expecting Chrome-style concurrency: the lease group fences *tabs* on both backends, but Firefox permits only **one BiDi session per browser instance**, so multiple agents under Firefox share and serialize on that single session (coordinated by the cross-process session lease above), not the independent concurrent sessions Chrome's unlimited CDP connections allow — see "Parallel tabs" below.
|
|
297
340
|
|
|
298
341
|
**Honest capability gaps, not oversold parity:**
|
|
299
342
|
|
|
@@ -308,6 +351,8 @@ Everything else, including `mock_request`/`list_mocks`/`clear_mocks` (Firefox's
|
|
|
308
351
|
|
|
309
352
|
## The tools (29 parity + 16 superset = 45)
|
|
310
353
|
|
|
354
|
+
This table is the **full-profile view** — all 45 tools, which is what `cdp --list` and `CDP_TOOL_PROFILE=full` show. On the MCP server's default `core` profile only 12 of them are listed at startup; the rest sit in 12 activatable groups: `input` (7: hover, drag, scroll, dispatch_mouse, press_key, fill_form, upload_file), `cookies` (3: list/set/delete_cookies), `network` (2: list/get_network_request), `console` (2: list/get_console_message), `mocking` (3: mock_request, list_mocks, clear_mocks), `emulation` (2: emulate, resize_page), `performance` (6: the four trace tools, take_heapsnapshot, lighthouse_audit), `recording` (2: start/stop_screen_recording), `leases` (3: claim_page, release_page, list_leases), `permissions` (1: grant_permissions), `dialogs` (1: handle_dialog), `downloads` (1: wait_for_download). See [Progressive disclosure](#progressive-disclosure).
|
|
355
|
+
|
|
311
356
|
The 29 parity tools are 1:1 with `chrome-devtools-mcp`; the 16 superset tools (`performance_trace`, the `list_cookies`/`set_cookie`/`delete_cookies` cookie group, the `mock_request`/`list_mocks`/`clear_mocks` group, the `claim_page`/`release_page`/`list_leases` lease group, the `start_screen_recording`/`stop_screen_recording` screen-recording pair, `scroll`, `dispatch_mouse`, `wait_for_download`, and `grant_permissions`) are toolkit additions. Each row notes the underlying CDP method(s) and the precise parity gaps.
|
|
312
357
|
|
|
313
358
|
| MCP name | CDP method(s) | Parity notes / gaps |
|
|
@@ -352,7 +397,7 @@ The 29 parity tools are 1:1 with `chrome-devtools-mcp`; the 16 superset tools (`
|
|
|
352
397
|
| `mock_request` *(superset)* | `Fetch.*` (+ `Page.reload`) | **A fake backend.** Registers a rule on a target's persistent session: fulfill with a canned response, fail, or continue (with optional `delayMs`/`failRate`). Persists across reloads until `clear_mocks`. Request-stage only. Cached requests aren't intercepted (use `reload:true`). |
|
|
353
398
|
| `list_mocks` *(superset)* | `Runtime.evaluate` (liveness probe) | Lists active mock sessions with rules + hit counts; prunes sessions whose tab closed. |
|
|
354
399
|
| `clear_mocks` *(superset)* | `Fetch.disable` | Tears down the resolved target's mock session (or all with `all:true`). |
|
|
355
|
-
| `claim_page` *(superset)* | `Target.createTarget` + lease file | Opens a fresh tab and claims it (no `target`/`targetId`), or takes over a tab that is already open (`target`, any selector, or the exact-id `targetId`) and returns an opaque lease token plus `opened` (true only when this call created the tab). Never steals a tab another live process holds. Also returns `humanActiveMs` (ms since input this server did not dispatch; `null` means no data, never "no human") and, on a takeover of a tab a human used within the last 30s, a `contention` warning — the claim is never refused for it. MCP only; the CLI refuses it. |
|
|
400
|
+
| `claim_page` *(superset)* | `Target.createTarget` + lease file | Opens a fresh tab and claims it (no `target`/`targetId`), or takes over a tab that is already open (`target`, any selector, or the exact-id `targetId`) and returns an opaque lease token plus `opened` (true only when this call created the tab). Never steals a tab another live process holds. Also returns `humanActiveMs` (ms since input this server did not dispatch; `null` means no data, never "no human") and, on a takeover of a tab a human used within the last 30s, a `contention` warning — the claim is never refused for it. MCP only; the CLI refuses it. The `browser_tools`/`describe_tool` meta-tools are likewise MCP-only. |
|
|
356
401
|
| `release_page` *(superset)* | lease file | Gives a lease back via `lease` (the token) or `target` (a selector, for a lease the gate auto-acquired and so never handed out a token for). Idempotent. Closes the tab by default when this toolkit's creation ledger says it opened it; leaves a merely-claimed tab open. Override either way with `close`. |
|
|
357
402
|
| `list_leases` *(superset)* | lease file | Who holds what, with pid liveness and reclaimability, plus computed `idleMs`/`expiresAt` and, where the tab's beacon answers, `humanActiveMs` (absent, not null, when there's no answer). Needs no token; never returns the nonce. A lease file that could not be read or parsed is reported as an `unreadable` row instead of being skipped (with `idleMs`/`expiresAt` omitted). Under `CDP_REQUIRE_LEASE` also reaps abandoned agent tabs, reporting closures in an additive `reaped` array. |
|
|
358
403
|
| `wait_for_download` *(superset)* | `Browser.setDownloadBehavior` + `Browser.downloadWillBegin`/`downloadProgress` on a standing browser-endpoint connection | Waits for a download to finish and returns it as a real file on disk: `{path,suggestedFilename,bytes,url,target}`. **Ordering rule, not optional:** call `wait_for_download{arm:true}` *before* the click that starts the download, then click, then call `wait_for_download` again to collect it — arming this late is not a preference, it's because Chrome reverts the download-behavior override the instant the arming client disconnects, and denies an unarmed download outright. **Browser-global side effect:** while armed, *every* download in the browser is redirected into the toolkit's downloads directory. **MCP-server only:** the arm lives on a connection this server process holds open; the one-shot CLI's connection dies with the process, so nothing is captured there. Chrome-only (`browser.downloads`). |
|
|
@@ -407,6 +452,8 @@ Two agents pointed at the same Chrome both resolve `target: active` to whatever
|
|
|
407
452
|
|
|
408
453
|
Pass that token as `lease` on every later call against the tab. `new_page` with `claim: true` does the open-and-claim in one call. `release_page` gives it back and, by default, closes the tab if this toolkit opened it (see "Close on release" below). `list_leases` shows who holds what.
|
|
409
454
|
|
|
455
|
+
**Firefox: the ceiling is one BiDi session per browser instance.** The `claim_page`/`release_page`/`list_leases` tab-lease group works under both backends, but a real asymmetry sits underneath it. Chrome accepts unlimited concurrent CDP connections, so "many agents, one browser" there means many genuinely independent sessions. Firefox permits only **one** WebDriver BiDi session per browser instance (see [Firefox](#firefox-webdriver-bidi)), so under Firefox "many agents, one browser" means many agents *sharing and serialized on that single session* — coordinated by the cross-process session lease, not running as independent sessions. Expect Chrome-style multi-session concurrency on Chrome only; on Firefox the tabs are still leasable, but the session beneath them is a single shared slot.
|
|
456
|
+
|
|
410
457
|
**Opt in, and only ever a refusal — this is the default, with `CDP_REQUIRE_LEASE` unset.** A tab nobody claimed behaves exactly as it did before. Omitting `lease` is identical to pre-1.2 behavior in every respect: no default changed, no return shape changed, no previously legal call now needs a new argument. This is why it was 1.2 and not 2.0.
|
|
411
458
|
|
|
412
459
|
### Strict mode: `CDP_REQUIRE_LEASE`
|
|
@@ -508,8 +555,10 @@ src/
|
|
|
508
555
|
types.ts # Target, TargetSelector, Uid, CDP envelopes
|
|
509
556
|
index.ts # TOOLS registry (45) + re-exported client primitives
|
|
510
557
|
cli.ts # the Bun CLI
|
|
511
|
-
mcp.ts # stdio MCP server (exposes TOOLS via @modelcontextprotocol/
|
|
558
|
+
mcp.ts # stdio MCP server (exposes TOOLS via @modelcontextprotocol/server)
|
|
512
559
|
manifest.ts # JSON Schemas advertised by the MCP server (one per tool)
|
|
560
|
+
toolGroups.ts # tool-to-group map + core/full profiles (progressive disclosure)
|
|
561
|
+
toolDocs.ts # full per-tool prose served on demand by describe_tool
|
|
513
562
|
leases.ts # lease records, staleness, reclamation, assertLeaseOk, CDP_REQUIRE_LEASE
|
|
514
563
|
leases-tools.ts # claim_page / release_page / list_leases
|
|
515
564
|
origins.ts # tab creation ledger: who OPENED a tab, outliving its lease
|