cdp-toolkit 1.11.0 → 2.1.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 +71 -0
- package/README.md +76 -7
- package/dist/cli.js +331 -299
- package/dist/index.js +11 -11
- package/dist/mcp.js +1002 -336
- package/package.json +3 -2
- package/src/bidi/driver.ts +29 -1
- package/src/bidi/launch.ts +51 -1
- package/src/manifest.ts +297 -297
- package/src/mcp.ts +187 -55
- package/src/toolDocs.ts +468 -0
- package/src/toolGroups.ts +81 -0
- package/src/tools/navigation.ts +4 -2
- package/src/version.ts +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cdp-toolkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "A lightweight chrome-devtools-mcp alternative: an MCP server + CLI that drives the tabs you name, one target per call over one direct WebSocket, with a bounded timeout on every call, so a stuck page can't wedge your agent or force a /mcp restart. Chrome over CDP and Firefox over WebDriver BiDi, with tools a backend can't run absent from tools/list rather than throwing mid-task. 45 tools including built-in network mocking, a cookie group, tab leases with human-activity detection, real HTML5 drag-and-drop, raw mouse/scroll dispatch, downloads, permission grants, and tab-to-video screen recording so several agents can drive one browser without stealing each other's tabs or a human's. No Puppeteer, no all-target fan-out.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -86,10 +86,11 @@
|
|
|
86
86
|
"extension:smoke": "bun run ./test/extension-smoke.ts"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
|
+
"@modelcontextprotocol/client": "^2.0.0",
|
|
89
90
|
"@types/node": "22.10.10",
|
|
90
91
|
"typescript": "5.7.3"
|
|
91
92
|
},
|
|
92
93
|
"dependencies": {
|
|
93
|
-
"@modelcontextprotocol/
|
|
94
|
+
"@modelcontextprotocol/server": "^2.0.0"
|
|
94
95
|
}
|
|
95
96
|
}
|
package/src/bidi/driver.ts
CHANGED
|
@@ -136,6 +136,20 @@ function mapBidiError(e: unknown): DriverError {
|
|
|
136
136
|
if (code === "unsupported operation") return driverError("unsupported", e.message);
|
|
137
137
|
if (!code && /timed out/i.test(e.message)) return driverError("timeout", e.message);
|
|
138
138
|
if (!code && /(disposed|not open|connection closed)/i.test(e.message)) return driverError("disconnected", e.message);
|
|
139
|
+
// NS_ERROR_FILE_ACCESS_DENIED from browsingContext.navigate means the browser process was
|
|
140
|
+
// refused at the OS level while opening the file:// target — not a protocol failure. Two
|
|
141
|
+
// mechanisms produce it (#6): (1) the automation prefs that relax file:// handling were never
|
|
142
|
+
// applied — launch.ts now seeds them via user.js, so this should not surface for a
|
|
143
|
+
// toolkit-launched Firefox; (2) kernel-level confinement of the browser itself (Ubuntu's snap
|
|
144
|
+
// Firefox is the common one: a snap process cannot see host paths outside its mount
|
|
145
|
+
// namespace), which no preference can bypass. The error must name the way out rather than
|
|
146
|
+
// relay the raw nsresult.
|
|
147
|
+
if (/NS_ERROR_FILE_ACCESS_DENIED/i.test(e.message)) {
|
|
148
|
+
return driverError(
|
|
149
|
+
"page-error",
|
|
150
|
+
`${e.message} — the browser process was not allowed to open this file:// path. For a snap- or AppArmor-confined Firefox (Ubuntu default) that is kernel-level and no preference fixes it: serve the directory over a local http server (e.g. python3 -m http.server) and navigate to the http://127.0.0.1:<port>/ URL instead. If this Firefox was launched by cdp-toolkit and still fails, please report it at https://github.com/sblattj/cdp-toolkit/issues.`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
139
153
|
return driverError("page-error", e.message);
|
|
140
154
|
}
|
|
141
155
|
return driverError("page-error", e instanceof Error ? e.message : String(e));
|
|
@@ -655,7 +669,21 @@ class BidiPageDriver implements PageDriver {
|
|
|
655
669
|
const sendOpts = opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {};
|
|
656
670
|
try {
|
|
657
671
|
if (reload) {
|
|
658
|
-
|
|
672
|
+
// Firefox rejects ANY `ignoreCache` argument to browsingContext.reload — even an
|
|
673
|
+
// explicit `false` — with `Argument "ignoreCache" is not supported yet` (observed on
|
|
674
|
+
// Firefox 153.0.3): the server-side validator refuses the key unconditionally, it does
|
|
675
|
+
// not read its value. So the key is OMITTED from the params unless a hard reload was
|
|
676
|
+
// actually requested, and a requested hard reload fails fast HERE with a
|
|
677
|
+
// backend-capability error instead of an opaque mid-flight protocol rejection — the
|
|
678
|
+
// same doctrine as click()'s modifiers on this backend. If a future Firefox starts
|
|
679
|
+
// honoring the argument, re-verify and lift the guard.
|
|
680
|
+
if (opts.ignoreCache === true) {
|
|
681
|
+
throw driverError(
|
|
682
|
+
"unsupported",
|
|
683
|
+
"navigate: ignoreCache:true (hard reload) is not supported on the Firefox WebDriver-BiDi backend — Firefox rejects the ignoreCache argument outright. A plain reload (reload:true, no ignoreCache) works.",
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
const r = await this.conn.send("browsingContext.reload", { context: this.contextId, wait }, sendOpts);
|
|
659
687
|
return { url: r.url, contextId: this.contextId, reloaded: true, waitedFor: waitUntil };
|
|
660
688
|
}
|
|
661
689
|
if (history) {
|
package/src/bidi/launch.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* node:os and node:path.
|
|
23
23
|
*/
|
|
24
24
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
25
|
-
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
25
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
26
26
|
import { createConnection, createServer } from "node:net";
|
|
27
27
|
import { tmpdir } from "node:os";
|
|
28
28
|
import { join } from "node:path";
|
|
@@ -145,6 +145,55 @@ function waitForExit(proc: ChildProcess, deadlineMs: number): Promise<void> {
|
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Automation prefs seeded into every THROWAWAY profile this toolkit launches (#6).
|
|
150
|
+
*
|
|
151
|
+
* Firefox's Remote Agent has its own recommended automation-pref bundle, but it is gated on
|
|
152
|
+
* `remote.prefs.recommended`, which DEFAULTS TO FALSE — a bare `--remote-debugging-port`
|
|
153
|
+
* launch therefore runs with consumer prefs (verified in mozilla-central's
|
|
154
|
+
* remote/shared/RecommendedPreferences.sys.mjs; `defineLazyPreferenceGetter(..., "remote.prefs.recommended", false)`).
|
|
155
|
+
* The two file://-relevant consequences, and the reason each pref is here:
|
|
156
|
+
*
|
|
157
|
+
* - `remote.prefs.recommended: true` — flips on Firefox's own bundle at BiDi session start. This
|
|
158
|
+
* is the same mechanism geckodriver/Marionette automation leans on.
|
|
159
|
+
* - `security.fileuri.strict_origin_policy: false` — a member of that bundle, also set explicitly
|
|
160
|
+
* so the behavior does not depend on the bundle's exact contents per Firefox version. Without
|
|
161
|
+
* it, file:// documents get unique opaque origins and sibling file:// subresources fail.
|
|
162
|
+
* - `dom.file.createInChild: true` — the bundle's file-API automation companion pref.
|
|
163
|
+
*
|
|
164
|
+
* Only the throwaway profile is seeded. An explicit `profilePath` belongs to its owner: writing
|
|
165
|
+
* prefs into someone's real profile (where `security.fileuri.strict_origin_policy: false` would
|
|
166
|
+
* persist as a security relaxation of their daily browsing) is not this toolkit's call to make.
|
|
167
|
+
*/
|
|
168
|
+
const LAUNCH_USER_JS_PREFS: Record<string, boolean> = {
|
|
169
|
+
"remote.prefs.recommended": true,
|
|
170
|
+
"security.fileuri.strict_origin_policy": false,
|
|
171
|
+
"dom.file.createInChild": true,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Seed `user.js` in a profile directory with the toolkit's automation prefs. Firefox reads
|
|
176
|
+
* user.js exactly once, at profile startup — which is why this runs BEFORE spawn, never after.
|
|
177
|
+
* Merges with an existing user.js (never rewrites a pref that file already sets, never drops a
|
|
178
|
+
* line it did not write) so the function is idempotent and safe on a reused profile dir.
|
|
179
|
+
* Exported for unit tests; returns the file's final content.
|
|
180
|
+
*/
|
|
181
|
+
export function writeLaunchUserJs(profilePath: string): string {
|
|
182
|
+
const target = join(profilePath, "user.js");
|
|
183
|
+
const existing = existsSync(target) ? readFileSync(target, "utf8") : "";
|
|
184
|
+
const present = new Set<string>();
|
|
185
|
+
for (const line of existing.split("\n")) {
|
|
186
|
+
const m = /^user_pref\("([^"]+)"\s*,/.exec(line.trim());
|
|
187
|
+
if (m) present.add(m[1]!);
|
|
188
|
+
}
|
|
189
|
+
const additions = Object.entries(LAUNCH_USER_JS_PREFS)
|
|
190
|
+
.filter(([k]) => !present.has(k))
|
|
191
|
+
.map(([k, v]) => `user_pref("${k}", ${JSON.stringify(v)});`);
|
|
192
|
+
const content = (existing.length > 0 ? existing.replace(/\n*$/, "\n") : "") + additions.join("\n") + (additions.length > 0 ? "\n" : "");
|
|
193
|
+
if (content !== existing) writeFileSync(target, content, "utf-8");
|
|
194
|
+
return content;
|
|
195
|
+
}
|
|
196
|
+
|
|
148
197
|
/**
|
|
149
198
|
* Launch Firefox with the BiDi debug port enabled and resolve once the port
|
|
150
199
|
* actually accepts a TCP connection. Never resolves on a fixed sleep.
|
|
@@ -156,6 +205,7 @@ export async function launchFirefox(opts: LaunchFirefoxOptions = {}): Promise<La
|
|
|
156
205
|
|
|
157
206
|
const isThrowaway = opts.profilePath === undefined;
|
|
158
207
|
const profilePath = opts.profilePath ?? mkdtempSync(join(tmpdir(), "cdp-toolkit-ff-profile-"));
|
|
208
|
+
if (isThrowaway) writeLaunchUserJs(profilePath);
|
|
159
209
|
|
|
160
210
|
const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
|
|
161
211
|
if (opts.headless) args.push("--headless");
|