cdp-toolkit 1.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 ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ All notable changes to cdp-toolkit are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] — 2026-06-24
9
+
10
+ First public release.
11
+
12
+ ### Added
13
+ - **33 tools** over raw Chrome DevTools Protocol on a single direct WebSocket:
14
+ - **29 `chrome-devtools-mcp` parity tools** — `list_pages`, `new_page`, `close_page`, `select_page`, `navigate_page`, `wait_for`, `evaluate_script`, `take_snapshot`, `click`, `hover`, `drag`, `fill`, `fill_form`, `type_text`, `press_key`, `upload_file`, `take_screenshot`, `emulate`, `resize_page`, `handle_dialog`, `list_console_messages`, `get_console_message`, `list_network_requests`, `get_network_request`, `performance_start_trace`, `performance_stop_trace`, `performance_analyze_insight`, `take_heapsnapshot`, `lighthouse_audit`.
15
+ - **4 superset tools** beyond parity — `performance_trace` (a robust single-call trace that survives the cross-process limitation of the start/stop pair), and the network-mocking group `mock_request` / `list_mocks` / `clear_mocks` (a persistent per-target fake backend).
16
+ - **Single direct WebSocket per call** — every tool resolves exactly one target (`active | index:N | url:<substr> | title:<substr> | <targetId>`); no all-target fan-out.
17
+ - **Per-command timeout** (`CDP_TIMEOUT_MS`, default 15s) — `CdpConnection.send()` rejects rather than hangs, so a stuck renderer can't block a caller indefinitely.
18
+ - **Lazy domain enabling** — domains are enabled only where a tool needs them; no eager `Network.enable` on connect.
19
+ - **Stateless element refs** — a `uid` IS the CDP `backendDOMNodeId`, resolved on demand; no server-side handle table to drift or expire.
20
+ - **stdio MCP server** (`src/mcp.ts`) exposing every tool via `@modelcontextprotocol/sdk`, with JSON Schemas in `src/manifest.ts`. Connects to Chrome lazily per call, so it loads cleanly even when Chrome isn't running.
21
+ - **Bun CLI** (`src/cli.ts`) — `bun run src/cli.ts <tool> [--target <sel>] [--json '<obj>'] [--<key> <value> ...]`.
22
+ - Hermetic test fixtures — `mock-smoke.ts` mocks `https://mock.invalid/*` (CDP `Fetch.requestPaused` intercepts before DNS, so the smoke needs no real network).
23
+
24
+ ### Notes
25
+ - License: MIT.
26
+ - Zero runtime dependencies in the CDP/CLI layer (Node's global `WebSocket` + `fetch`). The MCP server adds only `@modelcontextprotocol/sdk`; `lighthouse_audit` is the sole non-CDP tool and shells out to `npx lighthouse`.
27
+ - Runtime: Bun ≥ 1.1 (recommended) or Node ≥ 22 (for the global `WebSocket`). Requires Chrome/Chromium started with `--remote-debugging-port=9222`.
28
+
29
+ [1.0.0]: https://github.com/sblattj/cdp-toolkit/releases/tag/v1.0.0
package/CONTRACT.md ADDED
@@ -0,0 +1,116 @@
1
+ # cdp-toolkit — implementation contract
2
+
3
+ **Single source of truth for every tool module.** The goal: replicate each of the
4
+ 29 `chrome-devtools-mcp` tools over raw CDP on a direct WebSocket
5
+ (`ws://127.0.0.1:9222/...`), no Puppeteer / MCP layer. Read this fully before
6
+ writing a module.
7
+
8
+ ## Hard rules
9
+
10
+ 1. **Zero runtime dependencies.** Use Node's global `WebSocket` (Node ≥ 22; we run 25.9) and `fetch`. The only allowed devDeps are `typescript` + `@types/node` (already installed). `lighthouse.ts` is the sole exception and shells out to a subprocess — see its section.
11
+ 2. **Build only on `src/client.ts` and `src/types.ts`.** Do not open raw `new WebSocket` yourself. Use `openPage`, `withPage`, `openBrowser`, `resolveTarget`, `CdpConnection`. This is what gives us the per-command timeout that prevents the wedged-tab hang.
12
+ 3. **TypeScript strict.** `tsc --noEmit` must pass with `strict`, `noUncheckedIndexedAccess`, `verbatimModuleSyntax`. Import types with `import type`. Use `.ts` extensions in imports (e.g. `import { withPage } from "../client.ts"`).
13
+ 4. **One module per assigned bundle.** Write only the files you are assigned. Never edit `client.ts`, `types.ts`, or another agent's file.
14
+ 5. **Tool fn naming = camelCase of the MCP tool name.** `take_snapshot` → `export async function takeSnapshot(...)`. `list_network_requests` → `listNetworkRequests`. The integration step maps mechanically, so the names must be exact.
15
+
16
+ ## Core client API (from `src/client.ts`)
17
+
18
+ ```ts
19
+ const BASE: string; // http://127.0.0.1:9222 (env CDP_BASE)
20
+ const DEFAULT_TIMEOUT_MS: number; // 15000 (env CDP_TIMEOUT_MS)
21
+ class CdpError extends Error {}
22
+
23
+ class CdpConnection {
24
+ send<T>(method: string, params?: object, opts?: {timeoutMs?: number; sessionId?: string}): Promise<T>; // resolves to result, rejects on CDP error/timeout
25
+ on(method: string, handler: (params, sessionId?) => void): () => void; // returns unsubscribe; "*" = all events
26
+ waitFor<P>(method: string, predicate?: (p: P) => boolean, timeoutMs?): Promise<P>;
27
+ close(): void;
28
+ }
29
+
30
+ function listTargets(): Promise<Target[]>; // GET /json/list
31
+ function browserWsUrl(): Promise<string>; // GET /json/version -> browser endpoint
32
+ function resolveTarget(sel: TargetSelector): Promise<Target>;
33
+ function openBrowser(opts?): Promise<CdpConnection>; // Target.* / Browser.*
34
+ function openPage(sel, opts?): Promise<{conn: CdpConnection; target: Target}>;
35
+ function withPage<T>(sel, fn: (conn, target) => Promise<T>, opts?): Promise<T>; // opens, runs, always closes
36
+ ```
37
+
38
+ `send` returns the CDP `result` object directly. Example:
39
+ ```ts
40
+ const { data } = await conn.send<{ data: string }>("Page.captureScreenshot", { format: "png" });
41
+ ```
42
+
43
+ ## Tool function signature convention
44
+
45
+ Every tool is an exported async function:
46
+
47
+ ```ts
48
+ export interface ClickArgs { target?: TargetSelector; uid: Uid; /* ... */ }
49
+ export async function click(args: ClickArgs): Promise<unknown>;
50
+ ```
51
+
52
+ - First/only param is a single typed args object. Always include `target?: TargetSelector` for page-scoped tools (defaults to the active page via `resolveTarget(undefined)`).
53
+ - **Return natural data** (plain JSON-serializable objects). For artifacts (screenshots, traces, heap snapshots) write the file and return `{ path, bytes, ... }`.
54
+ - **Throw `CdpError` (or any Error) on failure.** The dispatcher catches and wraps. Do not return error sentinels.
55
+ - Use `withPage` for stateless one-shot tools. Use a persistent `openPage` + `conn.close()` only when you must subscribe to events across time (recorder).
56
+ - Keep functions stateless except where the recorder / selected-target state file is explicitly specified below.
57
+
58
+ ## Artifact + state locations
59
+
60
+ - Artifacts (PNG, trace, heapsnapshot, lighthouse report): write under `ARTIFACT_DIR` = `process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit"`. `mkdir -p` it. Filenames: `<tool>-<targetIdShort>-<isoish>.<ext>` where you derive a stamp from `Date.now()` at call time (allowed in normal runtime; only the Workflow *script* sandbox forbids it — modules run normally).
61
+ - Selected-target state file (for `select_page`): `process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit"` + `/selected`. Contains a bare targetId. `resolveTarget` does NOT read it; tools may read it as a fallback default if you choose, but the simplest correct behavior is fine.
62
+ - Recorder buffers: `${ARTIFACT_DIR}/rec-<targetId>.jsonl`.
63
+
64
+ ## The element-reference scheme (READ if you touch snapshot or input)
65
+
66
+ A `Uid` **is** a CDP `backendDOMNodeId` (a number). This makes refs stateless:
67
+
68
+ - `take_snapshot` calls `Accessibility.getFullAXTree` (enable `Accessibility` first), walks nodes, and emits a compact indented text tree. Each interactable/meaningful node line carries its `backendDOMNodeId` as the uid, e.g.:
69
+ ```
70
+ [<uid>] <role> "<name>" [extra: value/checked/url]
71
+ ```
72
+ Return `{ snapshot: string, target: {id,url,title}, nodeCount }`.
73
+ - **Shared helper, exported from `snapshot.ts`:**
74
+ ```ts
75
+ export async function resolveUid(conn: CdpConnection, uid: Uid): Promise<{ objectId: string }>;
76
+ // DOM.resolveNode({ backendNodeId: uid }) -> { object: { objectId } }
77
+ ```
78
+ - Interaction tools (`input.ts`) import `resolveUid`, then `Runtime.callFunctionOn` on that objectId to scroll into view + read `getBoundingClientRect`, compute the center, and dispatch via `Input.dispatchMouseEvent` / `Input.dispatchKeyEvent`. Pattern for click:
79
+ ```ts
80
+ const { objectId } = await resolveUid(conn, uid);
81
+ const { result } = await conn.send("Runtime.callFunctionOn", {
82
+ objectId,
83
+ functionDeclaration: "function(){this.scrollIntoView({block:'center',inline:'center'});const r=this.getBoundingClientRect();return {x:r.left+r.width/2,y:r.top+r.height/2};}",
84
+ returnByValue: true,
85
+ });
86
+ const { x, y } = result.value;
87
+ await conn.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
88
+ await conn.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
89
+ ```
90
+ Some tools also accept a CSS `selector` as an alternative to `uid` (resolve via `Runtime.evaluate`/`DOM.querySelector`). Support both: `{ uid?: Uid; selector?: string }`, require exactly one.
91
+
92
+ ## Module / tool assignments
93
+
94
+ | Module file | Tools (MCP name → fn) | Notes |
95
+ |---|---|---|
96
+ | `src/tools/pages.ts` | `list_pages`→`listPages`, `new_page`→`newPage`, `close_page`→`closePage`, `select_page`→`selectPage` | **Browser endpoint** (`openBrowser`, `Target.*`). `new_page`=`Target.createTarget`. `close_page`=`Target.closeTarget`. `select_page`=`Target.activateTarget` + write selected state file. `list_pages` may reuse `listTargets()`. |
97
+ | `src/tools/navigation.ts` | `navigate_page`→`navigatePage`, `wait_for`→`waitForText` (export as `waitFor` too) | `Page.enable` then `Page.navigate` (or `Page.reload` + `Page.getFrameTree` when `reload:true`; `ignoreCache:true` = hard reload); await `Page.frameStoppedLoading`/`Page.loadEventFired`. `wait_for` polls `Runtime.evaluate` for `document.body.innerText.includes(text)` with timeout, OR waits for a given event. |
98
+ | `src/tools/evaluate.ts` | `evaluate_script`→`evaluateScript` | `Runtime.enable` + `Runtime.evaluate` (`returnByValue`, `awaitPromise`). Support optional `args` via `Runtime.callFunctionOn` on a uid/selector. Surface `exceptionDetails` as thrown error. |
99
+ | `src/tools/snapshot.ts` | `take_snapshot`→`takeSnapshot` **+ export `resolveUid`** | a11y tree + ref scheme above. |
100
+ | `src/tools/input.ts` | `click`→`click`, `hover`→`hover`, `drag`→`drag`, `fill`→`fill`, `fill_form`→`fillForm`, `type_text`→`typeText`, `press_key`→`pressKey`, `upload_file`→`uploadFile` | imports `resolveUid` from `./snapshot.ts`. `hover`=mouseMoved. `drag`=press→move→release. `fill`/`type_text`=focus + `Input.insertText` (fast) or `dispatchKeyEvent`. `press_key`=`Input.dispatchKeyEvent` (support modifiers + named keys). `upload_file`=`DOM.setFileInputFiles` on resolved node. `fill_form`=array of {uid|selector,value}. |
101
+ | `src/tools/screenshot.ts` | `take_screenshot`→`takeScreenshot` | `Page.captureScreenshot`. Support `{ format:'png'|'jpeg', quality, fullPage, uid?/selector? (element clip via getBoxModel), savePath? }`. Write to ARTIFACT_DIR, return `{ path, bytes, format }` and optionally base64 when `{ returnBase64:true }`. |
102
+ | `src/tools/emulation.ts` | `emulate`→`emulate`, `resize_page`→`resizePage` | `Emulation.setDeviceMetricsOverride` (width,height,deviceScaleFactor,mobile), `Emulation.setUserAgentOverride`, `Emulation.setCPUThrottlingRate`, `Emulation.setEmulatedMedia`, `Network`-based throttling via `Network.emulateNetworkConditions`. `resize_page` = device metrics width/height. |
103
+ | `src/tools/dialogs.ts` | `handle_dialog`→`handleDialog` | `Page.enable`, subscribe `Page.javascriptDialogOpening`, respond with `Page.handleJavaScriptDialog({ accept, promptText })`. Support a "wait for next dialog then handle" mode and an "auto-handle for N ms" mode. |
104
+ | `src/tools/recorder.ts` + `src/tools/console.ts` + `src/tools/network.ts` | console: `list_console_messages`→`listConsoleMessages`, `get_console_message`→`getConsoleMessage`; network: `list_network_requests`→`listNetworkRequests`, `get_network_request`→`getNetworkRequest` | **Owned by one agent.** `recorder.ts` exports `startRecorder(target, {network,console}): Promise<{stop()}>` that opens a persistent `openPage` conn, enables `Network`+`Runtime`(+`Log`), and appends events to `rec-<targetId>.jsonl`. console/network `list_*` read that file (and support a one-shot `{ reload:true, durationMs }` mode that records fresh by reloading the page and capturing for a window, then returns). `get_*` filter by id/url. Document the recorder model at the top of `recorder.ts`. |
105
+ | `src/tools/performance.ts` | `performance_start_trace`→`performanceStartTrace`, `performance_stop_trace`→`performanceStopTrace`, `performance_analyze_insight`→`performanceAnalyzeInsight` | `Tracing.start`(categories incl. `devtools.timeline`,`blink.user_timing`,`loading`,`-*`), collect `Tracing.dataCollected`, `Tracing.end`→`Tracing.tracingComplete`, write trace JSON to ARTIFACT_DIR. start/stop need a persistent conn — use a state file to hold the in-flight trace path/connection target. `performance_analyze_insight` parses a trace file for LCP, FCP/navigation timing, long tasks (>50ms), layout shifts (CLS), total blocking time; return structured metrics. **Mark in a top comment that this is a CDP-native approximation of the MCP's insight analyzer.** |
106
+ | `src/tools/heap.ts` | `take_heapsnapshot`→`takeHeapsnapshot` | `HeapProfiler.enable` → `HeapProfiler.takeHeapSnapshot`, assemble `HeapProfiler.addHeapSnapshotChunk` events into a `.heapsnapshot` file under ARTIFACT_DIR; return `{ path, bytes }`. |
107
+ | `src/tools/lighthouse.ts` | `lighthouse_audit`→`lighthouseAudit` | **Not pure CDP.** Spawn `npx --yes lighthouse <url> --port=9222 --output=json --output-path=<artifact> --chrome-flags=...` via `node:child_process`. If lighthouse/npx unavailable, throw a clear `CdpError` explaining the dependency. Return `{ path, categories: {performance, accessibility, ...scores} }` parsed from the JSON. Top comment must state this is the one non-CDP tool. |
108
+ | `src/tools/network_mock.ts` | `mock_request`→`mockRequest`, `list_mocks`→`listMocks`, `clear_mocks`→`clearMocks` | **Toolkit addition (not MCP parity) — a persistent fake backend.** Holds a per-target session (module-level `Map`, like `recorder.ts`) with a persistent `openPage` conn and `Fetch.enable` (patterns). `Fetch.requestPaused` → `selectRule` → `Fetch.fulfillRequest`/`failRequest`/`continueRequest` (default-continue unmatched, or page hangs). Pure logic (`urlMatches`/`selectRule`/`buildFulfillParams`/`effectiveAction`) is unit-tested; I/O via `test/mock-smoke.ts`. Persistence is MCP-server-process-scoped (CLI is one-shot). Request-stage only. |
109
+
110
+ ## Each module's footer
111
+
112
+ End every module with a brief block comment listing the exact CDP methods/domains used and any parity gaps vs the MCP tool (one line each). The verification phase reads these.
113
+
114
+ ## Integration (built after all modules exist)
115
+
116
+ `src/index.ts` exports a `TOOLS` registry: `Record<mcpToolName, (args) => Promise<unknown>>` importing every fn. `src/cli.ts`: `bun run src/cli.ts <tool> [--target <sel>] [--json '<obj>'] [--<k> <v> ...]` → parse args, call `TOOLS[tool]`, print JSON result or `{ error }`, exit non-zero on throw. `README.md`: the full 29-tool mapping table + usage + the "why direct CDP beats MCP for known targets" rationale.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stephen Blatt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,255 @@
1
+ # cdp-toolkit
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 *one* Chrome tab you point it at over the raw DevTools Protocol, 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, one tab, no fan-out — plus a built-in network-mocking fake backend. **33 tools.**
4
+
5
+ > For AI-agent developers and Claude Code / Cursor users who need **one known tab driven reliably** — not a Puppeteer-managed browser.
6
+
7
+ [![CI](https://github.com/sblattj/cdp-toolkit/actions/workflows/ci.yml/badge.svg)](https://github.com/sblattj/cdp-toolkit/actions/workflows/ci.yml)
8
+ [![npm](https://img.shields.io/npm/v/cdp-toolkit?color=cb3837&logo=npm)](https://www.npmjs.com/package/cdp-toolkit)
9
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
10
+ [![Model Context Protocol](https://img.shields.io/badge/MCP-compatible-1f6feb)](https://modelcontextprotocol.io)
11
+
12
+ <!-- Demo: record with `vhs docs/demo.tape` (see docs/demo.tape) and commit docs/demo.gif -->
13
+ ![cdp-toolkit demo](docs/demo.gif)
14
+
15
+ ## In plain terms
16
+
17
+ You let an AI agent — Claude Code, Cursor, any MCP host — control a Chrome tab: click, type, read the page, take screenshots, even fake API responses to test a UI before its backend exists. [`chrome-devtools-mcp`](https://github.com/ChromeDevTools/chrome-devtools-mcp) does this too, but it runs a whole Puppeteer browser and talks to *all* your open tabs at once — which is why a single busy tab can freeze it and leave you typing `/mcp` to restart the server mid-task.
18
+
19
+ `cdp-toolkit` keeps it simple: **one connection to the one tab you point it at, and a time limit on every action.** When something stalls, you get an error back — not a frozen agent. Same things you could do before, minus the wedging and the restarts.
20
+
21
+ That's the whole pitch. The technical *why* — fan-out, lazy domain enabling, the `Network.enable` hang — is below.
22
+
23
+ ## Why it exists
24
+
25
+ If [`chrome-devtools-mcp`](https://github.com/ChromeDevTools/chrome-devtools-mcp) ever **wedged your agent on a busy tab**, you've met its design: it manages a Puppeteer browser, fans every operation out across *all* attached targets, and enables the `Network` domain on connect so it can passively buffer everything. That generality is exactly what makes it fragile once you already know which tab you want to drive.
26
+
27
+ `cdp-toolkit` makes the opposite bet. Every tool attaches **one** direct WebSocket to **one** resolved target, enables only the CDP domains it needs, and enforces a **per-command timeout** so a stuck renderer can never hang the caller. For driving a tab you already have in hand — the common automation and evidence-gathering case — it's materially more robust.
28
+
29
+ ## cdp-toolkit vs chrome-devtools-mcp
30
+
31
+ | | **cdp-toolkit** | **chrome-devtools-mcp** |
32
+ |---|---|---|
33
+ | **Target scope** | one resolved tab (`active` / `index:N` / `url:` / `title:`) | all attached targets — fan-out can stall on an unrelated tab |
34
+ | **`Network.enable`** | lazy — only when a tool needs it | eager on connect — a known hang on busy renderers |
35
+ | **Per-command timeout** | ✅ bounded (`CDP_TIMEOUT_MS`, 15s default) — rejects, never hangs | ❌ none — a stuck renderer blocks indefinitely |
36
+ | **Element refs** | stateless `backendDOMNodeId` (resolved on demand) | server-side handle table (can drift / expire) |
37
+ | **Network mocking** | ✅ persistent per-target fake backend | ❌ not available |
38
+ | **Runtime deps** | CDP/CLI layer: native `WebSocket` + `fetch` (the MCP server adds only the MCP SDK) | Puppeteer stack |
39
+ | **Auto-wait / retry** | ❌ single-shot — re-snapshot between steps | ✅ Puppeteer's auto-wait envelope |
40
+
41
+ **Use `chrome-devtools-mcp`** if you need multi-target autonomy or Puppeteer's auto-wait/retry on an *unknown* page. **Use `cdp-toolkit`** when you have one tab in hand and need it not to hang. They coexist — `cdp-toolkit`'s tools are namespaced `mcp__cdp-toolkit__*`, distinct from `mcp__chrome-devtools__*`.
42
+
43
+ ## Is this for you?
44
+
45
+ **Yes, if you —**
46
+ - drive one known tab from Claude Code / Cursor / any MCP host and want it to never wedge;
47
+ - need to **mock a backend** to build or test a UI before the real API exists;
48
+ - have been bitten by the eager-`Network.enable` hang on a busy renderer.
49
+
50
+ **Probably not, if you —**
51
+ - need cross-browser (Firefox / WebKit) — use [`playwright-mcp`](https://github.com/microsoft/playwright-mcp);
52
+ - need Puppeteer's auto-wait/retry envelope for an unknown, changing page;
53
+ - want one server to fan out across *all* your open tabs at once.
54
+
55
+ ## Quickstart (about 30 seconds)
56
+
57
+ ```bash
58
+ # 1. Start Chrome/Chromium with the DevTools port open
59
+ open -a "Google Chrome" --args --remote-debugging-port=9222
60
+ # (Linux: google-chrome --remote-debugging-port=9222)
61
+
62
+ # 2. Add cdp-toolkit to Claude Code at user scope (every project, no install step)
63
+ claude mcp add cdp-toolkit --scope user -- npx -y cdp-toolkit # or: bunx -y cdp-toolkit
64
+
65
+ # 3. In Claude Code, call a tool to prove it works:
66
+ # mcp__cdp-toolkit__list_pages
67
+ ```
68
+
69
+ That's it. Tools appear namespaced as `mcp__cdp-toolkit__<tool>`. The server connects to Chrome **lazily per call**, so it loads cleanly even when Chrome isn't running.
70
+
71
+ Prefer the **CLI**? Every tool is runnable directly:
72
+
73
+ ```bash
74
+ npx -y --package cdp-toolkit cdp list_pages # or swap npx → bunx
75
+ npx -y --package cdp-toolkit cdp navigate_page --target index:0 --url https://example.com
76
+ # …or from a clone: `bun run src/cli.ts <tool> …`
77
+ ```
78
+
79
+ **Requirements:** Node ≥ 22 **or** Bun ≥ 1.1 — `npx -y cdp-toolkit` and `bunx -y cdp-toolkit` both work (the published bins are plain Node ESM). Chrome/Chromium with `--remote-debugging-port=9222`. Smoke-check the port: `curl -s http://127.0.0.1:9222/json/version`.
80
+
81
+ ### MCP client setup
82
+
83
+ <details>
84
+ <summary><b>Claude Code</b></summary>
85
+
86
+ ```bash
87
+ claude mcp add cdp-toolkit --scope user -- npx -y cdp-toolkit # or: bunx -y cdp-toolkit
88
+ claude mcp get cdp-toolkit # status (should show ✓ Connected)
89
+ ```
90
+ A newly-registered server loads on the next Claude Code start; in an existing session, reconnect via `/mcp`.
91
+ </details>
92
+
93
+ <details>
94
+ <summary><b>Cursor / Windsurf / any MCP host</b></summary>
95
+
96
+ Add to your MCP config (e.g. `~/.cursor/mcp.json`):
97
+ ```json
98
+ {
99
+ "mcpServers": {
100
+ "cdp-toolkit": { "command": "npx", "args": ["-y", "cdp-toolkit"] }
101
+ }
102
+ }
103
+ ```
104
+ </details>
105
+
106
+ <details>
107
+ <summary><b>From a local checkout</b></summary>
108
+
109
+ ```bash
110
+ git clone https://github.com/sblattj/cdp-toolkit && cd cdp-toolkit && bun install
111
+ claude mcp add cdp-toolkit --scope user -- bun run "$(pwd)/src/mcp.ts"
112
+ bun run mcp:smoke # spawn the server + a real initialize/tools-list/tools-call round-trip
113
+ ```
114
+ </details>
115
+
116
+ ## Key capabilities
117
+
118
+ - **Single-target reliability.** One WebSocket to one resolved target, a bounded timeout on every CDP command, lazy domain enabling, and stateless element refs. There is no broadcast step that can stall on a wedged tab.
119
+ - **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`.
120
+ - **Full chrome-devtools-mcp parity + extras.** All 29 upstream tools, plus `performance_trace` (a robust single-call trace), Lighthouse audits, and heap snapshots. 33 single-purpose tools — no discovery overhead, and they coexist with `chrome-devtools-mcp` in a separate namespace.
121
+
122
+ ## Why raw CDP beats the MCP for known targets
123
+
124
+ Each point below leads with the symptom you've probably hit, then the cause, then the fix.
125
+
126
+ - **Your agent stalls on a call when a busy background tab is open** → that's the **all-target fan-out**: every operation broadcasts to all attached targets. cdp-toolkit resolves *one* target (`active | index:N | url:<substr> | title:<substr> | <targetId>`) and attaches a single WebSocket to just that page.
127
+ - **A tool hangs forever and never returns** → the MCP's eager `Network.enable` on a busy or hung renderer is a known wedge. cdp-toolkit enables domains **lazily**, only where a tool needs them (the recorder enables `Network`/`Runtime`/`Log`; most tools touch only `Page`/`Runtime`/`DOM`).
128
+ - **No way to bound a slow call** → `CdpConnection.send()` enforces `CDP_TIMEOUT_MS` (15s default) on *every* command and **rejects rather than hangs**, so a stuck renderer can never block a caller indefinitely.
129
+ - **Element handles drift across calls** → a `uid` **is** a CDP `backendDOMNodeId`, resolved on demand via `DOM.resolveNode`. There is no server-side handle table to drift or expire.
130
+
131
+ The trade-off is generality: this toolkit targets one known page at a time and does **not** replicate Puppeteer's auto-wait/retry envelope. Re-`take_snapshot` between steps rather than expecting an implicit wait.
132
+
133
+ ## Network mocking — a fake backend for building/testing UIs
134
+
135
+ Build and test a UI before its backend (or its data) exists. `mock_request` arms a persistent per-target interception session; mock several endpoints by calling it repeatedly, then iterate on the page — the mocks survive reloads and navigations until `clear_mocks`.
136
+
137
+ ```bash
138
+ # Return empty search results and reload to see how the UI renders the zero state
139
+ cdp mock_request --urlPattern '*/api/search*' --body '{"results":[],"total":0}' --reload true
140
+ # Force the endpoint to error (does the UI show a clean error or hang?)
141
+ cdp mock_request --urlPattern '*/api/search*' --action fail --errorReason Failed --reload true
142
+ # Resilience: fail 30% of calls + add 800ms latency
143
+ cdp mock_request --urlPattern '*/api/*' --failRate 0.3 --delayMs 800
144
+ cdp list_mocks
145
+ cdp clear_mocks --all true
146
+ ```
147
+
148
+ > Cross-origin fetches (e.g. from a `data:` page) need an `Access-Control-Allow-Origin` header on the mock: `--json '{"urlPattern":"*api*","body":"{}","headers":{"Access-Control-Allow-Origin":"*"}}'`. Persistent mock sessions live in the long-lived MCP-server process; under the one-shot CLI each `mock_request` is its own process, so use `--reload true` to apply-and-observe within the single invocation.
149
+
150
+ ## CLI usage
151
+
152
+ ```bash
153
+ # Run any tool by its MCP name; args come from --json and/or --key value flags.
154
+ cdp <tool> [--target <sel>] [--json '<obj>'] [--<key> <value> ...]
155
+ cdp --list # list every available tool name
156
+
157
+ cdp list_pages
158
+ cdp navigate_page --target index:0 --url https://example.com
159
+ cdp take_snapshot --target url:example --interactiveOnly true
160
+ cdp click --target index:0 --uid 42
161
+ cdp evaluate_script --json '{"expression":"document.title"}'
162
+ cdp take_screenshot --target url:example --fullPage true
163
+ cdp lighthouse_audit --url https://example.com --json '{"categories":["performance"]}'
164
+ ```
165
+
166
+ **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.
167
+
168
+ ### Programmatic use
169
+
170
+ ```ts
171
+ import { TOOLS, withPage, resolveTarget, CdpError } from "cdp-toolkit";
172
+
173
+ const pages = await TOOLS.list_pages({});
174
+ await TOOLS.navigate_page({ target: "index:0", url: "https://example.com" });
175
+ ```
176
+
177
+ ### Environment knobs
178
+
179
+ | Env var | Default | Purpose |
180
+ |---|---|---|
181
+ | `CDP_BASE` | `http://127.0.0.1:9222` | DevTools HTTP origin (drives discovery + the lighthouse `--port`). |
182
+ | `CDP_TIMEOUT_MS` | `15000` | Per-command timeout. |
183
+ | `CDP_ARTIFACT_DIR` | `/tmp/cdp-toolkit` | Screenshots, traces, heap snapshots, lighthouse reports, recorder buffers. |
184
+ | `CDP_STATE_DIR` | `/tmp/cdp-toolkit` | `select_page` selected-target file, in-flight trace state. |
185
+
186
+ ## The tools (29 parity + 4 superset = 33)
187
+
188
+ The 29 parity tools are 1:1 with `chrome-devtools-mcp`; the 4 superset tools (`performance_trace` + the `mock_request`/`list_mocks`/`clear_mocks` group) are toolkit additions. Each row notes the underlying CDP method(s) and the precise parity gaps.
189
+
190
+ | MCP name | CDP method(s) | Parity notes / gaps |
191
+ |---|---|---|
192
+ | `list_pages` | `GET /json/list` | `all` flag also exposes worker/background targets; MCP lists only page tabs. |
193
+ | `new_page` | `Target.createTarget` | Returns `{targetId,url}`; does not await navigation (use `navigate_page`). |
194
+ | `close_page` | `Target.closeTarget` | Reports `success:true` on the empty result newer Chromium returns. |
195
+ | `select_page` | `Target.activateTarget` + selected-state file | Writes a flat-file selected target; `resolveTarget` does not read it, so `active` still means `index:0` unless a tool opts in. |
196
+ | `navigate_page` | `Page.navigate` / `Page.reload` + load events | Returns `{url,frameId,waitedFor}` (no auto-snapshot). `waitUntil` supports `load`/`domcontentloaded`. `reload:true` (+ `ignoreCache:true` for a hard reload). |
197
+ | `wait_for` | `Runtime.evaluate` (poll `innerText`) | Text-substring waiting only; throws on timeout rather than returning `{found:false}`. |
198
+ | `evaluate_script` | `Runtime.evaluate` / `callFunctionOn` | No live `page`/element handle; `args` are plain JSON. Main-world context only. |
199
+ | `take_snapshot` | `Accessibility.getFullAXTree` | uid is the raw `backendDOMNodeId` (stateless, non-sequential). Full tree in one shot; frames flattened. `interactiveOnly` is a toolkit addition. |
200
+ | `click` | `Input.dispatchMouseEvent` | No implicit auto-wait/retry — resolves and acts once; re-snapshot between steps. |
201
+ | `hover` | `Input.dispatchMouseEvent` (`mouseMoved`) | Same single-shot model as `click`. |
202
+ | `drag` | `Input.dispatchMouseEvent` (press→move→release) | Synthetic mouse drag; native HTML5 DnD is approximated. |
203
+ | `fill` | `Input.insertText` | Atomic paste-like commit, not per-character keystrokes. |
204
+ | `fill_form` | per field: `callFunctionOn` + `insertText` | Array of `{uid|selector,value}`; same insertText caveat. |
205
+ | `type_text` | `Input.insertText` | Appends (does not clear first); insertText, not per-key. |
206
+ | `press_key` | `Input.dispatchKeyEvent` | Curated named-key table + single chars; not the full Puppeteer KeyInput enum. |
207
+ | `upload_file` | `DOM.setFileInputFiles` | Requires a resolvable `<input type=file>` (uid or selector). |
208
+ | `take_screenshot` | `Page.captureScreenshot` (+ layout metrics) | Clip scale fixed at 1. Full-page uses `captureBeyondViewport` + layout-metrics clip. |
209
+ | `emulate` | `Emulation.*` / `Network.emulateNetworkConditions` | Stateless: UA/CPU/media/network overrides reset when the per-call connection closes. No named device presets. |
210
+ | `resize_page` | `Emulation.setDeviceMetricsOverride` | Verifies via `window.innerWidth/innerHeight`. Override persists on the target. |
211
+ | `handle_dialog` | `Page.javascriptDialogOpening` / `handleJavaScriptDialog` | Caller arms first and triggers out-of-band (or `handleDialogForExpression` to trigger-and-handle atomically). Supports wait-for-next and auto-handle-for-N-ms. |
212
+ | `list_console_messages` | `Runtime`/`Log` events (+ `Page.reload`) | `reload:true` records console+network into a unique per-capture file; default read returns the latest. Args flattened best-effort. |
213
+ | `get_console_message` | reads the shared "latest" buffer | Index into the latest capture; throws if out of range. |
214
+ | `list_network_requests` | `Network.*` events (+ `Page.reload`) | Correlated rows from the per-capture buffer; redirect chains collapse to the first row. No timing breakdown / POST data. |
215
+ | `get_network_request` | above + `Network.getResponseBody` | Bodies only via a fresh reload capture (CDP serves bodies from the live session); `includeBody` matches by **url**. |
216
+ | `performance_start_trace` | `Tracing.start` / `dataCollected` | Works ONLY within one process — a live trace buffer is bound to its connection. Use `performance_trace` for robustness. |
217
+ | `performance_stop_trace` | `Tracing.end` / `tracingComplete` | Must run in the SAME process as `performance_start_trace`; throws a clear error otherwise. |
218
+ | `performance_analyze_insight` | parses a trace JSON file | A **CDP-native approximation** of the MCP insight analyzer (FCP/LCP/CLS/TBT/long-tasks); close but not byte-identical. Requires an explicit `tracePath`. |
219
+ | `take_heapsnapshot` | `HeapProfiler.takeHeapSnapshot` | Returns `{path,bytes,chunks,target}`; does not parse the snapshot (load the `.heapsnapshot` in the DevTools Memory panel). |
220
+ | `lighthouse_audit` | **none (non-CDP)** — spawns `npx --yes lighthouse …` | The toolkit's sole non-CDP tool. Defaults to the desktop preset. Returns numeric category scores (full report on disk). |
221
+ | `performance_trace` *(superset)* | `Tracing.*` (+ `Page.reload`) | **Toolkit convenience.** A robust single-call trace: start → optional reload → capture for `durationMs` → end → write the trace JSON → return `{path,bytes,events,metrics}`. Preferred over the `start`/`stop` pair (CDP tracing is browser-global and bound to one connection). |
222
+ | `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`). |
223
+ | `list_mocks` *(superset)* | `Runtime.evaluate` (liveness probe) | Lists active mock sessions with rules + hit counts; prunes sessions whose tab closed. |
224
+ | `clear_mocks` *(superset)* | `Fetch.disable` | Tears down the resolved target's mock session (or all with `all:true`). |
225
+
226
+ ## How it's built
227
+
228
+ ```
229
+ src/
230
+ client.ts # CdpConnection, openPage/withPage/openBrowser, resolveTarget, timeouts
231
+ types.ts # Target, TargetSelector, Uid, CDP envelopes
232
+ index.ts # TOOLS registry (33) + re-exported client primitives
233
+ cli.ts # the Bun CLI
234
+ mcp.ts # stdio MCP server (exposes TOOLS via @modelcontextprotocol/sdk)
235
+ manifest.ts # JSON Schemas advertised by the MCP server (one per tool)
236
+ tools/
237
+ pages.ts navigation.ts evaluate.ts snapshot.ts input.ts
238
+ screenshot.ts emulation.ts dialogs.ts recorder.ts console.ts
239
+ network.ts performance.ts heap.ts lighthouse.ts network_mock.ts
240
+ test/
241
+ smoke.ts # safe end-to-end tool smoke (bun run smoke)
242
+ mock-smoke.ts # network-mocking end-to-end smoke (bun run mock:smoke)
243
+ network_mock.test.ts # pure-logic unit tests (bun test)
244
+ mcp-smoke.ts # MCP handshake + live tools/call round-trip (bun run mcp:smoke)
245
+ ```
246
+
247
+ Each module ends with a footer comment listing the exact CDP methods it uses and its parity gaps. The full design contract lives in [`CONTRACT.md`](./CONTRACT.md).
248
+
249
+ ## Contributing
250
+
251
+ Issues and PRs welcome — see [`CONTRIBUTING.md`](./CONTRIBUTING.md) and the [`good first issue`](https://github.com/sblattj/cdp-toolkit/labels/good%20first%20issue) label. By participating you agree to the [Code of Conduct](./CODE_OF_CONDUCT.md).
252
+
253
+ ## License
254
+
255
+ [MIT](./LICENSE) © Stephen Blatt