cdp-toolkit 1.9.0 → 1.9.1
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 +118 -0
- package/CONTRACT.md +3 -3
- package/README.md +21 -7
- package/dist/cli.js +73 -14
- package/dist/index.js +60 -12
- package/dist/mcp.js +92 -23
- package/package.json +1 -1
- package/src/bidi-tools.ts +21 -0
- package/src/cli.ts +4 -2
- package/src/manifest.ts +22 -10
- package/src/mcp.ts +1 -1
- package/src/tools/console.ts +24 -7
- package/src/tools/network.ts +21 -7
- package/src/tools/recorder.ts +110 -10
- package/src/types.ts +8 -6
- package/src/workers.ts +69 -7
package/dist/mcp.js
CHANGED
|
@@ -406,6 +406,12 @@ async function resolveLiveLabel(backend, label, liveIds) {
|
|
|
406
406
|
|
|
407
407
|
// src/workers.ts
|
|
408
408
|
var WORKER_SELECTOR_PREFIX = "worker:";
|
|
409
|
+
var WORKER_CAPABLE_TOOLS = [
|
|
410
|
+
"evaluate_script",
|
|
411
|
+
"list_network_requests",
|
|
412
|
+
"get_network_request",
|
|
413
|
+
"list_console_messages"
|
|
414
|
+
];
|
|
409
415
|
var WORKER_TARGET_TYPES = new Set(["service_worker", "shared_worker"]);
|
|
410
416
|
function isWorkerSelector(selector) {
|
|
411
417
|
return typeof selector === "string" && selector.startsWith(WORKER_SELECTOR_PREFIX);
|
|
@@ -424,8 +430,18 @@ function resolveWorkerTargets(targets, needle) {
|
|
|
424
430
|
};
|
|
425
431
|
}
|
|
426
432
|
var WORKER_EMPTY_NEEDLE_MESSAGE = "'worker:' needs a substring of the worker's url, e.g. worker:<extension-id> or worker:background.js";
|
|
427
|
-
var WORKER_SELECTOR_UNSUPPORTED_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, which this backend cannot address " + "(WebDriver BiDi has no extension service-worker target; Capability 'worker.targets' is Chrome-only). " + "Use --browser chrome to evaluate in an extension's background worker.";
|
|
428
|
-
var WORKER_SELECTOR_PAGE_ONLY_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, and this tool is page-only " + "(closing, selecting or releasing a worker is meaningless).
|
|
433
|
+
var WORKER_SELECTOR_UNSUPPORTED_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, which this backend cannot address " + "(WebDriver BiDi has no extension service-worker target; Capability 'worker.targets' is Chrome-only). " + "Use --browser chrome to evaluate in, or record console/network from, an extension's background worker.";
|
|
434
|
+
var WORKER_SELECTOR_PAGE_ONLY_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, and this tool is page-only " + "(closing, selecting or releasing a worker is meaningless). The tools that accept a worker: target are " + `${WORKER_CAPABLE_TOOLS.join(", ")}.`;
|
|
435
|
+
function workerWakeMisuseMessage(kind) {
|
|
436
|
+
if (kind === "not-a-worker-selector") {
|
|
437
|
+
return "'wake' only applies to a target of the form 'worker:<substring>'. A page is never asleep, and a bare " + "worker target id cannot be woken either: Chrome destroys the target when an MV3 worker is evicted and " + "mints a NEW id when it restarts, so only the url-substring form can name a worker that is not running.";
|
|
438
|
+
}
|
|
439
|
+
return "'wake' only applies when a capture is being started (reload:true, or includeBody with url). Reading an " + "existing buffer cannot use a woken worker: a restarted MV3 worker gets a NEW target id, and the buffer is " + "keyed by id, so the wake would hand back an empty capture. Re-run with reload:true to record a fresh one.";
|
|
440
|
+
}
|
|
441
|
+
function workerBufferReadMissMessage(needle, liveWorkers) {
|
|
442
|
+
const running = liveWorkers.length ? ` (running workers: ${urlList(liveWorkers)})` : " (no workers are running)";
|
|
443
|
+
return `no running worker whose url contains '${needle}'${running}. ` + "An MV3 extension service worker is idle-evicted after seconds of inactivity and then appears in NO target " + "listing, so this is usually a sleeping worker rather than a wrong substring. This call only READS a buffer " + "recorded earlier, and that buffer is keyed by the worker's target id, which Chrome re-mints on restart — so " + "re-run with reload:true, which starts the worker and records a fresh capture window.";
|
|
444
|
+
}
|
|
429
445
|
function urlList(workers) {
|
|
430
446
|
return workers.map((w) => w.url).join(", ");
|
|
431
447
|
}
|
|
@@ -2840,6 +2856,31 @@ function captureFile(targetId, captureId) {
|
|
|
2840
2856
|
function delay2(ms) {
|
|
2841
2857
|
return new Promise((r) => setTimeout(r, ms));
|
|
2842
2858
|
}
|
|
2859
|
+
async function resolveRecorderTarget(target, opts) {
|
|
2860
|
+
if (!isWorkerSelector(target))
|
|
2861
|
+
return resolveTarget(target);
|
|
2862
|
+
if (opts.capture)
|
|
2863
|
+
return resolveWorkerSelector(target, { wake: opts.wake ?? true });
|
|
2864
|
+
const needle = workerNeedle(target);
|
|
2865
|
+
if (needle === "")
|
|
2866
|
+
throw new CdpError(WORKER_EMPTY_NEEDLE_MESSAGE);
|
|
2867
|
+
const { matches, liveWorkers } = resolveWorkerTargets(await listTargets(), needle);
|
|
2868
|
+
if (matches.length > 1)
|
|
2869
|
+
throw new CdpError(workerAmbiguityMessage(needle, matches));
|
|
2870
|
+
if (matches.length === 1)
|
|
2871
|
+
return matches[0];
|
|
2872
|
+
throw new CdpError(workerBufferReadMissMessage(needle, liveWorkers));
|
|
2873
|
+
}
|
|
2874
|
+
function assertWakeApplies(target, wake, capture) {
|
|
2875
|
+
if (wake === undefined)
|
|
2876
|
+
return;
|
|
2877
|
+
if (typeof wake !== "boolean")
|
|
2878
|
+
throw new CdpError("'wake' must be a boolean");
|
|
2879
|
+
if (!isWorkerSelector(target))
|
|
2880
|
+
throw new CdpError(workerWakeMisuseMessage("not-a-worker-selector"));
|
|
2881
|
+
if (!capture)
|
|
2882
|
+
throw new CdpError(workerWakeMisuseMessage("read-only-call"));
|
|
2883
|
+
}
|
|
2843
2884
|
var NETWORK_EVENTS = [
|
|
2844
2885
|
"Network.requestWillBeSent",
|
|
2845
2886
|
"Network.responseReceived",
|
|
@@ -2905,7 +2946,8 @@ async function startRecorder(target, opts = {}) {
|
|
|
2905
2946
|
};
|
|
2906
2947
|
}
|
|
2907
2948
|
async function captureWindow(target, durationMs, opts = {}) {
|
|
2908
|
-
const resolved = await
|
|
2949
|
+
const resolved = await resolveRecorderTarget(target, { capture: true, wake: opts.wake });
|
|
2950
|
+
const isWorker = isWorkerTargetType(resolved.type);
|
|
2909
2951
|
const captureId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2910
2952
|
const file = captureFile(resolved.id, captureId);
|
|
2911
2953
|
const rec = await startRecorder(resolved.id, {
|
|
@@ -2915,13 +2957,16 @@ async function captureWindow(target, durationMs, opts = {}) {
|
|
|
2915
2957
|
file,
|
|
2916
2958
|
timeoutMs: opts.timeoutMs
|
|
2917
2959
|
});
|
|
2918
|
-
|
|
2919
|
-
|
|
2960
|
+
if (!isWorker) {
|
|
2961
|
+
await rec.conn.send("Page.enable");
|
|
2962
|
+
await rec.conn.send("Page.reload", { ignoreCache: false });
|
|
2963
|
+
}
|
|
2920
2964
|
await delay2(durationMs);
|
|
2921
2965
|
return {
|
|
2922
2966
|
file: rec.file,
|
|
2923
2967
|
conn: rec.conn,
|
|
2924
2968
|
resolved: { id: rec.target.id, url: rec.target.url, title: rec.target.title },
|
|
2969
|
+
mode: isWorker ? "listen" : "reload",
|
|
2925
2970
|
droppedWrites: rec.droppedWrites,
|
|
2926
2971
|
flush: () => rec.flush(),
|
|
2927
2972
|
async stop() {
|
|
@@ -3021,8 +3066,9 @@ async function readConsoleEntries(file) {
|
|
|
3021
3066
|
return out;
|
|
3022
3067
|
}
|
|
3023
3068
|
async function listConsoleMessages(args = {}) {
|
|
3069
|
+
assertWakeApplies(args.target, args.wake, args.reload === true);
|
|
3024
3070
|
if (args.reload) {
|
|
3025
|
-
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS);
|
|
3071
|
+
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS, { wake: args.wake });
|
|
3026
3072
|
await cap.stop();
|
|
3027
3073
|
const messages2 = await readConsoleEntries(cap.file);
|
|
3028
3074
|
return {
|
|
@@ -3032,7 +3078,7 @@ async function listConsoleMessages(args = {}) {
|
|
|
3032
3078
|
droppedWrites: cap.droppedWrites()
|
|
3033
3079
|
};
|
|
3034
3080
|
}
|
|
3035
|
-
const target = await
|
|
3081
|
+
const target = await resolveRecorderTarget(args.target, { capture: false });
|
|
3036
3082
|
const messages = await readConsoleEntries(recFile(target.id));
|
|
3037
3083
|
return {
|
|
3038
3084
|
target: { id: target.id, url: target.url, title: target.title },
|
|
@@ -3041,7 +3087,7 @@ async function listConsoleMessages(args = {}) {
|
|
|
3041
3087
|
};
|
|
3042
3088
|
}
|
|
3043
3089
|
async function getConsoleMessage(args = {}) {
|
|
3044
|
-
const target = await
|
|
3090
|
+
const target = await resolveRecorderTarget(args.target, { capture: false });
|
|
3045
3091
|
const messages = await readConsoleEntries(recFile(target.id));
|
|
3046
3092
|
const index = args.index ?? 0;
|
|
3047
3093
|
const entry = messages[index];
|
|
@@ -3139,14 +3185,15 @@ async function listNetworkRequests(args = {}) {
|
|
|
3139
3185
|
let file;
|
|
3140
3186
|
let resolved;
|
|
3141
3187
|
let droppedWrites;
|
|
3188
|
+
assertWakeApplies(args.target, args.wake, args.reload === true);
|
|
3142
3189
|
if (args.reload) {
|
|
3143
|
-
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2);
|
|
3190
|
+
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2, { wake: args.wake });
|
|
3144
3191
|
await cap.stop();
|
|
3145
3192
|
file = cap.file;
|
|
3146
3193
|
resolved = cap.resolved;
|
|
3147
3194
|
droppedWrites = cap.droppedWrites();
|
|
3148
3195
|
} else {
|
|
3149
|
-
const target = await
|
|
3196
|
+
const target = await resolveRecorderTarget(args.target, { capture: false });
|
|
3150
3197
|
file = recFile(target.id);
|
|
3151
3198
|
resolved = { id: target.id, url: target.url, title: target.title };
|
|
3152
3199
|
}
|
|
@@ -3167,8 +3214,9 @@ async function getNetworkRequest(args = {}) {
|
|
|
3167
3214
|
return requests.find((r) => r.url.includes(args.url));
|
|
3168
3215
|
};
|
|
3169
3216
|
const describeSelector = () => args.requestId ? `requestId ${args.requestId}` : `url ~ '${args.url}'`;
|
|
3217
|
+
assertWakeApplies(args.target, args.wake, Boolean(args.includeBody && args.url));
|
|
3170
3218
|
if (args.includeBody && args.url) {
|
|
3171
|
-
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2);
|
|
3219
|
+
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2, { wake: args.wake });
|
|
3172
3220
|
try {
|
|
3173
3221
|
await cap.flush();
|
|
3174
3222
|
const match2 = (await readNetworkRequests(cap.file)).find((r) => r.url.includes(args.url));
|
|
@@ -3185,7 +3233,7 @@ async function getNetworkRequest(args = {}) {
|
|
|
3185
3233
|
await cap.stop();
|
|
3186
3234
|
}
|
|
3187
3235
|
}
|
|
3188
|
-
const target = await
|
|
3236
|
+
const target = await resolveRecorderTarget(args.target, { capture: false });
|
|
3189
3237
|
const match = pick(await readNetworkRequests(recFile(target.id)));
|
|
3190
3238
|
if (!match) {
|
|
3191
3239
|
throw new CdpError(`no network request matched ${describeSelector()}; run list_network_requests with reload:true first` + (args.includeBody ? `, then fetch the body with the url selector (reload re-mints requestIds)` : ``));
|
|
@@ -4481,7 +4529,7 @@ var MANIFEST = [
|
|
|
4481
4529
|
},
|
|
4482
4530
|
{
|
|
4483
4531
|
name: "evaluate_script",
|
|
4484
|
-
description: "Run arbitrary JavaScript in the target page's main-world context over raw CDP and return the evaluated value (returnByValue). With no 'args' the 'expression' is evaluated as a raw expression; when 'args' is provided 'expression' must be a function literal (arrow or classic) invoked on globalThis with the args passed positionally. A thrown exception surfaces as an error; non-serializable returns (DOM nodes, functions) come back as their CDP description string. Pass 'savePath' to write the evaluated value to a JSON file instead: the response then carries only {path,bytes,type,target} and the value itself never appears in it, in any form. That is the way to read a credential (a JWT or session token out of localStorage, for example) without putting the secret into the caller's transcript. This is
|
|
4532
|
+
description: "Run arbitrary JavaScript in the target page's main-world context over raw CDP and return the evaluated value (returnByValue). With no 'args' the 'expression' is evaluated as a raw expression; when 'args' is provided 'expression' must be a function literal (arrow or classic) invoked on globalThis with the args passed positionally. A thrown exception surfaces as an error; non-serializable returns (DOM nodes, functions) come back as their CDP description string. Pass 'savePath' to write the evaluated value to a JSON file instead: the response then carries only {path,bytes,type,target} and the value itself never appears in it, in any form. That is the way to read a credential (a JWT or session token out of localStorage, for example) without putting the secret into the caller's transcript. This is the one tool that can EVALUATE inside an MV3 extension's background SERVICE WORKER rather than a page (list_network_requests/get_network_request/list_console_messages OBSERVE one): pass target 'worker:<extension-id>' (Chrome only) and read chrome.storage.local, chrome.runtime and the worker's own globals directly, with an idle-evicted worker started for you first (see 'wake').",
|
|
4485
4533
|
inputSchema: {
|
|
4486
4534
|
type: "object",
|
|
4487
4535
|
properties: {
|
|
@@ -4491,7 +4539,7 @@ var MANIFEST = [
|
|
|
4491
4539
|
},
|
|
4492
4540
|
target: {
|
|
4493
4541
|
type: "string",
|
|
4494
|
-
description: "Page selector: 'active' (or omit) -> first page-type target | '<32-hex targetId>' -> exact target by id | 'index:N' -> Nth page-type target (0-based) | 'url:<substring>' -> first page whose url contains substring | 'title:<substring>' -> first page whose title contains substring | 'label:<name>' -> the page-type target with exactly this label (checked against the origin ledger and live leases) | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js) and read chrome.storage.local from it. The worker: arm is CHROME-ONLY (Capability 'worker.targets') and is accepted by
|
|
4542
|
+
description: "Page selector: 'active' (or omit) -> first page-type target | '<32-hex targetId>' -> exact target by id | 'index:N' -> Nth page-type target (0-based) | 'url:<substring>' -> first page whose url contains substring | 'title:<substring>' -> first page whose title contains substring | 'label:<name>' -> the page-type target with exactly this label (checked against the origin ledger and live leases) | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js) and read chrome.storage.local from it. The worker: arm is CHROME-ONLY (Capability 'worker.targets') and is accepted by four tools only — evaluate_script, list_network_requests, get_network_request and list_console_messages — never as universal grammar; an MV3 worker that has been idle-evicted is started automatically first, see 'wake'."
|
|
4495
4543
|
},
|
|
4496
4544
|
wake: {
|
|
4497
4545
|
type: "boolean",
|
|
@@ -5412,7 +5460,7 @@ var MANIFEST = [
|
|
|
5412
5460
|
},
|
|
5413
5461
|
{
|
|
5414
5462
|
name: "list_console_messages",
|
|
5415
|
-
description: "Read console output (logs, warnings, exceptions) captured for the target page. By default reads the target's existing shared buffer and returns parsed console entries (empty if no capture has run); with reload:true it reloads the page and records a fresh capture window (both console+network) so a network reload never wipes console history.",
|
|
5463
|
+
description: "Read console output (logs, warnings, exceptions) captured for the target page. By default reads the target's existing shared buffer and returns parsed console entries (empty if no capture has run); with reload:true it reloads the page and records a fresh capture window (both console+network) so a network reload never wipes console history. SERVICE WORKERS (Chrome only, Capability 'worker.targets'): pass target 'worker:<extension-id>' to watch an MV3 extension's BACKGROUND SERVICE WORKER. The outbound request that worker makes to your backend is recorded from CDP's Network domain on the worker's own session, which observes what the code ACTUALLY SENT and so needs neither of the two tricks a caller reaches for first. Both of those fail for ordinary JavaScript reasons rather than service-worker ones: a value bound at module top level is not visible from evaluate_script's global scope (plain lexical scoping, exactly the same in a page), and assigning self.fetch cannot rebind a fetch reference the module already captured. A worker's console.log arrives as Runtime.consoleAPICalled on its own session. A worker capture has no reload to drive it: reload:true means 'listen for durationMs while the worker runs', so trigger the worker's work (another tool call, an extension message, an alarm) during the window; a worker woken by the call has already run its top-level code by the time the recorder attaches, so a request it makes at startup can be missed. SIDE EFFECT, deliberate and documented: the recording holds a CDP session on the worker, and that KEEPS THE WORKER ALIVE for as long as the capture runs — an MV3 worker is otherwise idle-evicted within seconds — and Chrome resumes evicting it once the capture stops.",
|
|
5416
5464
|
inputSchema: {
|
|
5417
5465
|
type: "object",
|
|
5418
5466
|
properties: {
|
|
@@ -5422,15 +5470,19 @@ var MANIFEST = [
|
|
|
5422
5470
|
},
|
|
5423
5471
|
target: {
|
|
5424
5472
|
type: "string",
|
|
5425
|
-
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
5473
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>' | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js). The worker: arm is CHROME-ONLY (Capability 'worker.targets'), is refused on Firefox, and a bare worker target id works too."
|
|
5426
5474
|
},
|
|
5427
5475
|
reload: {
|
|
5428
5476
|
type: "boolean",
|
|
5429
|
-
description: "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer)."
|
|
5477
|
+
description: "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer). With a worker: target there is nothing to reload: the window records for durationMs while the worker runs, and the session it holds keeps that worker from being idle-evicted until the capture stops."
|
|
5430
5478
|
},
|
|
5431
5479
|
durationMs: {
|
|
5432
5480
|
type: "number",
|
|
5433
5481
|
description: "Capture window for reload mode, in milliseconds. Default 2500."
|
|
5482
|
+
},
|
|
5483
|
+
wake: {
|
|
5484
|
+
type: "boolean",
|
|
5485
|
+
description: "Worker targets only, and only when a capture is being started (reload:true). Default true: an MV3 extension service worker is idle-evicted after seconds of inactivity and then exists in NO target listing, so the capture asks Chrome to start it (ServiceWorker.startWorker over a page session) and then re-reads the target list to confirm it actually came up. Pass false to fail fast instead of starting anything. Rejected rather than ignored on a page target, on a bare target id (Chrome re-mints a worker's target id when it restarts, so only the url-substring form can name a worker that is not running), and on a read-only call (the buffer is keyed by target id, so a woken worker's buffer would be empty)."
|
|
5434
5486
|
}
|
|
5435
5487
|
},
|
|
5436
5488
|
required: [],
|
|
@@ -5462,7 +5514,7 @@ var MANIFEST = [
|
|
|
5462
5514
|
},
|
|
5463
5515
|
{
|
|
5464
5516
|
name: "list_network_requests",
|
|
5465
|
-
description: "Return correlated network request rows (one per requestId, with status/headers/state) for the target page. By default reads the target's existing buffer; with reload:true it reloads and records a fresh both-domains capture window. Use filterUrl to keep only requests whose URL contains a substring.",
|
|
5517
|
+
description: "Return correlated network request rows (one per requestId, with status/headers/state) for the target page. By default reads the target's existing buffer; with reload:true it reloads and records a fresh both-domains capture window. Use filterUrl to keep only requests whose URL contains a substring. SERVICE WORKERS (Chrome only, Capability 'worker.targets'): pass target 'worker:<extension-id>' to watch an MV3 extension's BACKGROUND SERVICE WORKER. The outbound request that worker makes to your backend is recorded from CDP's Network domain on the worker's own session, which observes what the code ACTUALLY SENT and so needs neither of the two tricks a caller reaches for first. Both of those fail for ordinary JavaScript reasons rather than service-worker ones: a value bound at module top level is not visible from evaluate_script's global scope (plain lexical scoping, exactly the same in a page), and assigning self.fetch cannot rebind a fetch reference the module already captured. A worker capture has no reload to drive it: reload:true means 'listen for durationMs while the worker runs', so trigger the worker's work (another tool call, an extension message, an alarm) during the window; a worker woken by the call has already run its top-level code by the time the recorder attaches, so a request it makes at startup can be missed. SIDE EFFECT, deliberate and documented: the recording holds a CDP session on the worker, and that KEEPS THE WORKER ALIVE for as long as the capture runs — an MV3 worker is otherwise idle-evicted within seconds — and Chrome resumes evicting it once the capture stops.",
|
|
5466
5518
|
inputSchema: {
|
|
5467
5519
|
type: "object",
|
|
5468
5520
|
properties: {
|
|
@@ -5472,11 +5524,11 @@ var MANIFEST = [
|
|
|
5472
5524
|
},
|
|
5473
5525
|
target: {
|
|
5474
5526
|
type: "string",
|
|
5475
|
-
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
5527
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>' | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js). The worker: arm is CHROME-ONLY (Capability 'worker.targets'), is refused on Firefox, and a bare worker target id works too."
|
|
5476
5528
|
},
|
|
5477
5529
|
reload: {
|
|
5478
5530
|
type: "boolean",
|
|
5479
|
-
description: "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer)."
|
|
5531
|
+
description: "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer). With a worker: target there is nothing to reload: the window records for durationMs while the worker runs, and the session it holds keeps that worker from being idle-evicted until the capture stops."
|
|
5480
5532
|
},
|
|
5481
5533
|
durationMs: {
|
|
5482
5534
|
type: "number",
|
|
@@ -5485,6 +5537,10 @@ var MANIFEST = [
|
|
|
5485
5537
|
filterUrl: {
|
|
5486
5538
|
type: "string",
|
|
5487
5539
|
description: "Only return requests whose URL contains this substring."
|
|
5540
|
+
},
|
|
5541
|
+
wake: {
|
|
5542
|
+
type: "boolean",
|
|
5543
|
+
description: "Worker targets only, and only when a capture is being started (reload:true). Default true: an MV3 extension service worker is idle-evicted after seconds of inactivity and then exists in NO target listing, so the capture asks Chrome to start it (ServiceWorker.startWorker over a page session) and then re-reads the target list to confirm it actually came up. Pass false to fail fast instead of starting anything. Rejected rather than ignored on a page target, on a bare target id (Chrome re-mints a worker's target id when it restarts, so only the url-substring form can name a worker that is not running), and on a read-only call (the buffer is keyed by target id, so a woken worker's buffer would be empty)."
|
|
5488
5544
|
}
|
|
5489
5545
|
},
|
|
5490
5546
|
required: [],
|
|
@@ -5493,7 +5549,7 @@ var MANIFEST = [
|
|
|
5493
5549
|
},
|
|
5494
5550
|
{
|
|
5495
5551
|
name: "get_network_request",
|
|
5496
|
-
description: "Return one network request (matched by exact requestId, else by url substring) including status/headers. Requires at least one of requestId or url (throws otherwise). With includeBody:true the body fetch drives a fresh reload
|
|
5552
|
+
description: "Return one network request (matched by exact requestId, else by url substring) including status/headers. Requires at least one of requestId or url (throws otherwise). With includeBody:true the body fetch drives a fresh capture (a reload for a page, a listen window for a worker) and is matched by url ONLY (a fresh capture re-mints requestIds, so a carried-over requestId cannot fetch a body, so it returns metadata plus bodyUnavailableReason). SERVICE WORKERS (Chrome only, Capability 'worker.targets'): pass target 'worker:<extension-id>' to watch an MV3 extension's BACKGROUND SERVICE WORKER. The outbound request that worker makes to your backend is recorded from CDP's Network domain on the worker's own session, which observes what the code ACTUALLY SENT and so needs neither of the two tricks a caller reaches for first. Both of those fail for ordinary JavaScript reasons rather than service-worker ones: a value bound at module top level is not visible from evaluate_script's global scope (plain lexical scoping, exactly the same in a page), and assigning self.fetch cannot rebind a fetch reference the module already captured. A worker capture has no reload to drive it: reload:true means 'listen for durationMs while the worker runs', so trigger the worker's work (another tool call, an extension message, an alarm) during the window; a worker woken by the call has already run its top-level code by the time the recorder attaches, so a request it makes at startup can be missed. SIDE EFFECT, deliberate and documented: the recording holds a CDP session on the worker, and that KEEPS THE WORKER ALIVE for as long as the capture runs — an MV3 worker is otherwise idle-evicted within seconds — and Chrome resumes evicting it once the capture stops.",
|
|
5497
5553
|
inputSchema: {
|
|
5498
5554
|
type: "object",
|
|
5499
5555
|
properties: {
|
|
@@ -5503,7 +5559,7 @@ var MANIFEST = [
|
|
|
5503
5559
|
},
|
|
5504
5560
|
target: {
|
|
5505
5561
|
type: "string",
|
|
5506
|
-
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
5562
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>' | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js). The worker: arm is CHROME-ONLY (Capability 'worker.targets'), is refused on Firefox, and a bare worker target id works too."
|
|
5507
5563
|
},
|
|
5508
5564
|
requestId: {
|
|
5509
5565
|
type: "string",
|
|
@@ -5520,6 +5576,10 @@ var MANIFEST = [
|
|
|
5520
5576
|
durationMs: {
|
|
5521
5577
|
type: "number",
|
|
5522
5578
|
description: "Capture window for the reload-driven body fetch, in milliseconds. Default 2500."
|
|
5579
|
+
},
|
|
5580
|
+
wake: {
|
|
5581
|
+
type: "boolean",
|
|
5582
|
+
description: "Worker targets only, and only when a capture is being started (includeBody together with url). Default true: an MV3 extension service worker is idle-evicted after seconds of inactivity and then exists in NO target listing, so the capture asks Chrome to start it (ServiceWorker.startWorker over a page session) and then re-reads the target list to confirm it actually came up. Pass false to fail fast instead of starting anything. Rejected rather than ignored on a page target, on a bare target id (Chrome re-mints a worker's target id when it restarts, so only the url-substring form can name a worker that is not running), and on a read-only call (the buffer is keyed by target id, so a woken worker's buffer would be empty)."
|
|
5523
5583
|
}
|
|
5524
5584
|
},
|
|
5525
5585
|
required: [],
|
|
@@ -7643,6 +7703,11 @@ async function withPage3(driver, target, fn) {
|
|
|
7643
7703
|
}
|
|
7644
7704
|
var consoleBuffers = new Map;
|
|
7645
7705
|
var networkBuffers = new Map;
|
|
7706
|
+
function refuseWorkerSelector(driver, target) {
|
|
7707
|
+
if (isWorkerSelector(target) && !driver.capabilities.has("worker.targets")) {
|
|
7708
|
+
throw new BidiToolError(WORKER_SELECTOR_UNSUPPORTED_MESSAGE);
|
|
7709
|
+
}
|
|
7710
|
+
}
|
|
7646
7711
|
function bufferFor(map, id) {
|
|
7647
7712
|
let b = map.get(id);
|
|
7648
7713
|
if (!b) {
|
|
@@ -7671,6 +7736,7 @@ async function captureFresh(page, durationMs) {
|
|
|
7671
7736
|
}
|
|
7672
7737
|
}
|
|
7673
7738
|
async function listConsoleMessages2(driver, args = {}) {
|
|
7739
|
+
refuseWorkerSelector(driver, args.target);
|
|
7674
7740
|
return withPage3(driver, args.target, async (page) => {
|
|
7675
7741
|
if (args.reload)
|
|
7676
7742
|
await captureFresh(page, args.durationMs ?? 2500);
|
|
@@ -7679,6 +7745,7 @@ async function listConsoleMessages2(driver, args = {}) {
|
|
|
7679
7745
|
});
|
|
7680
7746
|
}
|
|
7681
7747
|
async function getConsoleMessage2(driver, args = {}) {
|
|
7748
|
+
refuseWorkerSelector(driver, args.target);
|
|
7682
7749
|
return withPage3(driver, args.target, async (page) => {
|
|
7683
7750
|
const buf = bufferFor(consoleBuffers, page.info.id);
|
|
7684
7751
|
const idx = args.index ?? 0;
|
|
@@ -7689,6 +7756,7 @@ async function getConsoleMessage2(driver, args = {}) {
|
|
|
7689
7756
|
});
|
|
7690
7757
|
}
|
|
7691
7758
|
async function listNetworkRequests2(driver, args = {}) {
|
|
7759
|
+
refuseWorkerSelector(driver, args.target);
|
|
7692
7760
|
return withPage3(driver, args.target, async (page) => {
|
|
7693
7761
|
if (args.reload)
|
|
7694
7762
|
await captureFresh(page, args.durationMs ?? 2500);
|
|
@@ -7699,6 +7767,7 @@ async function listNetworkRequests2(driver, args = {}) {
|
|
|
7699
7767
|
});
|
|
7700
7768
|
}
|
|
7701
7769
|
async function getNetworkRequest2(driver, args = {}) {
|
|
7770
|
+
refuseWorkerSelector(driver, args.target);
|
|
7702
7771
|
if (!args.requestId && !args.url)
|
|
7703
7772
|
throw new BidiToolError("get_network_request requires requestId or url");
|
|
7704
7773
|
return withPage3(driver, args.target, async (page) => {
|
|
@@ -7790,7 +7859,7 @@ var FIREFOX_TOOLS = {
|
|
|
7790
7859
|
};
|
|
7791
7860
|
|
|
7792
7861
|
// src/mcp.ts
|
|
7793
|
-
var VERSION = "1.9.
|
|
7862
|
+
var VERSION = "1.9.1";
|
|
7794
7863
|
var BROWSER = resolveBrowserKind(stripBrowserFlag(process.argv.slice(2)));
|
|
7795
7864
|
var AVAILABILITY = toolAvailability(BROWSER);
|
|
7796
7865
|
var AVAILABLE_NAMES = new Set(AVAILABILITY.available);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cdp-toolkit",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.1",
|
|
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",
|
package/src/bidi-tools.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import type { BrowserDriver, InterceptRule, PageDriver, PageInfo } from "./driver.ts";
|
|
11
11
|
import { selectRule, buildFulfillParams, effectiveAction } from "./tools/network_mock.ts";
|
|
12
12
|
import type { TargetSelector } from "./types.ts";
|
|
13
|
+
import { isWorkerSelector, WORKER_SELECTOR_UNSUPPORTED_MESSAGE } from "./workers.ts";
|
|
13
14
|
|
|
14
15
|
class BidiToolError extends Error {}
|
|
15
16
|
|
|
@@ -33,6 +34,22 @@ async function withPage<T>(driver: BrowserDriver, target: TargetSelector | undef
|
|
|
33
34
|
const consoleBuffers = new Map<string, Array<Record<string, unknown>>>();
|
|
34
35
|
const networkBuffers = new Map<string, Array<Record<string, unknown>>>();
|
|
35
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Refuse the Chrome-only `worker:<substring>` arm BEFORE a browser is launched.
|
|
39
|
+
*
|
|
40
|
+
* pickContext already refuses it (bidi/driver.ts), but only after driver.page()
|
|
41
|
+
* has dialed — and on Firefox that means LAUNCHING a browser for a call that
|
|
42
|
+
* cannot possibly succeed. The gate is written as a capability check rather than
|
|
43
|
+
* "this file is the BiDi one" so it stays true if a future BiDi driver ever
|
|
44
|
+
* gains the capability, and the message is the shared one, so the teaching does
|
|
45
|
+
* not fork.
|
|
46
|
+
*/
|
|
47
|
+
function refuseWorkerSelector(driver: BrowserDriver, target: TargetSelector): void {
|
|
48
|
+
if (isWorkerSelector(target) && !driver.capabilities.has("worker.targets")) {
|
|
49
|
+
throw new BidiToolError(WORKER_SELECTOR_UNSUPPORTED_MESSAGE);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
function bufferFor(map: Map<string, Array<Record<string, unknown>>>, id: string): Array<Record<string, unknown>> {
|
|
37
54
|
let b = map.get(id);
|
|
38
55
|
if (!b) { b = []; map.set(id, b); }
|
|
@@ -59,6 +76,7 @@ export async function listConsoleMessages(
|
|
|
59
76
|
driver: BrowserDriver,
|
|
60
77
|
args: { target?: TargetSelector; reload?: boolean; durationMs?: number } = {},
|
|
61
78
|
): Promise<{ messages: Array<Record<string, unknown>>; count: number }> {
|
|
79
|
+
refuseWorkerSelector(driver, args.target);
|
|
62
80
|
return withPage(driver, args.target, async (page) => {
|
|
63
81
|
if (args.reload) await captureFresh(page, args.durationMs ?? 2500);
|
|
64
82
|
const buf = bufferFor(consoleBuffers, page.info.id);
|
|
@@ -70,6 +88,7 @@ export async function getConsoleMessage(
|
|
|
70
88
|
driver: BrowserDriver,
|
|
71
89
|
args: { target?: TargetSelector; index?: number } = {},
|
|
72
90
|
): Promise<Record<string, unknown>> {
|
|
91
|
+
refuseWorkerSelector(driver, args.target);
|
|
73
92
|
return withPage(driver, args.target, async (page) => {
|
|
74
93
|
const buf = bufferFor(consoleBuffers, page.info.id);
|
|
75
94
|
const idx = args.index ?? 0;
|
|
@@ -83,6 +102,7 @@ export async function listNetworkRequests(
|
|
|
83
102
|
driver: BrowserDriver,
|
|
84
103
|
args: { target?: TargetSelector; reload?: boolean; durationMs?: number; filterUrl?: string } = {},
|
|
85
104
|
): Promise<{ requests: Array<Record<string, unknown>>; count: number }> {
|
|
105
|
+
refuseWorkerSelector(driver, args.target);
|
|
86
106
|
return withPage(driver, args.target, async (page) => {
|
|
87
107
|
if (args.reload) await captureFresh(page, args.durationMs ?? 2500);
|
|
88
108
|
let buf = bufferFor(networkBuffers, page.info.id);
|
|
@@ -95,6 +115,7 @@ export async function getNetworkRequest(
|
|
|
95
115
|
driver: BrowserDriver,
|
|
96
116
|
args: { target?: TargetSelector; requestId?: string; url?: string } = {},
|
|
97
117
|
): Promise<Record<string, unknown>> {
|
|
118
|
+
refuseWorkerSelector(driver, args.target);
|
|
98
119
|
if (!args.requestId && !args.url) throw new BidiToolError("get_network_request requires requestId or url");
|
|
99
120
|
return withPage(driver, args.target, async (page) => {
|
|
100
121
|
const buf = bufferFor(networkBuffers, page.info.id);
|
package/src/cli.ts
CHANGED
|
@@ -49,8 +49,10 @@ zero behavior change for existing users). Firefox is LAUNCHED per invocation (it
|
|
|
49
49
|
to), used for exactly one tool call, then disposed before the process exits.
|
|
50
50
|
|
|
51
51
|
Target selector grammar: active | index:N | url:<substr> | title:<substr> | label:<name> | <targetId>
|
|
52
|
-
plus worker:<substr>
|
|
53
|
-
|
|
52
|
+
plus worker:<substr> (Chrome only) for evaluate_script, list_network_requests, get_network_request
|
|
53
|
+
and list_console_messages ONLY: an MV3 extension's background service worker, e.g.
|
|
54
|
+
worker:<extension-id>. An idle-evicted worker is started first unless wake:false. Recording a
|
|
55
|
+
worker keeps it alive for the length of the capture.
|
|
54
56
|
Leases: pass --lease <token> to act on a tab another agent claimed with claim_page (MCP only).
|
|
55
57
|
Run list_leases to see who holds what; release_page --lease <token> gives a lease back.
|
|
56
58
|
Run with --list to print every tool name available under the selected backend.
|
package/src/manifest.ts
CHANGED
|
@@ -204,7 +204,7 @@ export const MANIFEST: ToolSpec[] = [
|
|
|
204
204
|
},
|
|
205
205
|
{
|
|
206
206
|
"name": "evaluate_script",
|
|
207
|
-
"description": "Run arbitrary JavaScript in the target page's main-world context over raw CDP and return the evaluated value (returnByValue). With no 'args' the 'expression' is evaluated as a raw expression; when 'args' is provided 'expression' must be a function literal (arrow or classic) invoked on globalThis with the args passed positionally. A thrown exception surfaces as an error; non-serializable returns (DOM nodes, functions) come back as their CDP description string. Pass 'savePath' to write the evaluated value to a JSON file instead: the response then carries only {path,bytes,type,target} and the value itself never appears in it, in any form. That is the way to read a credential (a JWT or session token out of localStorage, for example) without putting the secret into the caller's transcript. This is
|
|
207
|
+
"description": "Run arbitrary JavaScript in the target page's main-world context over raw CDP and return the evaluated value (returnByValue). With no 'args' the 'expression' is evaluated as a raw expression; when 'args' is provided 'expression' must be a function literal (arrow or classic) invoked on globalThis with the args passed positionally. A thrown exception surfaces as an error; non-serializable returns (DOM nodes, functions) come back as their CDP description string. Pass 'savePath' to write the evaluated value to a JSON file instead: the response then carries only {path,bytes,type,target} and the value itself never appears in it, in any form. That is the way to read a credential (a JWT or session token out of localStorage, for example) without putting the secret into the caller's transcript. This is the one tool that can EVALUATE inside an MV3 extension's background SERVICE WORKER rather than a page (list_network_requests/get_network_request/list_console_messages OBSERVE one): pass target 'worker:<extension-id>' (Chrome only) and read chrome.storage.local, chrome.runtime and the worker's own globals directly, with an idle-evicted worker started for you first (see 'wake').",
|
|
208
208
|
"inputSchema": {
|
|
209
209
|
"type": "object",
|
|
210
210
|
"properties": {
|
|
@@ -214,7 +214,7 @@ export const MANIFEST: ToolSpec[] = [
|
|
|
214
214
|
},
|
|
215
215
|
"target": {
|
|
216
216
|
"type": "string",
|
|
217
|
-
"description": "Page selector: 'active' (or omit) -> first page-type target | '<32-hex targetId>' -> exact target by id | 'index:N' -> Nth page-type target (0-based) | 'url:<substring>' -> first page whose url contains substring | 'title:<substring>' -> first page whose title contains substring | 'label:<name>' -> the page-type target with exactly this label (checked against the origin ledger and live leases) | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js) and read chrome.storage.local from it. The worker: arm is CHROME-ONLY (Capability 'worker.targets') and is accepted by
|
|
217
|
+
"description": "Page selector: 'active' (or omit) -> first page-type target | '<32-hex targetId>' -> exact target by id | 'index:N' -> Nth page-type target (0-based) | 'url:<substring>' -> first page whose url contains substring | 'title:<substring>' -> first page whose title contains substring | 'label:<name>' -> the page-type target with exactly this label (checked against the origin ledger and live leases) | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js) and read chrome.storage.local from it. The worker: arm is CHROME-ONLY (Capability 'worker.targets') and is accepted by four tools only — evaluate_script, list_network_requests, get_network_request and list_console_messages — never as universal grammar; an MV3 worker that has been idle-evicted is started automatically first, see 'wake'."
|
|
218
218
|
},
|
|
219
219
|
"wake": {
|
|
220
220
|
"type": "boolean",
|
|
@@ -1135,7 +1135,7 @@ export const MANIFEST: ToolSpec[] = [
|
|
|
1135
1135
|
},
|
|
1136
1136
|
{
|
|
1137
1137
|
"name": "list_console_messages",
|
|
1138
|
-
"description": "Read console output (logs, warnings, exceptions) captured for the target page. By default reads the target's existing shared buffer and returns parsed console entries (empty if no capture has run); with reload:true it reloads the page and records a fresh capture window (both console+network) so a network reload never wipes console history.",
|
|
1138
|
+
"description": "Read console output (logs, warnings, exceptions) captured for the target page. By default reads the target's existing shared buffer and returns parsed console entries (empty if no capture has run); with reload:true it reloads the page and records a fresh capture window (both console+network) so a network reload never wipes console history. SERVICE WORKERS (Chrome only, Capability 'worker.targets'): pass target 'worker:<extension-id>' to watch an MV3 extension's BACKGROUND SERVICE WORKER. The outbound request that worker makes to your backend is recorded from CDP's Network domain on the worker's own session, which observes what the code ACTUALLY SENT and so needs neither of the two tricks a caller reaches for first. Both of those fail for ordinary JavaScript reasons rather than service-worker ones: a value bound at module top level is not visible from evaluate_script's global scope (plain lexical scoping, exactly the same in a page), and assigning self.fetch cannot rebind a fetch reference the module already captured. A worker's console.log arrives as Runtime.consoleAPICalled on its own session. A worker capture has no reload to drive it: reload:true means 'listen for durationMs while the worker runs', so trigger the worker's work (another tool call, an extension message, an alarm) during the window; a worker woken by the call has already run its top-level code by the time the recorder attaches, so a request it makes at startup can be missed. SIDE EFFECT, deliberate and documented: the recording holds a CDP session on the worker, and that KEEPS THE WORKER ALIVE for as long as the capture runs — an MV3 worker is otherwise idle-evicted within seconds — and Chrome resumes evicting it once the capture stops.",
|
|
1139
1139
|
"inputSchema": {
|
|
1140
1140
|
"type": "object",
|
|
1141
1141
|
"properties": {
|
|
@@ -1145,15 +1145,19 @@ export const MANIFEST: ToolSpec[] = [
|
|
|
1145
1145
|
},
|
|
1146
1146
|
"target": {
|
|
1147
1147
|
"type": "string",
|
|
1148
|
-
"description": "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
1148
|
+
"description": "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>' | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js). The worker: arm is CHROME-ONLY (Capability 'worker.targets'), is refused on Firefox, and a bare worker target id works too."
|
|
1149
1149
|
},
|
|
1150
1150
|
"reload": {
|
|
1151
1151
|
"type": "boolean",
|
|
1152
|
-
"description": "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer)."
|
|
1152
|
+
"description": "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer). With a worker: target there is nothing to reload: the window records for durationMs while the worker runs, and the session it holds keeps that worker from being idle-evicted until the capture stops."
|
|
1153
1153
|
},
|
|
1154
1154
|
"durationMs": {
|
|
1155
1155
|
"type": "number",
|
|
1156
1156
|
"description": "Capture window for reload mode, in milliseconds. Default 2500."
|
|
1157
|
+
},
|
|
1158
|
+
"wake": {
|
|
1159
|
+
"type": "boolean",
|
|
1160
|
+
"description": "Worker targets only, and only when a capture is being started (reload:true). Default true: an MV3 extension service worker is idle-evicted after seconds of inactivity and then exists in NO target listing, so the capture asks Chrome to start it (ServiceWorker.startWorker over a page session) and then re-reads the target list to confirm it actually came up. Pass false to fail fast instead of starting anything. Rejected rather than ignored on a page target, on a bare target id (Chrome re-mints a worker's target id when it restarts, so only the url-substring form can name a worker that is not running), and on a read-only call (the buffer is keyed by target id, so a woken worker's buffer would be empty)."
|
|
1157
1161
|
}
|
|
1158
1162
|
},
|
|
1159
1163
|
"required": [],
|
|
@@ -1185,7 +1189,7 @@ export const MANIFEST: ToolSpec[] = [
|
|
|
1185
1189
|
},
|
|
1186
1190
|
{
|
|
1187
1191
|
"name": "list_network_requests",
|
|
1188
|
-
"description": "Return correlated network request rows (one per requestId, with status/headers/state) for the target page. By default reads the target's existing buffer; with reload:true it reloads and records a fresh both-domains capture window. Use filterUrl to keep only requests whose URL contains a substring.",
|
|
1192
|
+
"description": "Return correlated network request rows (one per requestId, with status/headers/state) for the target page. By default reads the target's existing buffer; with reload:true it reloads and records a fresh both-domains capture window. Use filterUrl to keep only requests whose URL contains a substring. SERVICE WORKERS (Chrome only, Capability 'worker.targets'): pass target 'worker:<extension-id>' to watch an MV3 extension's BACKGROUND SERVICE WORKER. The outbound request that worker makes to your backend is recorded from CDP's Network domain on the worker's own session, which observes what the code ACTUALLY SENT and so needs neither of the two tricks a caller reaches for first. Both of those fail for ordinary JavaScript reasons rather than service-worker ones: a value bound at module top level is not visible from evaluate_script's global scope (plain lexical scoping, exactly the same in a page), and assigning self.fetch cannot rebind a fetch reference the module already captured. A worker capture has no reload to drive it: reload:true means 'listen for durationMs while the worker runs', so trigger the worker's work (another tool call, an extension message, an alarm) during the window; a worker woken by the call has already run its top-level code by the time the recorder attaches, so a request it makes at startup can be missed. SIDE EFFECT, deliberate and documented: the recording holds a CDP session on the worker, and that KEEPS THE WORKER ALIVE for as long as the capture runs — an MV3 worker is otherwise idle-evicted within seconds — and Chrome resumes evicting it once the capture stops.",
|
|
1189
1193
|
"inputSchema": {
|
|
1190
1194
|
"type": "object",
|
|
1191
1195
|
"properties": {
|
|
@@ -1195,11 +1199,11 @@ export const MANIFEST: ToolSpec[] = [
|
|
|
1195
1199
|
},
|
|
1196
1200
|
"target": {
|
|
1197
1201
|
"type": "string",
|
|
1198
|
-
"description": "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
1202
|
+
"description": "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>' | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js). The worker: arm is CHROME-ONLY (Capability 'worker.targets'), is refused on Firefox, and a bare worker target id works too."
|
|
1199
1203
|
},
|
|
1200
1204
|
"reload": {
|
|
1201
1205
|
"type": "boolean",
|
|
1202
|
-
"description": "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer)."
|
|
1206
|
+
"description": "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer). With a worker: target there is nothing to reload: the window records for durationMs while the worker runs, and the session it holds keeps that worker from being idle-evicted until the capture stops."
|
|
1203
1207
|
},
|
|
1204
1208
|
"durationMs": {
|
|
1205
1209
|
"type": "number",
|
|
@@ -1208,6 +1212,10 @@ export const MANIFEST: ToolSpec[] = [
|
|
|
1208
1212
|
"filterUrl": {
|
|
1209
1213
|
"type": "string",
|
|
1210
1214
|
"description": "Only return requests whose URL contains this substring."
|
|
1215
|
+
},
|
|
1216
|
+
"wake": {
|
|
1217
|
+
"type": "boolean",
|
|
1218
|
+
"description": "Worker targets only, and only when a capture is being started (reload:true). Default true: an MV3 extension service worker is idle-evicted after seconds of inactivity and then exists in NO target listing, so the capture asks Chrome to start it (ServiceWorker.startWorker over a page session) and then re-reads the target list to confirm it actually came up. Pass false to fail fast instead of starting anything. Rejected rather than ignored on a page target, on a bare target id (Chrome re-mints a worker's target id when it restarts, so only the url-substring form can name a worker that is not running), and on a read-only call (the buffer is keyed by target id, so a woken worker's buffer would be empty)."
|
|
1211
1219
|
}
|
|
1212
1220
|
},
|
|
1213
1221
|
"required": [],
|
|
@@ -1216,7 +1224,7 @@ export const MANIFEST: ToolSpec[] = [
|
|
|
1216
1224
|
},
|
|
1217
1225
|
{
|
|
1218
1226
|
"name": "get_network_request",
|
|
1219
|
-
"description": "Return one network request (matched by exact requestId, else by url substring) including status/headers. Requires at least one of requestId or url (throws otherwise). With includeBody:true the body fetch drives a fresh reload
|
|
1227
|
+
"description": "Return one network request (matched by exact requestId, else by url substring) including status/headers. Requires at least one of requestId or url (throws otherwise). With includeBody:true the body fetch drives a fresh capture (a reload for a page, a listen window for a worker) and is matched by url ONLY (a fresh capture re-mints requestIds, so a carried-over requestId cannot fetch a body, so it returns metadata plus bodyUnavailableReason). SERVICE WORKERS (Chrome only, Capability 'worker.targets'): pass target 'worker:<extension-id>' to watch an MV3 extension's BACKGROUND SERVICE WORKER. The outbound request that worker makes to your backend is recorded from CDP's Network domain on the worker's own session, which observes what the code ACTUALLY SENT and so needs neither of the two tricks a caller reaches for first. Both of those fail for ordinary JavaScript reasons rather than service-worker ones: a value bound at module top level is not visible from evaluate_script's global scope (plain lexical scoping, exactly the same in a page), and assigning self.fetch cannot rebind a fetch reference the module already captured. A worker capture has no reload to drive it: reload:true means 'listen for durationMs while the worker runs', so trigger the worker's work (another tool call, an extension message, an alarm) during the window; a worker woken by the call has already run its top-level code by the time the recorder attaches, so a request it makes at startup can be missed. SIDE EFFECT, deliberate and documented: the recording holds a CDP session on the worker, and that KEEPS THE WORKER ALIVE for as long as the capture runs — an MV3 worker is otherwise idle-evicted within seconds — and Chrome resumes evicting it once the capture stops.",
|
|
1220
1228
|
"inputSchema": {
|
|
1221
1229
|
"type": "object",
|
|
1222
1230
|
"properties": {
|
|
@@ -1226,7 +1234,7 @@ export const MANIFEST: ToolSpec[] = [
|
|
|
1226
1234
|
},
|
|
1227
1235
|
"target": {
|
|
1228
1236
|
"type": "string",
|
|
1229
|
-
"description": "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
1237
|
+
"description": "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>' | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js). The worker: arm is CHROME-ONLY (Capability 'worker.targets'), is refused on Firefox, and a bare worker target id works too."
|
|
1230
1238
|
},
|
|
1231
1239
|
"requestId": {
|
|
1232
1240
|
"type": "string",
|
|
@@ -1243,6 +1251,10 @@ export const MANIFEST: ToolSpec[] = [
|
|
|
1243
1251
|
"durationMs": {
|
|
1244
1252
|
"type": "number",
|
|
1245
1253
|
"description": "Capture window for the reload-driven body fetch, in milliseconds. Default 2500."
|
|
1254
|
+
},
|
|
1255
|
+
"wake": {
|
|
1256
|
+
"type": "boolean",
|
|
1257
|
+
"description": "Worker targets only, and only when a capture is being started (includeBody together with url). Default true: an MV3 extension service worker is idle-evicted after seconds of inactivity and then exists in NO target listing, so the capture asks Chrome to start it (ServiceWorker.startWorker over a page session) and then re-reads the target list to confirm it actually came up. Pass false to fail fast instead of starting anything. Rejected rather than ignored on a page target, on a bare target id (Chrome re-mints a worker's target id when it restarts, so only the url-substring form can name a worker that is not running), and on a read-only call (the buffer is keyed by target id, so a woken worker's buffer would be empty)."
|
|
1246
1258
|
}
|
|
1247
1259
|
},
|
|
1248
1260
|
"required": [],
|
package/src/mcp.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { toolAvailability } from "./capabilities.ts";
|
|
|
24
24
|
import { FIREFOX_TOOLS } from "./firefox-tools.ts";
|
|
25
25
|
import { leaseFromArgs, markLongLivedProcess, withLeaseScope } from "./leases.ts";
|
|
26
26
|
|
|
27
|
-
const VERSION = "1.9.
|
|
27
|
+
const VERSION = "1.9.1";
|
|
28
28
|
|
|
29
29
|
// --browser is read once at startup (MCP has no per-call notion of backend): explicit flag,
|
|
30
30
|
// else CDP_BROWSER env var, else "chrome" (zero behavior change for existing users/configs).
|