cdp-toolkit 1.4.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js CHANGED
@@ -13,6 +13,16 @@ function leaseTtlMs() {
13
13
  const raw = Number(process.env.CDP_LEASE_TTL_MS);
14
14
  return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_LEASE_TTL_MS;
15
15
  }
16
+ var longLived = false;
17
+ function markLongLivedProcess(value = true) {
18
+ longLived = value;
19
+ }
20
+ function requireLease() {
21
+ if (!longLived)
22
+ return false;
23
+ const raw = (process.env.CDP_REQUIRE_LEASE ?? "").trim().toLowerCase();
24
+ return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
25
+ }
16
26
  function defaultLabel() {
17
27
  return `pid-${process.pid}`;
18
28
  }
@@ -99,7 +109,8 @@ async function claimLease(backend, targetId, opts) {
99
109
  label: opts.label,
100
110
  createdAt: now,
101
111
  lastUsedAt: now,
102
- ttlMs: opts.ttlMs ?? leaseTtlMs()
112
+ ttlMs: opts.ttlMs ?? leaseTtlMs(),
113
+ auto: opts.auto === true
103
114
  };
104
115
  try {
105
116
  await writeFile(leaseFile(backend, targetId), JSON.stringify(record), { flag: "wx" });
@@ -175,8 +186,16 @@ async function assertLeaseOk(backend, targetId, ctx = {}) {
175
186
  const held = rec && !staleReason(rec, { now, liveIds: ctx.liveIds }) ? rec : undefined;
176
187
  const where = describeTarget(targetId, ctx);
177
188
  if (token === undefined) {
178
- if (!held)
189
+ if (!held) {
190
+ if (requireLease()) {
191
+ await claimLease(backend, targetId, { label: defaultLabel(), ttlMs: leaseTtlMs(), auto: true, now });
192
+ }
179
193
  return;
194
+ }
195
+ if (held.auto === true && held.pid === process.pid) {
196
+ await touchLease(held, now);
197
+ return;
198
+ }
180
199
  throw new LeaseConflictError(`${where} is leased by '${held.label}' (pid ${held.pid}). Pass that lease's token as the 'lease' argument, or call release_page to free it. list_leases shows every active lease.`, targetId, held.label);
181
200
  }
182
201
  const parts = tokenParts(token);
@@ -184,8 +203,12 @@ async function assertLeaseOk(backend, targetId, ctx = {}) {
184
203
  throw new LeaseConflictError(`malformed lease token for ${where}. Pass back the token claim_page returned, never a constructed one.`, targetId);
185
204
  }
186
205
  if (parts.backend !== backend || parts.targetId !== targetId) {
187
- if (!held)
206
+ if (!held) {
207
+ if (requireLease()) {
208
+ await claimLease(backend, targetId, { label: defaultLabel(), ttlMs: leaseTtlMs(), auto: true, now });
209
+ }
188
210
  return;
211
+ }
189
212
  throw new LeaseConflictError(`the lease token you passed is for ${parts.backend} target '${parts.targetId}', not ${where}, which is leased by '${held.label}'. Resolve the tab you actually hold, or claim this one.`, targetId, held.label);
190
213
  }
191
214
  if (!held) {
@@ -682,6 +705,8 @@ var REQUIRED_CAPABILITIES = {
682
705
  performance_trace: ["trace.performance"],
683
706
  take_heapsnapshot: ["heap.snapshot"],
684
707
  lighthouse_audit: ["audit.lighthouse"],
708
+ start_screen_recording: ["capture.screencast"],
709
+ stop_screen_recording: ["capture.screencast"],
685
710
  mock_request: ["network.intercept"],
686
711
  list_mocks: ["network.intercept"],
687
712
  clear_mocks: ["network.intercept"]
@@ -937,7 +962,8 @@ var CDP_CAPABILITIES = new Set([
937
962
  "snapshot.accessibilityTree",
938
963
  "input.insertTextAtomic",
939
964
  "locate.text",
940
- "locate.xpath"
965
+ "locate.xpath",
966
+ "capture.screencast"
941
967
  ]);
942
968
 
943
969
  class CdpPageDriver {
@@ -1426,6 +1452,20 @@ async function recordOrigin(backend, targetId, opts = {}) {
1426
1452
  await writeFile2(file, JSON.stringify(record), { flag: "wx" });
1427
1453
  } catch {}
1428
1454
  }
1455
+ async function readOrigin(backend, targetId) {
1456
+ let raw;
1457
+ try {
1458
+ raw = await readFile2(originFile(backend, targetId), "utf8");
1459
+ } catch {
1460
+ return;
1461
+ }
1462
+ try {
1463
+ const rec = JSON.parse(raw);
1464
+ if (typeof rec?.targetId === "string" && typeof rec?.label === "string" && typeof rec?.createdAt === "number")
1465
+ return rec;
1466
+ } catch {}
1467
+ return;
1468
+ }
1429
1469
  async function listOrigins(opts = {}) {
1430
1470
  let names = [];
1431
1471
  try {
@@ -1483,59 +1523,43 @@ async function newTrackedPage(driver, backend, opts = {}) {
1483
1523
  return page;
1484
1524
  }
1485
1525
 
1486
- // src/leases-tools.ts
1487
- class LeaseToolError extends Error {
1488
- }
1489
- function backendOf(driver) {
1490
- return driver.scheme === "bidi" ? "firefox" : "chrome";
1491
- }
1492
- async function claimPage(driver, args = {}) {
1493
- const backend = backendOf(driver);
1494
- const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
1495
- const pages = await driver.listPages();
1496
- let targetId = args.targetId;
1497
- let url = args.url ?? "about:blank";
1498
- if (targetId === undefined || targetId === "") {
1499
- const page = await newTrackedPage(driver, backend, { url: args.url, label });
1500
- targetId = page.id;
1501
- url = page.url;
1502
- } else {
1503
- const existing = pages.find((p) => p.id === targetId);
1504
- if (!existing)
1505
- throw new LeaseToolError(`claim_page: no page target with id '${targetId}'`);
1506
- url = existing.url;
1526
+ // src/reap.ts
1527
+ function staleAgentTabs(input) {
1528
+ const live = new Set(input.livePageIds);
1529
+ const out = [];
1530
+ for (const row of input.leases) {
1531
+ if (row.backend !== input.backend)
1532
+ continue;
1533
+ if (row.unreadable !== undefined)
1534
+ continue;
1535
+ if (row.stale !== "dead-pid" && row.stale !== "expired")
1536
+ continue;
1537
+ if (!live.has(row.targetId))
1538
+ continue;
1539
+ const origin = input.origins.get(row.targetId);
1540
+ if (origin === undefined || origin.unreadable !== undefined)
1541
+ continue;
1542
+ out.push({ targetId: row.targetId, label: row.label, reason: row.stale });
1507
1543
  }
1508
- const liveIds = [...pages.map((p) => p.id), targetId];
1509
- const { record, token } = await claimLease(backend, targetId, {
1510
- label,
1511
- ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
1512
- liveIds
1513
- });
1514
- return {
1515
- lease: token,
1516
- targetId,
1517
- url,
1518
- label: record.label,
1519
- ttlMs: record.ttlMs,
1520
- expiresAt: record.lastUsedAt + record.ttlMs
1521
- };
1544
+ return out;
1522
1545
  }
1523
- async function releasePage(_driver, args) {
1524
- if (typeof args.lease !== "string" || !args.lease.length) {
1525
- throw new LeaseToolError("release_page requires the 'lease' token returned by claim_page");
1546
+ async function reapStaleAgentTabs(driver, backend) {
1547
+ const livePageIds = (await driver.listPages()).map((p) => p.id);
1548
+ const [origins, leases] = await Promise.all([
1549
+ originIndex(backend, undefined),
1550
+ listLeases({ liveIds: livePageIds, liveBackend: backend }).catch(() => [])
1551
+ ]);
1552
+ const candidates = staleAgentTabs({ backend, livePageIds, origins, leases });
1553
+ const closed = [];
1554
+ for (const c of candidates) {
1555
+ const res = await driver.closePage(c.targetId).catch(() => ({ success: false }));
1556
+ if (res.success !== true)
1557
+ continue;
1558
+ await releaseLeaseFor(backend, c.targetId);
1559
+ closed.push(c);
1526
1560
  }
1527
- return releaseLease(args.lease);
1561
+ return closed;
1528
1562
  }
1529
- async function listLeasesTool(driver, _args = {}) {
1530
- const liveIds = (await driver.listPages()).map((p) => p.id);
1531
- const leases = await listLeases({ liveIds, liveBackend: backendOf(driver) });
1532
- return { leases, count: leases.length };
1533
- }
1534
- var LEASE_TOOLS = {
1535
- claim_page: claimPage,
1536
- release_page: releasePage,
1537
- list_leases: listLeasesTool
1538
- };
1539
1563
 
1540
1564
  // src/shared-tools.ts
1541
1565
  import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
@@ -1548,7 +1572,7 @@ class SharedToolError extends Error {
1548
1572
  function target3(p) {
1549
1573
  return { id: p.id, url: p.url, title: p.title };
1550
1574
  }
1551
- function backendOf2(driver) {
1575
+ function backendOf(driver) {
1552
1576
  return driver.scheme === "bidi" ? "firefox" : "chrome";
1553
1577
  }
1554
1578
  async function withPage2(driver, target, fn) {
@@ -1562,7 +1586,7 @@ async function withPage2(driver, target, fn) {
1562
1586
  async function resolvePage(driver, selector) {
1563
1587
  const pages = await driver.listPages();
1564
1588
  const hit = await pickPage(driver, pages, selector);
1565
- await assertLeaseOk(backendOf2(driver), hit.id, { url: hit.url, title: hit.title });
1589
+ await assertLeaseOk(backendOf(driver), hit.id, { url: hit.url, title: hit.title });
1566
1590
  return hit;
1567
1591
  }
1568
1592
  async function pickPage(driver, pages, selector) {
@@ -1617,8 +1641,10 @@ async function reapSet(driver, pages, all) {
1617
1641
  }
1618
1642
  }
1619
1643
  async function listPages(driver, args = {}) {
1620
- const pages = await driver.listPages({ all: args.all });
1621
- const ledger = await originIndex(backendOf2(driver), await reapSet(driver, pages, args.all));
1644
+ const reaped = requireLease() ? await reapStaleAgentTabs(driver, backendOf(driver)) : [];
1645
+ const reapedIds = new Set(reaped.map((r) => r.targetId));
1646
+ const pages = (await driver.listPages({ all: args.all })).filter((p) => !reapedIds.has(p.id));
1647
+ const ledger = await originIndex(backendOf(driver), await reapSet(driver, pages, args.all));
1622
1648
  const annotated = pages.map((p) => {
1623
1649
  const rec = ledger.get(p.id);
1624
1650
  if (!rec)
@@ -1627,16 +1653,18 @@ async function listPages(driver, args = {}) {
1627
1653
  return { ...p, origin: "unknown", originUnreadable: rec.unreadable };
1628
1654
  return { ...p, origin: "agent", label: rec.label, createdAt: rec.createdAt };
1629
1655
  });
1630
- return { pages: annotated, count: annotated.length };
1656
+ return { pages: annotated, count: annotated.length, ...reaped.length ? { reaped } : {} };
1631
1657
  }
1632
1658
  async function newPage(driver, args = {}) {
1633
1659
  const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
1634
- const p = await newTrackedPage(driver, backendOf2(driver), { url: args.url, label });
1635
- if (args.claim !== true)
1660
+ const p = await newTrackedPage(driver, backendOf(driver), { url: args.url, label });
1661
+ const explicit = args.claim === true;
1662
+ if (!explicit && !requireLease())
1636
1663
  return { targetId: p.id, url: p.url };
1637
- const { record, token } = await claimLease(backendOf2(driver), p.id, {
1664
+ const { record, token } = await claimLease(backendOf(driver), p.id, {
1638
1665
  label,
1639
- ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs()
1666
+ ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
1667
+ auto: !explicit
1640
1668
  });
1641
1669
  return { targetId: p.id, url: p.url, lease: token, label: record.label, expiresAt: record.lastUsedAt + record.ttlMs };
1642
1670
  }
@@ -1647,7 +1675,7 @@ async function closePage(driver, args) {
1647
1675
  const res = await driver.closePage(p.id);
1648
1676
  if (!res.success)
1649
1677
  return { closed: p.id, success: false, leaseReleased: false };
1650
- const released = await releaseLeaseFor(backendOf2(driver), p.id);
1678
+ const released = await releaseLeaseFor(backendOf(driver), p.id);
1651
1679
  return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
1652
1680
  }
1653
1681
  async function writeSelectedFile(id) {
@@ -1972,6 +2000,103 @@ var SHARED_TOOLS = {
1972
2000
  handle_dialog: handleDialog
1973
2001
  };
1974
2002
 
2003
+ // src/leases-tools.ts
2004
+ class LeaseToolError extends Error {
2005
+ }
2006
+ function backendOf2(driver) {
2007
+ return driver.scheme === "bidi" ? "firefox" : "chrome";
2008
+ }
2009
+ async function claimPage(driver, args = {}) {
2010
+ const hasTarget = typeof args.target === "string" && args.target.length > 0;
2011
+ const hasTargetId = typeof args.targetId === "string" && args.targetId.length > 0;
2012
+ if (hasTarget && hasTargetId) {
2013
+ throw new LeaseToolError("claim_page takes at most one of 'target' (any selector: active | index:N | url:<substr> | title:<substr> | <targetId>) or 'targetId' (an exact target id, kept for back-compat). Pass 'target' alone to claim an open tab, or neither to open a fresh one.");
2014
+ }
2015
+ const backend = backendOf2(driver);
2016
+ const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
2017
+ const pages = await driver.listPages();
2018
+ let targetId;
2019
+ let url;
2020
+ let opened;
2021
+ if (hasTarget) {
2022
+ const hit = await pickPage(driver, pages, args.target).catch((err) => {
2023
+ throw new LeaseToolError(`claim_page: no open tab matches target '${args.target}' (${err.message}). No tab was opened: claim_page never creates one when 'target' is given.`);
2024
+ });
2025
+ targetId = hit.id;
2026
+ url = hit.url;
2027
+ opened = false;
2028
+ } else if (hasTargetId) {
2029
+ const existing = pages.find((p) => p.id === args.targetId);
2030
+ if (!existing)
2031
+ throw new LeaseToolError(`claim_page: no page target with id '${args.targetId}'`);
2032
+ targetId = existing.id;
2033
+ url = existing.url;
2034
+ opened = false;
2035
+ } else {
2036
+ const page = await newTrackedPage(driver, backend, { url: args.url, label });
2037
+ targetId = page.id;
2038
+ url = page.url;
2039
+ opened = true;
2040
+ }
2041
+ const liveIds = [...pages.map((p) => p.id), targetId];
2042
+ const { record, token } = await claimLease(backend, targetId, {
2043
+ label,
2044
+ ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
2045
+ liveIds
2046
+ });
2047
+ return {
2048
+ lease: token,
2049
+ targetId,
2050
+ url,
2051
+ label: record.label,
2052
+ ttlMs: record.ttlMs,
2053
+ expiresAt: record.lastUsedAt + record.ttlMs,
2054
+ opened
2055
+ };
2056
+ }
2057
+ async function releasePage(driver, args = {}) {
2058
+ const hasLease = typeof args.lease === "string" && args.lease.length > 0;
2059
+ const hasTarget = typeof args.target === "string" && args.target.length > 0;
2060
+ if (hasLease === hasTarget) {
2061
+ throw new LeaseToolError("release_page takes exactly one of 'lease' (the token claim_page returned) or 'target' (a selector for a tab this process holds)");
2062
+ }
2063
+ const backend = backendOf2(driver);
2064
+ let targetId;
2065
+ let released;
2066
+ if (hasLease) {
2067
+ const parts = tokenParts(args.lease);
2068
+ if (!parts)
2069
+ return { released: false, closed: false };
2070
+ targetId = parts.targetId;
2071
+ released = (await releaseLease(args.lease)).released;
2072
+ } else {
2073
+ const page = await resolvePage(driver, args.target);
2074
+ targetId = page.id;
2075
+ released = (await releaseLeaseFor(backend, targetId)).released;
2076
+ }
2077
+ if (!released)
2078
+ return { released: false, closed: false, ...hasTarget ? { targetId } : {} };
2079
+ const shouldClose = args.close === false ? false : args.close === true ? true : await readOrigin(backend, targetId) !== undefined;
2080
+ if (!shouldClose)
2081
+ return { released: true, closed: false, targetId };
2082
+ const res = await driver.closePage(targetId).catch(() => ({ success: false }));
2083
+ return { released: true, closed: res.success === true, targetId };
2084
+ }
2085
+ async function listLeasesTool(driver, _args = {}) {
2086
+ const backend = backendOf2(driver);
2087
+ const reaped = requireLease() ? await reapStaleAgentTabs(driver, backend) : [];
2088
+ const reapedIds = new Set(reaped.map((r) => r.targetId));
2089
+ const liveIds = (await driver.listPages()).map((p) => p.id);
2090
+ const all = await listLeases({ liveIds, liveBackend: backend });
2091
+ const leases = all.filter((l) => !(l.backend === backend && reapedIds.has(l.targetId)));
2092
+ return { leases, count: leases.length, ...reaped.length ? { reaped } : {} };
2093
+ }
2094
+ var LEASE_TOOLS = {
2095
+ claim_page: claimPage,
2096
+ release_page: releasePage,
2097
+ list_leases: listLeasesTool
2098
+ };
2099
+
1975
2100
  // src/tools/console.ts
1976
2101
  import { readFile as readFile3 } from "node:fs/promises";
1977
2102
 
@@ -2579,11 +2704,375 @@ async function performanceAnalyzeInsight(args = {}) {
2579
2704
  return { ...analyzeEvents(events), tracePath: args.tracePath };
2580
2705
  }
2581
2706
 
2582
- // src/tools/heap.ts
2583
- import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
2707
+ // src/tools/screencast.ts
2708
+ import { spawn } from "node:child_process";
2709
+ import { mkdir as mkdir6, rm, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
2584
2710
  import { join as join5 } from "node:path";
2585
2711
  var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
2586
- function stamp3(targetId) {
2712
+ var FFMPEG_BIN = process.env.CDP_FFMPEG ?? "ffmpeg";
2713
+ function frameDurationsMs(ledger, stopWallclockMs) {
2714
+ return ledger.map((entry, i) => {
2715
+ const next = i + 1 < ledger.length ? ledger[i + 1].timestamp : stopWallclockMs;
2716
+ return Math.max(1, Math.round(next - entry.timestamp));
2717
+ });
2718
+ }
2719
+ var CONCAT_GRID_MS = 40;
2720
+ function coalesceToConcatGrid(ledger, stopWallclockMs, gridMs = CONCAT_GRID_MS) {
2721
+ if (!ledger.length)
2722
+ return [];
2723
+ const raw = frameDurationsMs(ledger, stopWallclockMs);
2724
+ let elapsed = 0;
2725
+ const kept = [];
2726
+ let lastSlot = -1;
2727
+ for (let i = 0;i < ledger.length; i++) {
2728
+ const slot = Math.round(elapsed / gridMs);
2729
+ if (slot > lastSlot) {
2730
+ kept.push({ file: ledger[i].file, slot });
2731
+ lastSlot = slot;
2732
+ }
2733
+ elapsed += raw[i];
2734
+ }
2735
+ const endSlot = Math.max(lastSlot + 1, Math.round(elapsed / gridMs));
2736
+ return kept.map((frame, i) => ({
2737
+ file: frame.file,
2738
+ durationMs: ((i + 1 < kept.length ? kept[i + 1].slot : endSlot) - frame.slot) * gridMs
2739
+ }));
2740
+ }
2741
+ function buildConcatManifest(frames) {
2742
+ if (!frames.length)
2743
+ throw new CdpError("cannot build a concat manifest from an empty frame ledger");
2744
+ const lines = ["ffconcat version 1.0"];
2745
+ for (const frame of frames) {
2746
+ lines.push(`file ${frame.file}`);
2747
+ lines.push(`duration ${(frame.durationMs / 1000).toFixed(6)}`);
2748
+ }
2749
+ lines.push(`file ${frames[frames.length - 1].file}`);
2750
+ return `${lines.join(`
2751
+ `)}
2752
+ `;
2753
+ }
2754
+ function evenDimensions(width, height) {
2755
+ return { width: Math.max(0, Math.trunc(width / 2) * 2), height: Math.max(0, Math.trunc(height / 2) * 2) };
2756
+ }
2757
+ function pngSize(buf) {
2758
+ if (buf.length < 24)
2759
+ return;
2760
+ if (buf.readUInt32BE(0) !== 2303741511 || buf.readUInt32BE(4) !== 218765834)
2761
+ return;
2762
+ if (buf.toString("latin1", 12, 16) !== "IHDR")
2763
+ return;
2764
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
2765
+ }
2766
+ var JPEG_SOF = new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
2767
+ function jpegSize(buf) {
2768
+ if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216)
2769
+ return;
2770
+ let i = 2;
2771
+ while (i + 3 < buf.length) {
2772
+ if (buf[i] !== 255) {
2773
+ i += 1;
2774
+ continue;
2775
+ }
2776
+ const marker = buf[i + 1];
2777
+ if (marker === 255) {
2778
+ i += 1;
2779
+ continue;
2780
+ }
2781
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
2782
+ i += 2;
2783
+ continue;
2784
+ }
2785
+ if (i + 3 >= buf.length)
2786
+ return;
2787
+ const segLen = buf.readUInt16BE(i + 2);
2788
+ if (JPEG_SOF.has(marker)) {
2789
+ if (i + 9 > buf.length)
2790
+ return;
2791
+ return { width: buf.readUInt16BE(i + 7), height: buf.readUInt16BE(i + 5) };
2792
+ }
2793
+ if (segLen < 2)
2794
+ return;
2795
+ i += 2 + segLen;
2796
+ }
2797
+ return;
2798
+ }
2799
+ function imageSize(buf) {
2800
+ return pngSize(buf) ?? jpegSize(buf);
2801
+ }
2802
+ var ENCODER_LADDER = [
2803
+ { encoder: "hevc_videotoolbox", codec: "hevc", tag: "hvc1" },
2804
+ { encoder: "h264_videotoolbox", codec: "h264" },
2805
+ { encoder: "libx265", codec: "hevc", tag: "hvc1" },
2806
+ { encoder: "libx264", codec: "h264" }
2807
+ ];
2808
+ function ffmpegEncoderNames(encodersOutput) {
2809
+ const names = new Set;
2810
+ for (const line of encodersOutput.split(`
2811
+ `)) {
2812
+ const m = /^\s*[VAS][A-Z.]{5}\s+(\S+)/.exec(line);
2813
+ if (m)
2814
+ names.add(m[1]);
2815
+ }
2816
+ return names;
2817
+ }
2818
+ function selectEncoder(encodersOutput) {
2819
+ const available = ffmpegEncoderNames(encodersOutput);
2820
+ for (const candidate of ENCODER_LADDER) {
2821
+ if (available.has(candidate.encoder))
2822
+ return candidate;
2823
+ }
2824
+ throw new CdpError(`ffmpeg reports none of the supported video encoders (${ENCODER_LADDER.map((c) => c.encoder).join(", ")}). ` + "Install a full ffmpeg build: 'brew install ffmpeg' (macOS) or 'apt install ffmpeg' (Debian/Ubuntu).");
2825
+ }
2826
+ function buildFfmpegArgs(manifestPath, outPath, choice) {
2827
+ return [
2828
+ "-hide_banner",
2829
+ "-loglevel",
2830
+ "error",
2831
+ "-y",
2832
+ "-f",
2833
+ "concat",
2834
+ "-safe",
2835
+ "0",
2836
+ "-i",
2837
+ manifestPath,
2838
+ "-vsync",
2839
+ "vfr",
2840
+ "-vf",
2841
+ "scale=trunc(iw/2)*2:trunc(ih/2)*2",
2842
+ "-c:v",
2843
+ choice.encoder,
2844
+ ...choice.tag ? ["-tag:v", choice.tag] : [],
2845
+ "-pix_fmt",
2846
+ "yuv420p",
2847
+ "-movflags",
2848
+ "+faststart",
2849
+ "-an",
2850
+ outPath
2851
+ ];
2852
+ }
2853
+ function run(bin, args) {
2854
+ return new Promise((resolve, reject) => {
2855
+ const child = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
2856
+ let stdout = "";
2857
+ let stderr = "";
2858
+ child.stdout?.on("data", (chunk) => {
2859
+ stdout += chunk.toString("utf8");
2860
+ });
2861
+ child.stderr?.on("data", (chunk) => {
2862
+ stderr += chunk.toString("utf8");
2863
+ });
2864
+ child.on("error", reject);
2865
+ child.on("close", (code) => resolve({ code, stdout, stderr }));
2866
+ });
2867
+ }
2868
+ var encoderProbe;
2869
+ var FFMPEG_MISSING = `ffmpeg is required to encode a screen recording but '${FFMPEG_BIN}' could not be executed. ` + "Install it with 'brew install ffmpeg' (macOS) or 'apt install ffmpeg' (Debian/Ubuntu), " + "or point CDP_FFMPEG at the binary.";
2870
+ async function probeEncoder() {
2871
+ if (!encoderProbe) {
2872
+ encoderProbe = (async () => {
2873
+ let res;
2874
+ try {
2875
+ res = await run(FFMPEG_BIN, ["-hide_banner", "-encoders"]);
2876
+ } catch (err) {
2877
+ throw new CdpError(`${FFMPEG_MISSING} (${err.message})`);
2878
+ }
2879
+ if (res.code !== 0)
2880
+ throw new CdpError(`${FFMPEG_MISSING} ('-encoders' exited ${res.code}: ${res.stderr.trim().slice(0, 300)})`);
2881
+ return selectEncoder(`${res.stdout}
2882
+ ${res.stderr}`);
2883
+ })();
2884
+ encoderProbe.catch(() => {
2885
+ encoderProbe = undefined;
2886
+ });
2887
+ }
2888
+ return encoderProbe;
2889
+ }
2890
+ var liveRecordings = new Map;
2891
+ function shortId2(id) {
2892
+ return id.slice(0, 8);
2893
+ }
2894
+ function assertNotRecording(targetId, activeIds) {
2895
+ if (activeIds.includes(targetId)) {
2896
+ throw new CdpError(`a screen recording is already in progress for target ${shortId2(targetId)}; call stop_screen_recording first`);
2897
+ }
2898
+ }
2899
+ function chooseRecordingId(activeIds, selector) {
2900
+ if (!activeIds.length) {
2901
+ throw new CdpError("no screen recording is in progress in this process. start_screen_recording parks a live CDP connection in memory, so a recording cannot be stopped from a different process than the one that started it (the CLI is one process per call: use the MCP server for the pair).");
2902
+ }
2903
+ if (selector === undefined || selector === "active") {
2904
+ if (activeIds.length === 1)
2905
+ return activeIds[0];
2906
+ throw new CdpError(`${activeIds.length} screen recordings are in progress (targets: ${activeIds.map(shortId2).join(", ")}); pass 'target' to say which one to stop`);
2907
+ }
2908
+ return activeIds.find((id) => id === selector);
2909
+ }
2910
+ function stamp3() {
2911
+ return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
2912
+ }
2913
+ function target32(t) {
2914
+ return { id: t.id, url: t.url, title: t.title };
2915
+ }
2916
+ async function startScreenRecording(args = {}) {
2917
+ const format = args.format ?? "jpeg";
2918
+ if (format !== "jpeg" && format !== "png")
2919
+ throw new CdpError("start_screen_recording: format must be 'jpeg' or 'png'");
2920
+ if (args.quality !== undefined && (!Number.isFinite(args.quality) || args.quality < 0 || args.quality > 100)) {
2921
+ throw new CdpError("start_screen_recording: quality must be a number between 0 and 100 (jpeg only)");
2922
+ }
2923
+ const encoder = await probeEncoder();
2924
+ const { conn, target } = await openPage(args.target);
2925
+ try {
2926
+ assertNotRecording(target.id, [...liveRecordings.keys()]);
2927
+ } catch (err) {
2928
+ conn.close();
2929
+ throw err;
2930
+ }
2931
+ const spoolDir = join5(ARTIFACT_DIR4, `screencast-${shortId2(target.id)}-${stamp3()}`);
2932
+ try {
2933
+ await mkdir6(spoolDir, { recursive: true });
2934
+ } catch (err) {
2935
+ conn.close();
2936
+ throw new CdpError(`cannot create the screencast spool directory '${spoolDir}': ${err.message}`);
2937
+ }
2938
+ const ext = format === "png" ? "png" : "jpg";
2939
+ const rec = {
2940
+ conn,
2941
+ target,
2942
+ spoolDir,
2943
+ ledger: [],
2944
+ startedAt: Date.now(),
2945
+ format,
2946
+ encoder,
2947
+ writes: Promise.resolve(),
2948
+ dropped: 0,
2949
+ unwritten: new Set,
2950
+ off: () => {}
2951
+ };
2952
+ let seq = 0;
2953
+ rec.off = conn.on("Page.screencastFrame", (params) => {
2954
+ const sessionId = params.sessionId;
2955
+ if (typeof sessionId === "number") {
2956
+ conn.send("Page.screencastFrameAck", { sessionId }).catch(() => {});
2957
+ }
2958
+ const data = params.data;
2959
+ if (typeof data !== "string" || !data.length) {
2960
+ rec.dropped += 1;
2961
+ return;
2962
+ }
2963
+ const meta = params.metadata;
2964
+ const timestamp = typeof meta?.timestamp === "number" ? Math.round(meta.timestamp * 1000) : Date.now();
2965
+ const buf = Buffer.from(data, "base64");
2966
+ const size = imageSize(buf);
2967
+ if (!size || size.width <= 0 || size.height <= 0) {
2968
+ rec.dropped += 1;
2969
+ return;
2970
+ }
2971
+ seq += 1;
2972
+ const file = `frame-${String(seq).padStart(6, "0")}.${ext}`;
2973
+ rec.ledger.push({ file, timestamp, width: size.width, height: size.height });
2974
+ rec.writes = rec.writes.then(() => writeFile6(join5(spoolDir, file), buf)).catch(() => {
2975
+ rec.dropped += 1;
2976
+ rec.unwritten.add(file);
2977
+ });
2978
+ });
2979
+ try {
2980
+ await conn.send("Page.enable");
2981
+ if (args.bringToFront)
2982
+ await conn.send("Page.bringToFront").catch(() => {});
2983
+ const params = { format };
2984
+ if (format === "jpeg" && args.quality !== undefined)
2985
+ params.quality = args.quality;
2986
+ if (args.maxWidth !== undefined)
2987
+ params.maxWidth = args.maxWidth;
2988
+ if (args.maxHeight !== undefined)
2989
+ params.maxHeight = args.maxHeight;
2990
+ if (args.everyNthFrame !== undefined)
2991
+ params.everyNthFrame = args.everyNthFrame;
2992
+ await conn.send("Page.startScreencast", params);
2993
+ } catch (err) {
2994
+ rec.off();
2995
+ conn.close();
2996
+ await rm(spoolDir, { recursive: true, force: true }).catch(() => {});
2997
+ throw err;
2998
+ }
2999
+ liveRecordings.set(target.id, rec);
3000
+ return {
3001
+ target: target32(target),
3002
+ format,
3003
+ spoolDir,
3004
+ encoder: encoder.encoder,
3005
+ codec: encoder.codec,
3006
+ startedAt: rec.startedAt,
3007
+ note: "Recording. Call stop_screen_recording in THIS process to encode the MP4; frames only arrive on repaint, so a still page yields one long-held frame."
3008
+ };
3009
+ }
3010
+ async function pickRecording(selector) {
3011
+ const chosen = chooseRecordingId([...liveRecordings.keys()], selector);
3012
+ if (chosen)
3013
+ return liveRecordings.get(chosen);
3014
+ const { conn, target } = await openPage(selector);
3015
+ conn.close();
3016
+ const hit = liveRecordings.get(target.id);
3017
+ if (!hit) {
3018
+ throw new CdpError(`no screen recording is in progress for target ${shortId2(target.id)} (recording: ${[...liveRecordings.keys()].map(shortId2).join(", ") || "none"})`);
3019
+ }
3020
+ return hit;
3021
+ }
3022
+ async function stopScreenRecording(args = {}) {
3023
+ const rec = await pickRecording(args.target);
3024
+ rec.off();
3025
+ try {
3026
+ await rec.conn.send("Page.stopScreencast");
3027
+ } catch {}
3028
+ const stoppedAt = Date.now();
3029
+ await rec.writes.catch(() => {});
3030
+ rec.conn.close();
3031
+ liveRecordings.delete(rec.target.id);
3032
+ const ledger = rec.unwritten.size ? rec.ledger.filter((entry) => !rec.unwritten.has(entry.file)) : rec.ledger;
3033
+ if (!ledger.length) {
3034
+ await rm(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
3035
+ throw new CdpError(`screen recording captured 0 frames for target ${shortId2(rec.target.id)}. Chrome emits a screencast frame only on repaint: a fully backgrounded or occluded tab may never paint. Retry with bringToFront:true, or drive the page while recording.`);
3036
+ }
3037
+ await mkdir6(ARTIFACT_DIR4, { recursive: true });
3038
+ const outPath = args.savePath ?? join5(ARTIFACT_DIR4, `screen-recording-${shortId2(rec.target.id)}-${stamp3()}.mp4`);
3039
+ const frames = coalesceToConcatGrid(ledger, stoppedAt);
3040
+ const manifestPath = join5(rec.spoolDir, "frames.ffconcat");
3041
+ await writeFile6(manifestPath, buildConcatManifest(frames), "utf8");
3042
+ const durationMs = frames.reduce((total, f) => total + f.durationMs, 0);
3043
+ const { width, height } = evenDimensions(ledger[0].width, ledger[0].height);
3044
+ let res;
3045
+ try {
3046
+ res = await run(FFMPEG_BIN, buildFfmpegArgs(manifestPath, outPath, rec.encoder));
3047
+ } catch (err) {
3048
+ throw new CdpError(`${FFMPEG_MISSING} Spooled frames kept at ${rec.spoolDir}. (${err.message})`);
3049
+ }
3050
+ if (res.code !== 0) {
3051
+ throw new CdpError(`ffmpeg (${rec.encoder.encoder}) exited ${res.code} encoding ${frames.length} frames. Spooled frames kept at ${rec.spoolDir}, re-run by hand with: ` + `${FFMPEG_BIN} ${buildFfmpegArgs(manifestPath, outPath, rec.encoder).join(" ")}
3052
+ ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
3053
+ }
3054
+ const { size } = await stat2(outPath);
3055
+ await rm(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
3056
+ return {
3057
+ path: outPath,
3058
+ bytes: size,
3059
+ durationMs,
3060
+ frameCount: ledger.length,
3061
+ encodedFrames: frames.length,
3062
+ codec: rec.encoder.codec,
3063
+ encoder: rec.encoder.encoder,
3064
+ width,
3065
+ height,
3066
+ droppedFrames: rec.dropped,
3067
+ target: target32(rec.target)
3068
+ };
3069
+ }
3070
+
3071
+ // src/tools/heap.ts
3072
+ import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
3073
+ import { join as join6 } from "node:path";
3074
+ var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3075
+ function stamp4(targetId) {
2587
3076
  const short = targetId.slice(0, 8);
2588
3077
  const iso = new Date().toISOString().replace(/[:.]/g, "-");
2589
3078
  return `${short}-${iso}`;
@@ -2606,9 +3095,9 @@ async function takeHeapsnapshot(args = {}) {
2606
3095
  if (snapshot.length === 0) {
2607
3096
  throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
2608
3097
  }
2609
- await mkdir6(ARTIFACT_DIR4, { recursive: true });
2610
- const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join5(ARTIFACT_DIR4, args.savePath) : join5(ARTIFACT_DIR4, `take_heapsnapshot-${stamp3(target.id)}.heapsnapshot`);
2611
- await writeFile6(path, snapshot, "utf8");
3098
+ await mkdir7(ARTIFACT_DIR5, { recursive: true });
3099
+ const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join6(ARTIFACT_DIR5, args.savePath) : join6(ARTIFACT_DIR5, `take_heapsnapshot-${stamp4(target.id)}.heapsnapshot`);
3100
+ await writeFile7(path, snapshot, "utf8");
2612
3101
  const bytes = Buffer.byteLength(snapshot, "utf8");
2613
3102
  return {
2614
3103
  path,
@@ -2620,15 +3109,15 @@ async function takeHeapsnapshot(args = {}) {
2620
3109
  }
2621
3110
 
2622
3111
  // src/tools/lighthouse.ts
2623
- import { spawn } from "node:child_process";
2624
- import { mkdir as mkdir7, readFile as readFile6, stat as stat2 } from "node:fs/promises";
2625
- var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3112
+ import { spawn as spawn2 } from "node:child_process";
3113
+ import { mkdir as mkdir8, readFile as readFile6, stat as stat3 } from "node:fs/promises";
3114
+ var ARTIFACT_DIR6 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
2626
3115
  function cdpPort() {
2627
3116
  const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
2628
3117
  const m = base.match(/:(\d+)/);
2629
3118
  return m?.[1] ? Number(m[1]) : 9222;
2630
3119
  }
2631
- function stamp4() {
3120
+ function stamp5() {
2632
3121
  return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
2633
3122
  }
2634
3123
  function shortHost(url) {
@@ -2645,7 +3134,7 @@ function runProcess(cmd, args, opts) {
2645
3134
  let stdout = "";
2646
3135
  let stderr = "";
2647
3136
  let settled = false;
2648
- const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
3137
+ const child = spawn2(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
2649
3138
  const timer = setTimeout(() => {
2650
3139
  if (settled)
2651
3140
  return;
@@ -2682,8 +3171,8 @@ async function lighthouseAudit(args) {
2682
3171
  const url = args.url.trim();
2683
3172
  const timeoutMs = args.timeoutMs ?? 120000;
2684
3173
  const port = cdpPort();
2685
- await mkdir7(ARTIFACT_DIR5, { recursive: true });
2686
- const outPath = args.savePath ?? `${ARTIFACT_DIR5}/lighthouse-${shortHost(url)}-${stamp4()}.json`;
3174
+ await mkdir8(ARTIFACT_DIR6, { recursive: true });
3175
+ const outPath = args.savePath ?? `${ARTIFACT_DIR6}/lighthouse-${shortHost(url)}-${stamp5()}.json`;
2687
3176
  const formFactor = args.formFactor ?? "desktop";
2688
3177
  const lhArgs = [
2689
3178
  "--yes",
@@ -2738,7 +3227,7 @@ async function lighthouseAudit(args) {
2738
3227
  if (Object.keys(categories).length === 0) {
2739
3228
  throw new CdpError(`lighthouse_audit report contained no category scores (exit code ${res.code}). stderr: ${truncate(res.stderr)}`);
2740
3229
  }
2741
- const { size } = await stat2(outPath);
3230
+ const { size } = await stat3(outPath);
2742
3231
  return {
2743
3232
  path: outPath,
2744
3233
  bytes: size,
@@ -2792,6 +3281,8 @@ var TOOLS = {
2792
3281
  performance_stop_trace: performanceStopTrace,
2793
3282
  performance_analyze_insight: performanceAnalyzeInsight,
2794
3283
  performance_trace: performanceTrace,
3284
+ start_screen_recording: startScreenRecording,
3285
+ stop_screen_recording: stopScreenRecording,
2795
3286
  take_heapsnapshot: takeHeapsnapshot,
2796
3287
  lighthouse_audit: lighthouseAudit,
2797
3288
  mock_request: mockRequest,
@@ -2812,13 +3303,13 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
2812
3303
  var MANIFEST = [
2813
3304
  {
2814
3305
  name: "list_pages",
2815
- description: "Enumerate browser page targets via the CDP browser endpoint (GET /json/list). By default returns only page-type tabs; set 'all' to also include workers and background pages. Each entry carries the targetId used as a target selector elsewhere, plus an 'origin' field recording where the tab came from: 'agent' means this toolkit created it (the entry then also carries the creating 'label' and 'createdAt'), and this stays true after the creating agent releases its lease or dies, which is what makes a stray agent tab findable later. 'unknown' means there is no creation record. It never says 'human': the toolkit cannot prove a person opened a tab, so 'unknown' is the honest word for a tab it did not create, one opened before the toolkit ran, or one whose record could not be written. A tab whose record exists but could not be read reports origin 'unknown' plus 'originUnreadable', so a broken record is never mistaken for no record.",
3306
+ description: "Enumerate browser page targets via the CDP browser endpoint (GET /json/list). By default returns only page-type tabs; set 'all' to also include workers and background pages. Each entry carries the targetId used as a target selector elsewhere, plus an 'origin' field recording where the tab came from: 'agent' means this toolkit created it (the entry then also carries the creating 'label' and 'createdAt'), and this stays true after the creating agent releases its lease or dies, which is what makes a stray agent tab findable later. 'unknown' means there is no creation record. It never says 'human': the toolkit cannot prove a person opened a tab, so 'unknown' is the honest word for a tab it did not create, one opened before the toolkit ran, or one whose record could not be written. A tab whose record exists but could not be read reports origin 'unknown' plus 'originUnreadable', so a broken record is never mistaken for no record. Under CDP_REQUIRE_LEASE this call also reaps: a tab this toolkit created whose lease is stale because the owning process died or the TTL elapsed is closed, and the closed tabs are reported in an additive 'reaped' array ({targetId,label,reason}) that is present only when something was actually closed. A tab with no lease is never reaped, so tabs opened by a caller that never uses leases are untouched.",
2816
3307
  inputSchema: {
2817
3308
  type: "object",
2818
3309
  properties: {
2819
3310
  lease: {
2820
3311
  type: "string",
2821
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
3312
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
2822
3313
  },
2823
3314
  all: {
2824
3315
  type: "boolean",
@@ -2837,7 +3328,7 @@ var MANIFEST = [
2837
3328
  properties: {
2838
3329
  lease: {
2839
3330
  type: "string",
2840
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
3331
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
2841
3332
  },
2842
3333
  url: {
2843
3334
  type: "string",
@@ -2845,7 +3336,7 @@ var MANIFEST = [
2845
3336
  },
2846
3337
  claim: {
2847
3338
  type: "boolean",
2848
- description: "Claim the new tab atomically as part of creating it and return a lease token alongside {targetId,url}. Default false, which is byte-identical to the pre-1.2 behavior."
3339
+ description: "Claim the new tab atomically as part of creating it and return a lease token alongside {targetId,url}. Default false, which is byte-identical to the pre-1.2 behavior. Under CDP_REQUIRE_LEASE the new tab is claimed and a lease returned even without claim:true, because a tab nobody holds is a tab nobody may drive; passing claim:true additionally makes the lease an explicit one, which requires its token on every later call even from this same process."
2849
3340
  },
2850
3341
  label: {
2851
3342
  type: "string",
@@ -2868,7 +3359,7 @@ var MANIFEST = [
2868
3359
  properties: {
2869
3360
  lease: {
2870
3361
  type: "string",
2871
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
3362
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
2872
3363
  },
2873
3364
  target: {
2874
3365
  type: "string",
@@ -2889,7 +3380,7 @@ var MANIFEST = [
2889
3380
  properties: {
2890
3381
  lease: {
2891
3382
  type: "string",
2892
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
3383
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
2893
3384
  },
2894
3385
  target: {
2895
3386
  type: "string",
@@ -2910,7 +3401,7 @@ var MANIFEST = [
2910
3401
  properties: {
2911
3402
  lease: {
2912
3403
  type: "string",
2913
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
3404
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
2914
3405
  },
2915
3406
  target: {
2916
3407
  type: "string",
@@ -2952,7 +3443,7 @@ var MANIFEST = [
2952
3443
  properties: {
2953
3444
  lease: {
2954
3445
  type: "string",
2955
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
3446
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
2956
3447
  },
2957
3448
  target: {
2958
3449
  type: "string",
@@ -2985,7 +3476,7 @@ var MANIFEST = [
2985
3476
  properties: {
2986
3477
  lease: {
2987
3478
  type: "string",
2988
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
3479
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
2989
3480
  },
2990
3481
  target: {
2991
3482
  type: "string",
@@ -3022,7 +3513,7 @@ var MANIFEST = [
3022
3513
  properties: {
3023
3514
  lease: {
3024
3515
  type: "string",
3025
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs."
3516
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3026
3517
  },
3027
3518
  target: {
3028
3519
  type: "string",
@@ -3053,7 +3544,7 @@ var MANIFEST = [
3053
3544
  properties: {
3054
3545
  lease: {
3055
3546
  type: "string",
3056
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs."
3547
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3057
3548
  },
3058
3549
  target: {
3059
3550
  type: "string",
@@ -3109,7 +3600,7 @@ var MANIFEST = [
3109
3600
  properties: {
3110
3601
  lease: {
3111
3602
  type: "string",
3112
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs."
3603
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3113
3604
  },
3114
3605
  target: {
3115
3606
  type: "string",
@@ -3144,7 +3635,7 @@ var MANIFEST = [
3144
3635
  properties: {
3145
3636
  lease: {
3146
3637
  type: "string",
3147
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
3638
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3148
3639
  },
3149
3640
  target: {
3150
3641
  type: "string",
@@ -3167,7 +3658,7 @@ var MANIFEST = [
3167
3658
  properties: {
3168
3659
  lease: {
3169
3660
  type: "string",
3170
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
3661
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3171
3662
  },
3172
3663
  target: {
3173
3664
  type: "string",
@@ -3207,7 +3698,7 @@ var MANIFEST = [
3207
3698
  properties: {
3208
3699
  lease: {
3209
3700
  type: "string",
3210
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
3701
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3211
3702
  },
3212
3703
  target: {
3213
3704
  type: "string",
@@ -3234,7 +3725,7 @@ var MANIFEST = [
3234
3725
  properties: {
3235
3726
  lease: {
3236
3727
  type: "string",
3237
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
3728
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3238
3729
  },
3239
3730
  target: {
3240
3731
  type: "string",
@@ -3286,7 +3777,7 @@ var MANIFEST = [
3286
3777
  properties: {
3287
3778
  lease: {
3288
3779
  type: "string",
3289
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
3780
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3290
3781
  },
3291
3782
  target: {
3292
3783
  type: "string",
@@ -3319,7 +3810,7 @@ var MANIFEST = [
3319
3810
  properties: {
3320
3811
  lease: {
3321
3812
  type: "string",
3322
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
3813
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3323
3814
  },
3324
3815
  target: {
3325
3816
  type: "string",
@@ -3365,7 +3856,7 @@ var MANIFEST = [
3365
3856
  properties: {
3366
3857
  lease: {
3367
3858
  type: "string",
3368
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
3859
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3369
3860
  },
3370
3861
  target: {
3371
3862
  type: "string",
@@ -3398,7 +3889,7 @@ var MANIFEST = [
3398
3889
  properties: {
3399
3890
  lease: {
3400
3891
  type: "string",
3401
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
3892
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3402
3893
  },
3403
3894
  target: {
3404
3895
  type: "string",
@@ -3430,7 +3921,7 @@ var MANIFEST = [
3430
3921
  properties: {
3431
3922
  lease: {
3432
3923
  type: "string",
3433
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
3924
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3434
3925
  },
3435
3926
  target: {
3436
3927
  type: "string",
@@ -3469,7 +3960,7 @@ var MANIFEST = [
3469
3960
  properties: {
3470
3961
  lease: {
3471
3962
  type: "string",
3472
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
3963
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3473
3964
  },
3474
3965
  target: {
3475
3966
  type: "string",
@@ -3520,7 +4011,7 @@ var MANIFEST = [
3520
4011
  properties: {
3521
4012
  lease: {
3522
4013
  type: "string",
3523
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
4014
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3524
4015
  },
3525
4016
  target: {
3526
4017
  type: "string",
@@ -3620,7 +4111,7 @@ var MANIFEST = [
3620
4111
  properties: {
3621
4112
  lease: {
3622
4113
  type: "string",
3623
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
4114
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3624
4115
  },
3625
4116
  target: {
3626
4117
  type: "string",
@@ -3658,7 +4149,7 @@ var MANIFEST = [
3658
4149
  properties: {
3659
4150
  lease: {
3660
4151
  type: "string",
3661
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
4152
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3662
4153
  },
3663
4154
  target: {
3664
4155
  type: "string",
@@ -3695,7 +4186,7 @@ var MANIFEST = [
3695
4186
  properties: {
3696
4187
  lease: {
3697
4188
  type: "string",
3698
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
4189
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3699
4190
  },
3700
4191
  target: {
3701
4192
  type: "string",
@@ -3722,7 +4213,7 @@ var MANIFEST = [
3722
4213
  properties: {
3723
4214
  lease: {
3724
4215
  type: "string",
3725
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
4216
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3726
4217
  },
3727
4218
  target: {
3728
4219
  type: "string",
@@ -3745,7 +4236,7 @@ var MANIFEST = [
3745
4236
  properties: {
3746
4237
  lease: {
3747
4238
  type: "string",
3748
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
4239
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3749
4240
  },
3750
4241
  target: {
3751
4242
  type: "string",
@@ -3776,7 +4267,7 @@ var MANIFEST = [
3776
4267
  properties: {
3777
4268
  lease: {
3778
4269
  type: "string",
3779
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
4270
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3780
4271
  },
3781
4272
  target: {
3782
4273
  type: "string",
@@ -3811,7 +4302,7 @@ var MANIFEST = [
3811
4302
  properties: {
3812
4303
  lease: {
3813
4304
  type: "string",
3814
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
4305
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3815
4306
  },
3816
4307
  target: {
3817
4308
  type: "string",
@@ -3837,7 +4328,7 @@ var MANIFEST = [
3837
4328
  properties: {
3838
4329
  lease: {
3839
4330
  type: "string",
3840
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
4331
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3841
4332
  },
3842
4333
  target: {
3843
4334
  type: "string",
@@ -3856,7 +4347,7 @@ var MANIFEST = [
3856
4347
  properties: {
3857
4348
  lease: {
3858
4349
  type: "string",
3859
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
4350
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3860
4351
  },
3861
4352
  tracePath: {
3862
4353
  type: "string",
@@ -3875,7 +4366,7 @@ var MANIFEST = [
3875
4366
  properties: {
3876
4367
  lease: {
3877
4368
  type: "string",
3878
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
4369
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3879
4370
  },
3880
4371
  target: {
3881
4372
  type: "string",
@@ -3905,6 +4396,76 @@ var MANIFEST = [
3905
4396
  additionalProperties: false
3906
4397
  }
3907
4398
  },
4399
+ {
4400
+ name: "start_screen_recording",
4401
+ description: "Start recording the target tab to video over raw CDP (Page.startScreencast), holding one persistent connection open and spooling every Page.screencastFrame to disk. Chrome emits a frame ON REPAINT, not on a clock, so the stream is variable-rate: each frame is timestamped into a ledger and stop_screen_recording turns those timestamps into per-frame display durations, which is what makes a still page hold one long frame instead of the video racing or freezing. Every frame is acked immediately (an unacked frame stalls the stream). ffmpeg is probed here, not at stop, so a missing encoder fails before a recording is captured and thrown away. Must be paired with stop_screen_recording WITHIN THE SAME PROCESS (the frames are events on this connection and cannot be re-attached from another process, so the pair works under the MCP server, not the one-process-per-call CLI). Throws if a recording is already in progress for the target; recordings on DIFFERENT targets run concurrently. Chrome only: WebDriver BiDi has no screencast primitive, so both tools are absent under --browser firefox.",
4402
+ inputSchema: {
4403
+ type: "object",
4404
+ properties: {
4405
+ lease: {
4406
+ type: "string",
4407
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
4408
+ },
4409
+ target: {
4410
+ type: "string",
4411
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
4412
+ },
4413
+ format: {
4414
+ type: "string",
4415
+ enum: [
4416
+ "jpeg",
4417
+ "png"
4418
+ ],
4419
+ description: "Frame image format. Defaults to jpeg (far smaller per repaint); png is lossless. quality applies to jpeg only."
4420
+ },
4421
+ quality: {
4422
+ type: "number",
4423
+ description: "JPEG frame quality 0-100. Ignored for png. Omitted by default, which leaves Chrome's own default in place."
4424
+ },
4425
+ maxWidth: {
4426
+ type: "number",
4427
+ description: "Cap the streamed frame width in pixels. Also the way to pin frame size if the viewport might change mid-recording."
4428
+ },
4429
+ maxHeight: {
4430
+ type: "number",
4431
+ description: "Cap the streamed frame height in pixels."
4432
+ },
4433
+ everyNthFrame: {
4434
+ type: "number",
4435
+ description: "Capture only every Nth repaint (1 = every frame). Reduces spool size on a busy page; per-frame durations keep playback at wallclock speed either way."
4436
+ },
4437
+ bringToFront: {
4438
+ type: "boolean",
4439
+ description: "Activate the tab (Page.bringToFront) before recording. Default false. A fully backgrounded or occluded tab may never repaint, and a recording of one captures 0 frames."
4440
+ }
4441
+ },
4442
+ required: [],
4443
+ additionalProperties: false
4444
+ }
4445
+ },
4446
+ {
4447
+ name: "stop_screen_recording",
4448
+ description: "Stop the recording started by start_screen_recording, assemble the spooled frames into an MP4 with ffmpeg using PER-FRAME durations from the ledger (frame N is held until frame N+1 painted; the last frame is held until this call), and return {path,bytes,durationMs,frameCount,encodedFrames,codec,encoder,width,height,droppedFrames,target}. Encoder ladder, probed once at start: hevc_videotoolbox then h264_videotoolbox then libx265 then libx264, with -tag:v hvc1 on HEVC so QuickTime plays it, +faststart, yuv420p, and even dimensions forced. With one recording in flight 'target' may be omitted; with several it is required, because guessing which to stop can lose another agent's recording. Throws if nothing is recording in this process, and throws rather than writing a silent empty video when 0 frames arrived. On an ffmpeg failure the spooled frames are KEPT and the spool path plus the exact command are named in the error. Encoded video tops out at 25 fps: ffmpeg's concat demuxer represents image timestamps on a 1/25s grid, so frames captured less than 40ms apart are coalesced deliberately (frameCount reports what was captured, encodedFrames what reached the video) instead of being dropped silently by ffmpeg. durationMs is the sum of the encoded durations; the file itself runs one 40ms step longer, because the concat demuxer needs the final frame repeated for its hold to count.",
4449
+ inputSchema: {
4450
+ type: "object",
4451
+ properties: {
4452
+ lease: {
4453
+ type: "string",
4454
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
4455
+ },
4456
+ target: {
4457
+ type: "string",
4458
+ description: "Which recording to stop: 'active' (default, valid only when exactly one recording is in flight) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
4459
+ },
4460
+ savePath: {
4461
+ type: "string",
4462
+ description: "Override the output file path. Default: /tmp/cdp-toolkit/screen-recording-<targetIdShort>-<stamp>.mp4."
4463
+ }
4464
+ },
4465
+ required: [],
4466
+ additionalProperties: false
4467
+ }
4468
+ },
3908
4469
  {
3909
4470
  name: "take_heapsnapshot",
3910
4471
  description: "Capture a V8 heap snapshot of the selected page target over raw CDP (HeapProfiler.takeHeapSnapshot, accumulating addHeapSnapshotChunk events) and write it as a .heapsnapshot JSON file loadable by the DevTools Memory panel; returns {path,bytes,chunks,target} only and does not parse/summarize the snapshot. Writes under /tmp/cdp-toolkit (CDP_ARTIFACT_DIR) unless savePath is given.",
@@ -3913,7 +4474,7 @@ var MANIFEST = [
3913
4474
  properties: {
3914
4475
  lease: {
3915
4476
  type: "string",
3916
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
4477
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3917
4478
  },
3918
4479
  target: {
3919
4480
  type: "string",
@@ -3936,7 +4497,7 @@ var MANIFEST = [
3936
4497
  properties: {
3937
4498
  lease: {
3938
4499
  type: "string",
3939
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
4500
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3940
4501
  },
3941
4502
  url: {
3942
4503
  type: "string",
@@ -3980,7 +4541,7 @@ var MANIFEST = [
3980
4541
  properties: {
3981
4542
  lease: {
3982
4543
  type: "string",
3983
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
4544
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
3984
4545
  },
3985
4546
  target: {
3986
4547
  type: "string",
@@ -4050,7 +4611,7 @@ var MANIFEST = [
4050
4611
  properties: {
4051
4612
  lease: {
4052
4613
  type: "string",
4053
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
4614
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
4054
4615
  },
4055
4616
  target: {
4056
4617
  type: "string",
@@ -4069,7 +4630,7 @@ var MANIFEST = [
4069
4630
  properties: {
4070
4631
  lease: {
4071
4632
  type: "string",
4072
- description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
4633
+ description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
4073
4634
  },
4074
4635
  target: {
4075
4636
  type: "string",
@@ -4086,17 +4647,21 @@ var MANIFEST = [
4086
4647
  },
4087
4648
  {
4088
4649
  name: "claim_page",
4089
- description: "Take exclusive ownership of a browser tab and return an opaque lease token. With no targetId, opens a fresh tab (optionally at url) and claims it, so 'give me my own tab' is one call. Any later tool call against a leased tab must carry the matching token in its 'lease' argument or it is refused, naming the holder. The lease is reclaimable once its owning process dies, its ttlMs elapses without use, or its tab is closed. Refused from the CLI: a CLI invocation is one process per call, so its lease would be reclaimable immediately.",
4650
+ description: "Take exclusive ownership of a browser tab and return an opaque lease token. Two modes. FRESH TAB: with no target and no targetId, opens a new tab (optionally at url) and claims it, so 'give me my own tab' is one call. TAKEOVER: with target, claims a tab that is already open — this is how you work in a tab the human already has open when they ask you to, instead of opening your own — resolving any target selector against the live page list and never creating anything. The answer's 'opened' flag says which happened (true only when this call created the tab), and that same creation record is what release_page consults, so a tab you took over is left open when you release it. Any later tool call against a leased tab must carry the matching token in its 'lease' argument or it is refused, naming the holder. The lease is reclaimable once its owning process dies, its ttlMs elapses without use, or its tab is closed. Refused from the CLI: a CLI invocation is one process per call, so its lease would be reclaimable immediately.",
4090
4651
  inputSchema: {
4091
4652
  type: "object",
4092
4653
  properties: {
4654
+ target: {
4655
+ type: "string",
4656
+ description: "Take over an already-open tab, e.g. one the human has open, when asked to: pass any target selector — active | index:N | url:<substr> | title:<substr> | <targetId>. Resolved against the live page list only, so it NEVER opens a tab: a selector that matches nothing is an error, not a new blank tab. The tab is left open on release because the toolkit did not create it. Refused if another live process holds it, including a lease the gate acquired for that process automatically under CDP_REQUIRE_LEASE: this takes over unleased (human) tabs and never steals one from a live agent. Mutually exclusive with targetId."
4657
+ },
4093
4658
  targetId: {
4094
4659
  type: "string",
4095
- description: "Claim this already-open page target instead of opening a new tab. Must be an exact target id, not a selector."
4660
+ description: "Claim this already-open page target instead of opening a new tab. Must be an exact target id, not a selector. Kept for back-compat: prefer 'target', which accepts an exact id too plus every other selector form. Mutually exclusive with target."
4096
4661
  },
4097
4662
  url: {
4098
4663
  type: "string",
4099
- description: "When opening a fresh tab, navigate it here. Ignored when targetId is given."
4664
+ description: "When opening a fresh tab, navigate it here. Ignored when target or targetId is given, since both of those claim a tab that is already open."
4100
4665
  },
4101
4666
  label: {
4102
4667
  type: "string",
@@ -4113,24 +4678,30 @@ var MANIFEST = [
4113
4678
  },
4114
4679
  {
4115
4680
  name: "release_page",
4116
- description: "Release a held lease, given its token. Idempotent: releasing an already-released, reclaimed, or expired lease reports released:false rather than erroring. Does not close the tab.",
4681
+ description: "Give a lease back, and close the tab if this toolkit opened it. Takes exactly one of 'lease' (the token claim_page returned) or 'target' (a selector for a tab this process holds, which is how you release a lease the gate acquired for you automatically, since that path never hands you a token). A tab with a creation record from this toolkit is closed; a tab that was already open and merely claimed is released and left alone. Override either way with 'close'. Idempotent: an already-released, reclaimed, or expired lease reports released:false, and a release that did not happen never closes anything, because by then the tab may belong to another agent. Answers {released, closed, targetId}.",
4117
4682
  inputSchema: {
4118
4683
  type: "object",
4119
4684
  properties: {
4120
4685
  lease: {
4121
4686
  type: "string",
4122
- description: "The opaque token claim_page (or new_page with claim:true) returned."
4687
+ description: "The opaque token claim_page (or new_page with claim:true) returned. Mutually exclusive with 'target'. Under CDP_REQUIRE_LEASE a lease the gate acquired for you automatically never produced a token in the first place, so give 'target' instead."
4688
+ },
4689
+ target: {
4690
+ type: "string",
4691
+ description: "Target selector for a tab this process holds: active | index:N | url:<substr> | title:<substr> | <targetId>. Mutually exclusive with 'lease'. Refused if another process holds the tab."
4692
+ },
4693
+ close: {
4694
+ type: "boolean",
4695
+ description: "Force the close decision instead of letting the creation ledger decide. true closes even a tab this toolkit did not open; false keeps an agent-created tab open. Omit for the default, which closes only tabs this toolkit created."
4123
4696
  }
4124
4697
  },
4125
- required: [
4126
- "lease"
4127
- ],
4698
+ required: [],
4128
4699
  additionalProperties: false
4129
4700
  }
4130
4701
  },
4131
4702
  {
4132
4703
  name: "list_leases",
4133
- description: 'Enumerate every active lease for diagnosis: backend, target id, agent label, pid, createdAt, lastUsedAt, ttlMs, whether the owning process is still alive, and whether the lease is reclaimable and why. Requires no token. Never returns the lease nonce, so this cannot be used to forge a token. A row for a lease file that could not be read or parsed instead carries `unreadable` (the errno, or "unparseable"), with label, pid, and the timestamp fields as zero placeholders and stale always false, since stale is what marks a lease free to take and an unreadable one must never read that way.',
4704
+ description: "Enumerate every active lease for diagnosis: backend, target id, agent label, pid, createdAt, lastUsedAt, ttlMs, whether the owning process is still alive, and whether the lease is reclaimable and why. Requires no token. Never returns the lease nonce, so this cannot be used to forge a token. A row for a lease file that could not be read or parsed instead carries `unreadable` (the errno, or \"unparseable\"), with label, pid, and the timestamp fields as zero placeholders and stale always false, since stale is what marks a lease free to take and an unreadable one must never read that way. Under CDP_REQUIRE_LEASE this call reaps abandoned agent tabs exactly as list_pages does, reporting them in an additive 'reaped' array and omitting their rows from the listing.",
4134
4705
  inputSchema: {
4135
4706
  type: "object",
4136
4707
  properties: {},
@@ -4678,8 +5249,8 @@ function hostFromUrl(url, toolName) {
4678
5249
  function driverError2(code, message, data) {
4679
5250
  return Object.assign(new Error(message), { code, ...data !== undefined ? { data } : {} });
4680
5251
  }
4681
- function encodeUid2(stamp5) {
4682
- return `bidi:${stamp5}`;
5252
+ function encodeUid2(stamp6) {
5253
+ return `bidi:${stamp6}`;
4683
5254
  }
4684
5255
  function decodeStamp(uid) {
4685
5256
  if (!uid.startsWith("bidi:"))
@@ -4786,7 +5357,7 @@ function parseSnapshotLine(line) {
4786
5357
  const m = LINE_RE.exec(line);
4787
5358
  if (!m)
4788
5359
  return;
4789
- const indent = m[1] ?? "", stamp5 = m[2], role = m[3], nameLit = m[4], extrasRaw = m[5];
5360
+ const indent = m[1] ?? "", stamp6 = m[2], role = m[3], nameLit = m[4], extrasRaw = m[5];
4790
5361
  const depth = indent.length / 2;
4791
5362
  let name;
4792
5363
  if (nameLit) {
@@ -4814,7 +5385,7 @@ function parseSnapshotLine(line) {
4814
5385
  extras[key] = val;
4815
5386
  }
4816
5387
  }
4817
- return { uid: encodeUid2(stamp5), role, depth, ...name !== undefined ? { name } : {}, ...Object.keys(extras).length ? { extras } : {} };
5388
+ return { uid: encodeUid2(stamp6), role, depth, ...name !== undefined ? { name } : {}, ...Object.keys(extras).length ? { extras } : {} };
4818
5389
  }
4819
5390
  var INTERACTIVE_ROLES3 = new Set([
4820
5391
  "button",
@@ -4847,8 +5418,8 @@ function sharedRefOf(node, describe) {
4847
5418
  }
4848
5419
  async function resolveElementLocator2(conn, contextId, loc) {
4849
5420
  if ("uid" in loc) {
4850
- const stamp5 = decodeStamp(loc.uid);
4851
- const nodes2 = await locateNodes(conn, contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(stamp5)}"]` });
5421
+ const stamp6 = decodeStamp(loc.uid);
5422
+ const nodes2 = await locateNodes(conn, contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(stamp6)}"]` });
4852
5423
  if (!nodes2.length)
4853
5424
  throw driverError2("stale-uid", `uid does not resolve: ${loc.uid}`);
4854
5425
  return sharedRefOf(nodes2[0], loc.uid);
@@ -5060,14 +5631,14 @@ class BidiPageDriver {
5060
5631
  return loc.uid;
5061
5632
  }
5062
5633
  const ref = await resolveElementLocator2(this.conn, this.contextId, loc);
5063
- const stamp5 = await this.callFunctionValue(STAMP_ONE_SOURCE, [], ref);
5064
- if (typeof stamp5 !== "string")
5634
+ const stamp6 = await this.callFunctionValue(STAMP_ONE_SOURCE, [], ref);
5635
+ if (typeof stamp6 !== "string")
5065
5636
  throw driverError2("page-error", "stamping the located element did not return a string");
5066
- return encodeUid2(stamp5);
5637
+ return encodeUid2(stamp6);
5067
5638
  }
5068
5639
  async resolve(uid) {
5069
- const stamp5 = decodeStamp(uid);
5070
- const nodes = await locateNodes(this.conn, this.contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(stamp5)}"]` });
5640
+ const stamp6 = decodeStamp(uid);
5641
+ const nodes = await locateNodes(this.conn, this.contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(stamp6)}"]` });
5071
5642
  if (!nodes.length)
5072
5643
  throw driverError2("stale-uid", `uid does not resolve: ${uid}`);
5073
5644
  }
@@ -5496,11 +6067,11 @@ function createFirefoxDriver(port, opts) {
5496
6067
  }
5497
6068
 
5498
6069
  // src/bidi/launch.ts
5499
- import { spawn as spawn2 } from "node:child_process";
6070
+ import { spawn as spawn3 } from "node:child_process";
5500
6071
  import { mkdtempSync, rmSync } from "node:fs";
5501
6072
  import { createConnection, createServer } from "node:net";
5502
6073
  import { tmpdir } from "node:os";
5503
- import { join as join6 } from "node:path";
6074
+ import { join as join7 } from "node:path";
5504
6075
  function resolveBinary() {
5505
6076
  const candidates = [
5506
6077
  process.env.FIREFOX_BIN,
@@ -5574,11 +6145,11 @@ async function launchFirefox(opts = {}) {
5574
6145
  const binary = resolveBinary();
5575
6146
  const port = await pickPort(opts.port);
5576
6147
  const isThrowaway = opts.profilePath === undefined;
5577
- const profilePath = opts.profilePath ?? mkdtempSync(join6(tmpdir(), "cdp-toolkit-ff-profile-"));
6148
+ const profilePath = opts.profilePath ?? mkdtempSync(join7(tmpdir(), "cdp-toolkit-ff-profile-"));
5578
6149
  const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
5579
6150
  if (opts.headless)
5580
6151
  args.push("--headless");
5581
- const proc = spawn2(binary, args, { stdio: "ignore", detached: false });
6152
+ const proc = spawn3(binary, args, { stdio: "ignore", detached: false });
5582
6153
  let closed = false;
5583
6154
  const close = async () => {
5584
6155
  if (closed)
@@ -5844,7 +6415,7 @@ var FIREFOX_TOOLS = {
5844
6415
  };
5845
6416
 
5846
6417
  // src/mcp.ts
5847
- var VERSION = "1.4.0";
6418
+ var VERSION = "1.7.0";
5848
6419
  var BROWSER = resolveBrowserKind(stripBrowserFlag(process.argv.slice(2)));
5849
6420
  var AVAILABILITY = toolAvailability(BROWSER);
5850
6421
  var AVAILABLE_NAMES = new Set(AVAILABILITY.available);
@@ -5901,6 +6472,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5901
6472
  });
5902
6473
  });
5903
6474
  auditCoverage();
6475
+ markLongLivedProcess();
5904
6476
  await server.connect(new StdioServerTransport);
5905
6477
  console.error(`[cdp-toolkit] MCP server v${VERSION} ready, browser=${BROWSER}, ${AVAILABILITY.available.length} tools, CDP_BASE=${BASE}`);
5906
6478
  var shuttingDown = false;