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/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cdp-toolkit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight chrome-devtools-mcp alternative: a single-target Chrome DevTools Protocol MCP server + CLI that drives one tab over a direct WebSocket with a bounded timeout on every call, so a stuck page can't wedge your agent or force a /mcp restart. 33 tools including built-in network mocking. No Puppeteer, no all-target fan-out.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Stephen Blatt",
|
|
9
|
+
"url": "https://github.com/sblattj"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/sblattj/cdp-toolkit#readme",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/sblattj/cdp-toolkit.git"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/sblattj/cdp-toolkit/issues"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"bun": ">=1.1.0",
|
|
21
|
+
"node": ">=22.0.0"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"mcp",
|
|
25
|
+
"mcp-server",
|
|
26
|
+
"model-context-protocol",
|
|
27
|
+
"chrome-devtools-mcp",
|
|
28
|
+
"chrome-devtools-mcp-alternative",
|
|
29
|
+
"chrome-devtools-protocol",
|
|
30
|
+
"cdp",
|
|
31
|
+
"browser-automation",
|
|
32
|
+
"web-automation",
|
|
33
|
+
"headless-chrome",
|
|
34
|
+
"chrome-automation",
|
|
35
|
+
"network-mocking",
|
|
36
|
+
"api-mocking",
|
|
37
|
+
"claude-code",
|
|
38
|
+
"claude",
|
|
39
|
+
"cursor",
|
|
40
|
+
"windsurf",
|
|
41
|
+
"ai-agent",
|
|
42
|
+
"llm-tools",
|
|
43
|
+
"puppeteer-alternative",
|
|
44
|
+
"devtools",
|
|
45
|
+
"bun"
|
|
46
|
+
],
|
|
47
|
+
"main": "./dist/index.js",
|
|
48
|
+
"exports": {
|
|
49
|
+
".": "./dist/index.js",
|
|
50
|
+
"./package.json": "./package.json"
|
|
51
|
+
},
|
|
52
|
+
"bin": {
|
|
53
|
+
"cdp-toolkit": "./dist/mcp.js",
|
|
54
|
+
"cdp": "./dist/cli.js"
|
|
55
|
+
},
|
|
56
|
+
"files": [
|
|
57
|
+
"dist/",
|
|
58
|
+
"src/",
|
|
59
|
+
"CONTRACT.md",
|
|
60
|
+
"README.md",
|
|
61
|
+
"CHANGELOG.md",
|
|
62
|
+
"LICENSE"
|
|
63
|
+
],
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "bun run scripts/build.ts",
|
|
66
|
+
"prepublishOnly": "bun run build",
|
|
67
|
+
"cdp": "bun run ./src/cli.ts",
|
|
68
|
+
"mcp": "bun run ./src/mcp.ts",
|
|
69
|
+
"typecheck": "tsc --noEmit",
|
|
70
|
+
"test": "bun test",
|
|
71
|
+
"smoke": "bun run ./test/smoke.ts",
|
|
72
|
+
"mock:smoke": "bun run ./test/mock-smoke.ts",
|
|
73
|
+
"mcp:smoke": "bun run ./test/mcp-smoke.ts"
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@types/node": "22.10.10",
|
|
77
|
+
"typescript": "5.7.3"
|
|
78
|
+
},
|
|
79
|
+
"dependencies": {
|
|
80
|
+
"@modelcontextprotocol/sdk": "1.29.0"
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* cdp-toolkit CLI.
|
|
4
|
+
*
|
|
5
|
+
* bun run src/cli.ts <tool> [--target <sel>] [--json '<obj>'] [--<key> <value> ...]
|
|
6
|
+
* bun run src/cli.ts --list
|
|
7
|
+
*
|
|
8
|
+
* Argv parsing rules (see CONTRACT.md "Integration"):
|
|
9
|
+
* - First positional token is the tool name (a key of TOOLS).
|
|
10
|
+
* - `--json '<obj>'` parses a JSON object and merges it into the args object.
|
|
11
|
+
* - `--target <sel>` sets `args.target` (a TargetSelector string).
|
|
12
|
+
* - Any other `--key value` pair becomes `args.key`, with the value coerced:
|
|
13
|
+
* "true"/"false" -> boolean, a numeric string -> number, else the raw
|
|
14
|
+
* string. A bare `--flag` with no following value (or followed by another
|
|
15
|
+
* --flag) is treated as boolean true.
|
|
16
|
+
* - Explicit `--key` pairs OVERRIDE keys merged from `--json` (last writer
|
|
17
|
+
* wins; --json is applied first, then the individual flags).
|
|
18
|
+
*
|
|
19
|
+
* Output:
|
|
20
|
+
* - success -> JSON.stringify(result, null, 2) to stdout, exit 0.
|
|
21
|
+
* - throw -> JSON.stringify({ error: message }) to stderr, exit 1.
|
|
22
|
+
* - no tool -> usage message to stderr, exit 1.
|
|
23
|
+
* - --list -> all tool names (one per line) to stdout, exit 0.
|
|
24
|
+
*/
|
|
25
|
+
import { TOOLS, TOOL_NAMES, type ToolName } from "./index.ts";
|
|
26
|
+
|
|
27
|
+
const USAGE = `cdp-toolkit — raw single-target CDP, 29-tool chrome-devtools-mcp parity.
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
bun run src/cli.ts <tool> [--target <sel>] [--json '<obj>'] [--<key> <value> ...]
|
|
31
|
+
bun run src/cli.ts --list
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
bun run src/cli.ts list_pages
|
|
35
|
+
bun run src/cli.ts navigate_page --target index:0 --url https://example.com
|
|
36
|
+
bun run src/cli.ts click --target index:0 --uid 42
|
|
37
|
+
bun run src/cli.ts evaluate_script --json '{"expression":"1+2"}'
|
|
38
|
+
bun run src/cli.ts take_screenshot --target url:example --fullPage true
|
|
39
|
+
|
|
40
|
+
Target selector grammar: active | index:N | url:<substr> | title:<substr> | <targetId>
|
|
41
|
+
Run with --list to print every available tool name.`;
|
|
42
|
+
|
|
43
|
+
/** Coerce a raw CLI string into boolean / number / string. */
|
|
44
|
+
function coerce(raw: string): boolean | number | string {
|
|
45
|
+
if (raw === "true") return true;
|
|
46
|
+
if (raw === "false") return false;
|
|
47
|
+
// Numbers: only when the trimmed string round-trips through Number cleanly.
|
|
48
|
+
if (raw.trim() !== "" && !Number.isNaN(Number(raw)) && /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(raw.trim())) {
|
|
49
|
+
return Number(raw);
|
|
50
|
+
}
|
|
51
|
+
return raw;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface ParsedArgs {
|
|
55
|
+
tool?: string;
|
|
56
|
+
list: boolean;
|
|
57
|
+
args: Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Parse process argv (already sliced to drop the runtime + script). */
|
|
61
|
+
function parseArgv(argv: string[]): ParsedArgs {
|
|
62
|
+
const out: ParsedArgs = { list: false, args: {} };
|
|
63
|
+
// Collect flag pairs separately so --json can be merged BEFORE individual
|
|
64
|
+
// flags override it, regardless of token order.
|
|
65
|
+
let jsonObj: Record<string, unknown> | undefined;
|
|
66
|
+
const flagPairs: Array<[string, unknown]> = [];
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < argv.length; i++) {
|
|
69
|
+
const token = argv[i]!;
|
|
70
|
+
if (token === "--list") {
|
|
71
|
+
out.list = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (token.startsWith("--")) {
|
|
75
|
+
const key = token.slice(2);
|
|
76
|
+
const next = argv[i + 1];
|
|
77
|
+
// A value follows unless we're at the end or the next token is another flag.
|
|
78
|
+
const hasValue = next !== undefined && !next.startsWith("--");
|
|
79
|
+
const rawValue = hasValue ? next! : "true";
|
|
80
|
+
if (hasValue) i++;
|
|
81
|
+
|
|
82
|
+
if (key === "json") {
|
|
83
|
+
const parsed = JSON.parse(rawValue) as unknown;
|
|
84
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
85
|
+
throw new Error("--json must be a JSON object");
|
|
86
|
+
}
|
|
87
|
+
jsonObj = parsed as Record<string, unknown>;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (key === "target") {
|
|
91
|
+
flagPairs.push(["target", rawValue]);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
flagPairs.push([key, hasValue ? coerce(rawValue) : true]);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
// First non-flag positional is the tool name; ignore any extras.
|
|
98
|
+
if (out.tool === undefined) out.tool = token;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --json first, then explicit flags win.
|
|
102
|
+
if (jsonObj) Object.assign(out.args, jsonObj);
|
|
103
|
+
for (const [k, v] of flagPairs) out.args[k] = v;
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isToolName(name: string): name is ToolName {
|
|
108
|
+
return Object.prototype.hasOwnProperty.call(TOOLS, name);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function main(): Promise<number> {
|
|
112
|
+
const parsed = parseArgv(process.argv.slice(2));
|
|
113
|
+
|
|
114
|
+
if (parsed.list) {
|
|
115
|
+
process.stdout.write(`${TOOL_NAMES.join("\n")}\n`);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!parsed.tool) {
|
|
120
|
+
process.stderr.write(`${USAGE}\n`);
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!isToolName(parsed.tool)) {
|
|
125
|
+
process.stderr.write(
|
|
126
|
+
`${JSON.stringify({ error: `unknown tool '${parsed.tool}'. Run --list to see all ${TOOL_NAMES.length} tools.` })}\n`,
|
|
127
|
+
);
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const fn = TOOLS[parsed.tool];
|
|
132
|
+
// The registry's value type is the union of all tool signatures; each tool
|
|
133
|
+
// validates its own args at runtime, so we hand the parsed object through.
|
|
134
|
+
const result = await (fn as (args: unknown) => Promise<unknown>)(parsed.args);
|
|
135
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
main()
|
|
140
|
+
.then((code) => process.exit(code))
|
|
141
|
+
.catch((err: unknown) => {
|
|
142
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
143
|
+
process.stderr.write(`${JSON.stringify({ error: message })}\n`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
});
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core raw-CDP client. Zero runtime dependencies — uses Node's global
|
|
3
|
+
* WebSocket (Node >= 22; verified on Node 25.9). Every tool module is built on
|
|
4
|
+
* the primitives exported here. See CONTRACT.md.
|
|
5
|
+
*/
|
|
6
|
+
import type { CdpResponse, Target, TargetSelector } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
/** Base HTTP origin of the DevTools endpoint. Override with CDP_BASE. */
|
|
9
|
+
export const BASE = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
10
|
+
|
|
11
|
+
/** Default per-command timeout (ms). Override with CDP_TIMEOUT_MS. */
|
|
12
|
+
export const DEFAULT_TIMEOUT_MS = Number(process.env.CDP_TIMEOUT_MS ?? 15_000);
|
|
13
|
+
|
|
14
|
+
export class CdpError extends Error {
|
|
15
|
+
constructor(
|
|
16
|
+
message: string,
|
|
17
|
+
readonly code?: number,
|
|
18
|
+
readonly data?: unknown,
|
|
19
|
+
) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "CdpError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type EventHandler = (params: Record<string, unknown>, sessionId?: string) => void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A single CDP WebSocket connection (to either the browser endpoint or a page
|
|
29
|
+
* target endpoint). Correlates id->response, fans out events to subscribers,
|
|
30
|
+
* and enforces a per-command timeout so a wedged renderer can never hang a
|
|
31
|
+
* caller indefinitely (the exact failure mode that breaks the MCP layer).
|
|
32
|
+
*/
|
|
33
|
+
export class CdpConnection {
|
|
34
|
+
private ws?: WebSocket;
|
|
35
|
+
private nextId = 0;
|
|
36
|
+
private readonly pending = new Map<
|
|
37
|
+
number,
|
|
38
|
+
{ resolve: (r: CdpResponse) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }
|
|
39
|
+
>();
|
|
40
|
+
private readonly listeners = new Map<string, Set<EventHandler>>();
|
|
41
|
+
private closed = false;
|
|
42
|
+
|
|
43
|
+
constructor(
|
|
44
|
+
readonly wsUrl: string,
|
|
45
|
+
private readonly opts: { timeoutMs?: number } = {},
|
|
46
|
+
) {}
|
|
47
|
+
|
|
48
|
+
connect(): Promise<this> {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const ws = new WebSocket(this.wsUrl);
|
|
51
|
+
this.ws = ws;
|
|
52
|
+
const openTimer = setTimeout(() => reject(new CdpError(`connect timeout: ${this.wsUrl}`)), this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
53
|
+
ws.onopen = () => {
|
|
54
|
+
clearTimeout(openTimer);
|
|
55
|
+
resolve(this);
|
|
56
|
+
};
|
|
57
|
+
ws.onerror = (e: Event) => {
|
|
58
|
+
clearTimeout(openTimer);
|
|
59
|
+
const msg = (e as ErrorEvent)?.message ?? "websocket error";
|
|
60
|
+
if (this.pending.size === 0) reject(new CdpError(msg));
|
|
61
|
+
this.rejectAll(new CdpError(msg));
|
|
62
|
+
};
|
|
63
|
+
ws.onclose = () => {
|
|
64
|
+
this.closed = true;
|
|
65
|
+
this.rejectAll(new CdpError("connection closed"));
|
|
66
|
+
};
|
|
67
|
+
ws.onmessage = (ev: MessageEvent) => this.onMessage(String(ev.data));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private onMessage(raw: string): void {
|
|
72
|
+
let msg: CdpResponse & { method?: string; params?: Record<string, unknown>; sessionId?: string };
|
|
73
|
+
try {
|
|
74
|
+
msg = JSON.parse(raw);
|
|
75
|
+
} catch {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (typeof msg.id === "number" && this.pending.has(msg.id)) {
|
|
79
|
+
const p = this.pending.get(msg.id)!;
|
|
80
|
+
clearTimeout(p.timer);
|
|
81
|
+
this.pending.delete(msg.id);
|
|
82
|
+
p.resolve(msg);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (msg.method) {
|
|
86
|
+
const set = this.listeners.get(msg.method);
|
|
87
|
+
if (set) for (const h of set) h(msg.params ?? {}, msg.sessionId);
|
|
88
|
+
const star = this.listeners.get("*");
|
|
89
|
+
if (star) for (const h of star) h({ method: msg.method, ...(msg.params ?? {}) }, msg.sessionId);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private rejectAll(err: Error): void {
|
|
94
|
+
for (const [, p] of this.pending) {
|
|
95
|
+
clearTimeout(p.timer);
|
|
96
|
+
p.reject(err);
|
|
97
|
+
}
|
|
98
|
+
this.pending.clear();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Send a CDP command and await its result. Rejects on CDP error or timeout. */
|
|
102
|
+
send<T = Record<string, unknown>>(
|
|
103
|
+
method: string,
|
|
104
|
+
params: Record<string, unknown> = {},
|
|
105
|
+
opts: { timeoutMs?: number; sessionId?: string } = {},
|
|
106
|
+
): Promise<T> {
|
|
107
|
+
if (this.closed || !this.ws) return Promise.reject(new CdpError("connection not open"));
|
|
108
|
+
const id = ++this.nextId;
|
|
109
|
+
const timeoutMs = opts.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
110
|
+
const payload: Record<string, unknown> = { id, method, params };
|
|
111
|
+
if (opts.sessionId) payload.sessionId = opts.sessionId;
|
|
112
|
+
return new Promise<T>((resolve, reject) => {
|
|
113
|
+
const timer = setTimeout(() => {
|
|
114
|
+
this.pending.delete(id);
|
|
115
|
+
reject(new CdpError(`'${method}' timed out after ${timeoutMs}ms`));
|
|
116
|
+
}, timeoutMs);
|
|
117
|
+
this.pending.set(id, {
|
|
118
|
+
resolve: (r) => {
|
|
119
|
+
if (r.error) reject(new CdpError(`${method}: ${r.error.message}`, r.error.code, r.error.data));
|
|
120
|
+
else resolve((r.result ?? {}) as T);
|
|
121
|
+
},
|
|
122
|
+
reject,
|
|
123
|
+
timer,
|
|
124
|
+
});
|
|
125
|
+
this.ws!.send(JSON.stringify(payload));
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Subscribe to a CDP event method (or "*" for all). Returns an unsubscribe fn. */
|
|
130
|
+
on(method: string, handler: EventHandler): () => void {
|
|
131
|
+
let set = this.listeners.get(method);
|
|
132
|
+
if (!set) {
|
|
133
|
+
set = new Set();
|
|
134
|
+
this.listeners.set(method, set);
|
|
135
|
+
}
|
|
136
|
+
set.add(handler);
|
|
137
|
+
return () => set!.delete(handler);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resolve when an event matching `method` (and optional predicate) fires, or
|
|
142
|
+
* reject on timeout. Useful for navigation/dialog/load synchronization.
|
|
143
|
+
*/
|
|
144
|
+
waitFor<P = Record<string, unknown>>(
|
|
145
|
+
method: string,
|
|
146
|
+
predicate?: (params: P) => boolean,
|
|
147
|
+
timeoutMs = this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
148
|
+
): Promise<P> {
|
|
149
|
+
return new Promise<P>((resolve, reject) => {
|
|
150
|
+
const timer = setTimeout(() => {
|
|
151
|
+
off();
|
|
152
|
+
reject(new CdpError(`waitFor('${method}') timed out after ${timeoutMs}ms`));
|
|
153
|
+
}, timeoutMs);
|
|
154
|
+
const off = this.on(method, (params) => {
|
|
155
|
+
if (!predicate || predicate(params as P)) {
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
off();
|
|
158
|
+
resolve(params as P);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
close(): void {
|
|
165
|
+
this.closed = true;
|
|
166
|
+
this.rejectAll(new CdpError("closed by caller"));
|
|
167
|
+
try {
|
|
168
|
+
this.ws?.close();
|
|
169
|
+
} catch {
|
|
170
|
+
/* ignore */
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ----------------------------- endpoint discovery ----------------------------- */
|
|
176
|
+
|
|
177
|
+
async function httpJson<T>(path: string): Promise<T> {
|
|
178
|
+
const res = await fetch(`${BASE}${path}`);
|
|
179
|
+
if (!res.ok) throw new CdpError(`${path} -> HTTP ${res.status}`);
|
|
180
|
+
return (await res.json()) as T;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** All targets (GET /json/list). */
|
|
184
|
+
export function listTargets(): Promise<Target[]> {
|
|
185
|
+
return httpJson<Target[]>("/json/list");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Browser-level WebSocket URL (GET /json/version) — for Target.* / Browser.*. */
|
|
189
|
+
export async function browserWsUrl(): Promise<string> {
|
|
190
|
+
const v = await httpJson<{ webSocketDebuggerUrl: string }>("/json/version");
|
|
191
|
+
return v.webSocketDebuggerUrl;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Resolve a TargetSelector to a concrete page target. See types.ts for grammar. */
|
|
195
|
+
export async function resolveTarget(selector: TargetSelector): Promise<Target> {
|
|
196
|
+
const targets = await listTargets();
|
|
197
|
+
const pages = targets.filter((t) => t.type === "page");
|
|
198
|
+
if (!selector || selector === "active") {
|
|
199
|
+
if (!pages.length) throw new CdpError("no page targets open");
|
|
200
|
+
return pages[0]!;
|
|
201
|
+
}
|
|
202
|
+
if (selector.startsWith("index:")) {
|
|
203
|
+
const i = Number(selector.slice(6));
|
|
204
|
+
if (!pages[i]) throw new CdpError(`no page target at index ${i} (have ${pages.length})`);
|
|
205
|
+
return pages[i]!;
|
|
206
|
+
}
|
|
207
|
+
if (selector.startsWith("url:")) {
|
|
208
|
+
const needle = selector.slice(4);
|
|
209
|
+
const hit = pages.find((t) => t.url.includes(needle));
|
|
210
|
+
if (!hit) throw new CdpError(`no page url containing '${needle}'`);
|
|
211
|
+
return hit;
|
|
212
|
+
}
|
|
213
|
+
if (selector.startsWith("title:")) {
|
|
214
|
+
const needle = selector.slice(6);
|
|
215
|
+
const hit = pages.find((t) => t.title.includes(needle));
|
|
216
|
+
if (!hit) throw new CdpError(`no page title containing '${needle}'`);
|
|
217
|
+
return hit;
|
|
218
|
+
}
|
|
219
|
+
// bare id
|
|
220
|
+
const byId = targets.find((t) => t.id === selector);
|
|
221
|
+
if (!byId) throw new CdpError(`no target with id '${selector}'`);
|
|
222
|
+
return byId;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Open a connection to the browser endpoint (Target.* / Browser.* domains). */
|
|
226
|
+
export async function openBrowser(opts: { timeoutMs?: number } = {}): Promise<CdpConnection> {
|
|
227
|
+
return new CdpConnection(await browserWsUrl(), opts).connect();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Open a connection to a page target. Returns the connection and the target. */
|
|
231
|
+
export async function openPage(
|
|
232
|
+
selector: TargetSelector,
|
|
233
|
+
opts: { timeoutMs?: number } = {},
|
|
234
|
+
): Promise<{ conn: CdpConnection; target: Target }> {
|
|
235
|
+
const target = await resolveTarget(selector);
|
|
236
|
+
const conn = await new CdpConnection(target.webSocketDebuggerUrl, opts).connect();
|
|
237
|
+
return { conn, target };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Convenience: open a page, run `fn`, and always close the connection.
|
|
242
|
+
* The standard wrapper for stateless one-shot tools.
|
|
243
|
+
*/
|
|
244
|
+
export async function withPage<T>(
|
|
245
|
+
selector: TargetSelector,
|
|
246
|
+
fn: (conn: CdpConnection, target: Target) => Promise<T>,
|
|
247
|
+
opts: { timeoutMs?: number } = {},
|
|
248
|
+
): Promise<T> {
|
|
249
|
+
const { conn, target } = await openPage(selector, opts);
|
|
250
|
+
try {
|
|
251
|
+
return await fn(conn, target);
|
|
252
|
+
} finally {
|
|
253
|
+
conn.close();
|
|
254
|
+
}
|
|
255
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cdp-toolkit public surface.
|
|
3
|
+
*
|
|
4
|
+
* Two exports:
|
|
5
|
+
* 1. `TOOLS` — the registry mapping every chrome-devtools-mcp tool name
|
|
6
|
+
* (snake_case string, all 29) to its raw-CDP implementation function. The
|
|
7
|
+
* CLI and any embedder dispatch through this single table.
|
|
8
|
+
* 2. The client primitives, re-exported so consumers can build their own
|
|
9
|
+
* flows on the same connection/timeout machinery the tools use.
|
|
10
|
+
*
|
|
11
|
+
* Every tool fn takes a single typed args object and returns a JSON-serializable
|
|
12
|
+
* value (or throws). See CONTRACT.md for the design rules each module follows.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// --- pages (browser endpoint: Target.*) ---
|
|
16
|
+
import { listPages, newPage, closePage, selectPage } from "./tools/pages.ts";
|
|
17
|
+
// --- navigation ---
|
|
18
|
+
import { navigatePage, waitForText } from "./tools/navigation.ts";
|
|
19
|
+
// --- evaluate ---
|
|
20
|
+
import { evaluateScript } from "./tools/evaluate.ts";
|
|
21
|
+
// --- snapshot (a11y tree + shared resolveUid helper) ---
|
|
22
|
+
import { takeSnapshot } from "./tools/snapshot.ts";
|
|
23
|
+
// --- input / interaction ---
|
|
24
|
+
import { click, hover, drag, fill, fillForm, typeText, pressKey, uploadFile } from "./tools/input.ts";
|
|
25
|
+
// --- screenshot + emulation ---
|
|
26
|
+
import { takeScreenshot } from "./tools/screenshot.ts";
|
|
27
|
+
import { emulate, resizePage } from "./tools/emulation.ts";
|
|
28
|
+
// --- dialogs ---
|
|
29
|
+
import { handleDialog } from "./tools/dialogs.ts";
|
|
30
|
+
// --- console + network (read the recorder buffer) ---
|
|
31
|
+
import { listConsoleMessages, getConsoleMessage } from "./tools/console.ts";
|
|
32
|
+
import { listNetworkRequests, getNetworkRequest } from "./tools/network.ts";
|
|
33
|
+
// --- performance ---
|
|
34
|
+
import {
|
|
35
|
+
performanceStartTrace,
|
|
36
|
+
performanceStopTrace,
|
|
37
|
+
performanceAnalyzeInsight,
|
|
38
|
+
performanceTrace,
|
|
39
|
+
} from "./tools/performance.ts";
|
|
40
|
+
// --- heap ---
|
|
41
|
+
import { takeHeapsnapshot } from "./tools/heap.ts";
|
|
42
|
+
// --- lighthouse (the sole non-CDP tool) ---
|
|
43
|
+
import { lighthouseAudit } from "./tools/lighthouse.ts";
|
|
44
|
+
// --- network mocking (Fetch domain) — toolkit addition beyond the 29 parity tools ---
|
|
45
|
+
import { mockRequest, listMocks, clearMocks } from "./tools/network_mock.ts";
|
|
46
|
+
|
|
47
|
+
/** A toolkit tool: a single typed-args function returning JSON-serializable data. */
|
|
48
|
+
export type ToolFn = (args: never) => Promise<unknown>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The complete chrome-devtools-mcp parity surface: all 29 MCP tools keyed by
|
|
52
|
+
* their canonical MCP (snake_case) name, plus one convenience superset tool
|
|
53
|
+
* (`performance_trace`, a robust single-call trace — the start/stop pair cannot
|
|
54
|
+
* span two stateless CLI processes), plus a 3-tool network-mocking group
|
|
55
|
+
* (mock_request/list_mocks/clear_mocks — a persistent per-target fake backend).
|
|
56
|
+
* 33 entries total. Listed explicitly so the mapping is auditable at a glance and
|
|
57
|
+
* the CLI can `--list` it.
|
|
58
|
+
*/
|
|
59
|
+
export const TOOLS = {
|
|
60
|
+
// pages (4)
|
|
61
|
+
list_pages: listPages,
|
|
62
|
+
new_page: newPage,
|
|
63
|
+
close_page: closePage,
|
|
64
|
+
select_page: selectPage,
|
|
65
|
+
// navigation (2)
|
|
66
|
+
navigate_page: navigatePage,
|
|
67
|
+
wait_for: waitForText,
|
|
68
|
+
// evaluate (1)
|
|
69
|
+
evaluate_script: evaluateScript,
|
|
70
|
+
// snapshot (1)
|
|
71
|
+
take_snapshot: takeSnapshot,
|
|
72
|
+
// interaction (8)
|
|
73
|
+
click: click,
|
|
74
|
+
hover: hover,
|
|
75
|
+
drag: drag,
|
|
76
|
+
fill: fill,
|
|
77
|
+
fill_form: fillForm,
|
|
78
|
+
type_text: typeText,
|
|
79
|
+
press_key: pressKey,
|
|
80
|
+
upload_file: uploadFile,
|
|
81
|
+
// screenshot + emulation (3)
|
|
82
|
+
take_screenshot: takeScreenshot,
|
|
83
|
+
emulate: emulate,
|
|
84
|
+
resize_page: resizePage,
|
|
85
|
+
// dialogs (1)
|
|
86
|
+
handle_dialog: handleDialog,
|
|
87
|
+
// console + network (4)
|
|
88
|
+
list_console_messages: listConsoleMessages,
|
|
89
|
+
get_console_message: getConsoleMessage,
|
|
90
|
+
list_network_requests: listNetworkRequests,
|
|
91
|
+
get_network_request: getNetworkRequest,
|
|
92
|
+
// performance (3 MCP + 1 convenience one-shot)
|
|
93
|
+
performance_start_trace: performanceStartTrace,
|
|
94
|
+
performance_stop_trace: performanceStopTrace,
|
|
95
|
+
performance_analyze_insight: performanceAnalyzeInsight,
|
|
96
|
+
performance_trace: performanceTrace,
|
|
97
|
+
// heap (1)
|
|
98
|
+
take_heapsnapshot: takeHeapsnapshot,
|
|
99
|
+
// lighthouse (1) — the only non-CDP tool
|
|
100
|
+
lighthouse_audit: lighthouseAudit,
|
|
101
|
+
// network mocking (3) — persistent per-target fake backend (toolkit addition, Fetch domain)
|
|
102
|
+
mock_request: mockRequest,
|
|
103
|
+
list_mocks: listMocks,
|
|
104
|
+
clear_mocks: clearMocks,
|
|
105
|
+
} satisfies Record<string, (args: never) => Promise<unknown>>;
|
|
106
|
+
|
|
107
|
+
/** Canonical MCP tool name accepted by the registry. */
|
|
108
|
+
export type ToolName = keyof typeof TOOLS;
|
|
109
|
+
|
|
110
|
+
/** All tool names (handy for `--list` and validation). */
|
|
111
|
+
export const TOOL_NAMES = Object.keys(TOOLS) as ToolName[];
|
|
112
|
+
|
|
113
|
+
/* ----------------------------- client primitives ----------------------------- */
|
|
114
|
+
|
|
115
|
+
export {
|
|
116
|
+
BASE,
|
|
117
|
+
DEFAULT_TIMEOUT_MS,
|
|
118
|
+
CdpError,
|
|
119
|
+
CdpConnection,
|
|
120
|
+
listTargets,
|
|
121
|
+
browserWsUrl,
|
|
122
|
+
resolveTarget,
|
|
123
|
+
openBrowser,
|
|
124
|
+
openPage,
|
|
125
|
+
withPage,
|
|
126
|
+
} from "./client.ts";
|
|
127
|
+
|
|
128
|
+
export type { Target, TargetSelector, Uid, CdpResponse, CdpEvent, ToolResult } from "./types.ts";
|