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/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). evaluate_script is the one tool that " + "accepts a worker: target.";
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 resolveTarget(target);
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
- await rec.conn.send("Page.enable");
2919
- await rec.conn.send("Page.reload", { ignoreCache: false });
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 resolveTarget(args.target);
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 resolveTarget(args.target);
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 resolveTarget(args.target);
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 resolveTarget(args.target);
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 also the one tool that can evaluate inside an MV3 extension's background SERVICE WORKER rather than a page: 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').",
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 this tool only; an MV3 worker that has been idle-evicted is started automatically first, see 'wake'."
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 capture and is matched by url ONLY (reload re-mints requestIds, so a carried-over requestId cannot fetch a body, so it returns metadata plus bodyUnavailableReason).",
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.0";
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.0",
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> for evaluate_script ONLY (Chrome only): an MV3 extension's background service
53
- worker, e.g. worker:<extension-id>. An idle-evicted worker is started first unless wake:false.
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 also the one tool that can evaluate inside an MV3 extension's background SERVICE WORKER rather than a page: 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').",
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 this tool only; an MV3 worker that has been idle-evicted is started automatically first, see 'wake'."
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 capture and is matched by url ONLY (reload re-mints requestIds, so a carried-over requestId cannot fetch a body, so it returns metadata plus bodyUnavailableReason).",
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.0";
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).