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 +29 -0
- package/CONTRACT.md +116 -0
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/dist/cli.js +2108 -0
- package/dist/index.js +2019 -0
- package/dist/mcp.js +3056 -0
- package/package.json +82 -0
- package/src/cli.ts +145 -0
- package/src/client.ts +255 -0
- package/src/index.ts +128 -0
- package/src/manifest.ts +1030 -0
- package/src/mcp.ts +61 -0
- package/src/tools/console.ts +199 -0
- package/src/tools/dialogs.ts +236 -0
- package/src/tools/emulation.ts +212 -0
- package/src/tools/evaluate.ts +155 -0
- package/src/tools/heap.ts +109 -0
- package/src/tools/input.ts +399 -0
- package/src/tools/lighthouse.ts +285 -0
- package/src/tools/navigation.ts +202 -0
- package/src/tools/network.ts +275 -0
- package/src/tools/network_mock.ts +367 -0
- package/src/tools/pages.ts +159 -0
- package/src/tools/performance.ts +492 -0
- package/src/tools/recorder.ts +265 -0
- package/src/tools/screenshot.ts +180 -0
- package/src/tools/snapshot.ts +206 -0
- package/src/types.ts +63 -0
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
|
+
[](https://github.com/sblattj/cdp-toolkit/actions/workflows/ci.yml)
|
|
8
|
+
[](https://www.npmjs.com/package/cdp-toolkit)
|
|
9
|
+
[](./LICENSE)
|
|
10
|
+
[](https://modelcontextprotocol.io)
|
|
11
|
+
|
|
12
|
+
<!-- Demo: record with `vhs docs/demo.tape` (see docs/demo.tape) and commit docs/demo.gif -->
|
|
13
|
+

|
|
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
|