cdp-toolkit 1.1.0 → 1.2.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
@@ -1,5 +1,257 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/leases.ts
4
+ import { AsyncLocalStorage } from "node:async_hooks";
5
+ import { randomBytes } from "node:crypto";
6
+ import { mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+ var DEFAULT_LEASE_TTL_MS = 900000;
9
+ function leaseDir() {
10
+ return process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
11
+ }
12
+ function leaseTtlMs() {
13
+ const raw = Number(process.env.CDP_LEASE_TTL_MS);
14
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_LEASE_TTL_MS;
15
+ }
16
+ function safeId(id) {
17
+ return id.replace(/[^A-Za-z0-9._-]/g, "_");
18
+ }
19
+ function leaseFile(backend, targetId) {
20
+ return join(leaseDir(), `lease-${backend}-${safeId(targetId)}.json`);
21
+ }
22
+ function mintToken(backend, targetId, nonce) {
23
+ return `${backend}:${targetId}:${nonce}`;
24
+ }
25
+ function tokenParts(token) {
26
+ if (typeof token !== "string")
27
+ return;
28
+ const first = token.indexOf(":");
29
+ const last = token.lastIndexOf(":");
30
+ if (first <= 0 || last <= first)
31
+ return;
32
+ const backend = token.slice(0, first);
33
+ if (backend !== "chrome" && backend !== "firefox")
34
+ return;
35
+ const targetId = token.slice(first + 1, last);
36
+ const nonce = token.slice(last + 1);
37
+ if (!targetId.length || !/^[0-9a-f]{24}$/.test(nonce))
38
+ return;
39
+ return { backend, targetId, nonce };
40
+ }
41
+
42
+ class LeaseConflictError extends Error {
43
+ targetId;
44
+ holder;
45
+ constructor(message, targetId, holder) {
46
+ super(message);
47
+ this.targetId = targetId;
48
+ this.holder = holder;
49
+ this.name = "LeaseConflictError";
50
+ }
51
+ }
52
+ async function readLease(backend, targetId) {
53
+ let raw;
54
+ try {
55
+ raw = await readFile(leaseFile(backend, targetId), "utf8");
56
+ } catch (err) {
57
+ if (err.code === "ENOENT")
58
+ return;
59
+ throw err;
60
+ }
61
+ try {
62
+ const rec = JSON.parse(raw);
63
+ if (typeof rec?.nonce === "string" && typeof rec?.pid === "number")
64
+ return rec;
65
+ } catch {}
66
+ return;
67
+ }
68
+ function isPidAlive(pid) {
69
+ if (!Number.isInteger(pid) || pid <= 0)
70
+ return false;
71
+ try {
72
+ process.kill(pid, 0);
73
+ return true;
74
+ } catch (err) {
75
+ return err.code === "EPERM";
76
+ }
77
+ }
78
+ function staleReason(rec, opts) {
79
+ if (!isPidAlive(rec.pid))
80
+ return "dead-pid";
81
+ if (opts.now - rec.lastUsedAt > rec.ttlMs)
82
+ return "expired";
83
+ if (opts.liveIds && !opts.liveIds.includes(rec.targetId))
84
+ return "target-gone";
85
+ return false;
86
+ }
87
+ async function claimLease(backend, targetId, opts) {
88
+ const now = opts.now ?? Date.now();
89
+ await mkdir(leaseDir(), { recursive: true });
90
+ for (let attempt = 0;attempt < 3; attempt++) {
91
+ const record = {
92
+ backend,
93
+ targetId,
94
+ nonce: randomBytes(12).toString("hex"),
95
+ pid: process.pid,
96
+ label: opts.label,
97
+ createdAt: now,
98
+ lastUsedAt: now,
99
+ ttlMs: opts.ttlMs ?? leaseTtlMs()
100
+ };
101
+ try {
102
+ await writeFile(leaseFile(backend, targetId), JSON.stringify(record), { flag: "wx" });
103
+ return { token: mintToken(backend, targetId, record.nonce), record };
104
+ } catch (err) {
105
+ if (err.code !== "EEXIST")
106
+ throw err;
107
+ }
108
+ const existing = await readLease(backend, targetId);
109
+ if (!existing)
110
+ continue;
111
+ const stale = staleReason(existing, { now, liveIds: opts.liveIds });
112
+ if (!stale) {
113
+ throw new LeaseConflictError(`target '${targetId}' is already leased by '${existing.label}' (pid ${existing.pid}); call release_page with that lease's token, or list_leases to inspect it`, targetId, existing.label);
114
+ }
115
+ await unlink(leaseFile(backend, targetId)).catch(() => {
116
+ return;
117
+ });
118
+ }
119
+ throw new LeaseConflictError(`target '${targetId}' could not be claimed after 3 attempts: another process is claiming and releasing it concurrently`, targetId);
120
+ }
121
+ async function releaseLease(token) {
122
+ const parts = tokenParts(token);
123
+ if (!parts)
124
+ return { released: false };
125
+ const rec = await readLease(parts.backend, parts.targetId);
126
+ if (!rec || rec.nonce !== parts.nonce)
127
+ return { released: false };
128
+ await unlink(leaseFile(parts.backend, parts.targetId)).catch(() => {
129
+ return;
130
+ });
131
+ return { released: true };
132
+ }
133
+ async function releaseLeaseFor(backend, targetId) {
134
+ if (!await readLease(backend, targetId))
135
+ return { released: false };
136
+ await unlink(leaseFile(backend, targetId)).catch(() => {
137
+ return;
138
+ });
139
+ return { released: true };
140
+ }
141
+ var leaseScope = new AsyncLocalStorage;
142
+ function withLeaseScope(lease, fn) {
143
+ return leaseScope.run(lease, fn);
144
+ }
145
+ function currentLease() {
146
+ return leaseScope.getStore();
147
+ }
148
+ function leaseFromArgs(args) {
149
+ if (!args || typeof args !== "object")
150
+ return;
151
+ const lease = args.lease;
152
+ return typeof lease === "string" && lease.length > 0 ? lease : undefined;
153
+ }
154
+ async function touchLease(rec, now = Date.now()) {
155
+ const current = await readLease(rec.backend, rec.targetId).catch(() => {
156
+ return;
157
+ });
158
+ if (!current || current.nonce !== rec.nonce)
159
+ return;
160
+ await writeFile(leaseFile(current.backend, current.targetId), JSON.stringify({ ...current, lastUsedAt: now }), "utf8").catch(() => {
161
+ return;
162
+ });
163
+ }
164
+ function describeTarget(targetId, ctx) {
165
+ const url = ctx.url ? ` (url ${ctx.url})` : ctx.title ? ` (title ${JSON.stringify(ctx.title)})` : "";
166
+ return `target '${targetId}'${url}`;
167
+ }
168
+ async function assertLeaseOk(backend, targetId, ctx = {}) {
169
+ const token = ctx.lease ?? currentLease();
170
+ const now = ctx.now ?? Date.now();
171
+ const rec = await readLease(backend, targetId);
172
+ const held = rec && !staleReason(rec, { now, liveIds: ctx.liveIds }) ? rec : undefined;
173
+ const where = describeTarget(targetId, ctx);
174
+ if (token === undefined) {
175
+ if (!held)
176
+ return;
177
+ 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);
178
+ }
179
+ const parts = tokenParts(token);
180
+ if (!parts) {
181
+ throw new LeaseConflictError(`malformed lease token for ${where}. Pass back the token claim_page returned, never a constructed one.`, targetId);
182
+ }
183
+ if (parts.backend !== backend || parts.targetId !== targetId) {
184
+ if (!held)
185
+ return;
186
+ 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);
187
+ }
188
+ if (!held) {
189
+ throw new LeaseConflictError(`${where} is not leased any more: the token you passed belongs to a lease that was released or reclaimed. Call claim_page again to take the tab.`, targetId);
190
+ }
191
+ if (parts.nonce !== held.nonce) {
192
+ throw new LeaseConflictError(`${where} was reclaimed and is now leased by '${held.label}' (pid ${held.pid}). Your token was invalidated by that reclamation. Call claim_page again if you still need this tab.`, targetId, held.label);
193
+ }
194
+ await touchLease(held, now);
195
+ }
196
+ var LEASE_FILE_RE = /^lease-(chrome|firefox)-(.+)\.json$/;
197
+ async function listLeases(opts = {}) {
198
+ const now = opts.now ?? Date.now();
199
+ let names = [];
200
+ try {
201
+ names = await readdir(leaseDir());
202
+ } catch (err) {
203
+ if (err.code === "ENOENT")
204
+ return [];
205
+ throw err;
206
+ }
207
+ const out = [];
208
+ for (const name of names) {
209
+ const parts = LEASE_FILE_RE.exec(name);
210
+ if (!parts)
211
+ continue;
212
+ const fromName = {
213
+ backend: parts[1],
214
+ targetId: parts[2],
215
+ label: "",
216
+ pid: 0,
217
+ createdAt: 0,
218
+ lastUsedAt: 0,
219
+ ttlMs: 0,
220
+ pidAlive: false,
221
+ stale: false
222
+ };
223
+ let rec;
224
+ try {
225
+ rec = JSON.parse(await readFile(join(leaseDir(), name), "utf8"));
226
+ } catch (err) {
227
+ const code = err.code;
228
+ if (code === "ENOENT")
229
+ continue;
230
+ out.push({ ...fromName, unreadable: code ?? "unparseable" });
231
+ continue;
232
+ }
233
+ if (typeof rec?.nonce !== "string" || typeof rec?.pid !== "number") {
234
+ out.push({ ...fromName, unreadable: "unparseable" });
235
+ continue;
236
+ }
237
+ out.push({
238
+ backend: rec.backend,
239
+ targetId: rec.targetId,
240
+ label: rec.label,
241
+ pid: rec.pid,
242
+ createdAt: rec.createdAt,
243
+ lastUsedAt: rec.lastUsedAt,
244
+ ttlMs: rec.ttlMs,
245
+ pidAlive: isPidAlive(rec.pid),
246
+ stale: staleReason(rec, {
247
+ now,
248
+ liveIds: opts.liveBackend === undefined || opts.liveBackend === rec.backend ? opts.liveIds : undefined
249
+ })
250
+ });
251
+ }
252
+ return out;
253
+ }
254
+
3
255
  // src/client.ts
4
256
  var BASE = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
5
257
  var DEFAULT_TIMEOUT_MS = Number(process.env.CDP_TIMEOUT_MS ?? 15000);
@@ -153,8 +405,13 @@ async function browserWsUrl() {
153
405
  const v = await httpJson("/json/version");
154
406
  return v.webSocketDebuggerUrl;
155
407
  }
156
- async function resolveTarget(selector) {
408
+ async function resolveTarget(selector, opts = {}) {
157
409
  const targets = await listTargets();
410
+ const hit = pickTarget(targets, selector);
411
+ await assertLeaseOk("chrome", hit.id, { lease: opts.lease, url: hit.url, title: hit.title });
412
+ return hit;
413
+ }
414
+ function pickTarget(targets, selector) {
158
415
  const pages = targets.filter((t) => t.type === "page");
159
416
  if (!selector || selector === "active") {
160
417
  if (!pages.length)
@@ -190,7 +447,7 @@ async function openBrowser(opts = {}) {
190
447
  return new CdpConnection(await browserWsUrl(), opts).connect();
191
448
  }
192
449
  async function openPage(selector, opts = {}) {
193
- const target = await resolveTarget(selector);
450
+ const target = await resolveTarget(selector, { lease: opts.lease });
194
451
  const conn = await new CdpConnection(target.webSocketDebuggerUrl, opts).connect();
195
452
  return { conn, target };
196
453
  }
@@ -1071,9 +1328,63 @@ function createCdpDriver() {
1071
1328
  return new CdpBrowserDriver;
1072
1329
  }
1073
1330
 
1331
+ // src/leases-tools.ts
1332
+ class LeaseToolError extends Error {
1333
+ }
1334
+ function backendOf(driver) {
1335
+ return driver.scheme === "bidi" ? "firefox" : "chrome";
1336
+ }
1337
+ async function claimPage(driver, args = {}) {
1338
+ const backend = backendOf(driver);
1339
+ const label = typeof args.label === "string" && args.label.length ? args.label : `pid-${process.pid}`;
1340
+ const pages = await driver.listPages();
1341
+ let targetId = args.targetId;
1342
+ let url = args.url ?? "about:blank";
1343
+ if (targetId === undefined || targetId === "") {
1344
+ const page = await driver.newPage(args.url);
1345
+ targetId = page.id;
1346
+ url = page.url;
1347
+ } else {
1348
+ const existing = pages.find((p) => p.id === targetId);
1349
+ if (!existing)
1350
+ throw new LeaseToolError(`claim_page: no page target with id '${targetId}'`);
1351
+ url = existing.url;
1352
+ }
1353
+ const liveIds = [...pages.map((p) => p.id), targetId];
1354
+ const { record, token } = await claimLease(backend, targetId, {
1355
+ label,
1356
+ ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
1357
+ liveIds
1358
+ });
1359
+ return {
1360
+ lease: token,
1361
+ targetId,
1362
+ url,
1363
+ label: record.label,
1364
+ ttlMs: record.ttlMs,
1365
+ expiresAt: record.lastUsedAt + record.ttlMs
1366
+ };
1367
+ }
1368
+ async function releasePage(_driver, args) {
1369
+ if (typeof args.lease !== "string" || !args.lease.length) {
1370
+ throw new LeaseToolError("release_page requires the 'lease' token returned by claim_page");
1371
+ }
1372
+ return releaseLease(args.lease);
1373
+ }
1374
+ async function listLeasesTool(driver, _args = {}) {
1375
+ const liveIds = (await driver.listPages()).map((p) => p.id);
1376
+ const leases = await listLeases({ liveIds, liveBackend: backendOf(driver) });
1377
+ return { leases, count: leases.length };
1378
+ }
1379
+ var LEASE_TOOLS = {
1380
+ claim_page: claimPage,
1381
+ release_page: releasePage,
1382
+ list_leases: listLeasesTool
1383
+ };
1384
+
1074
1385
  // src/shared-tools.ts
1075
- import { mkdir, writeFile } from "node:fs/promises";
1076
- import { join } from "node:path";
1386
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
1387
+ import { join as join2 } from "node:path";
1077
1388
  var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
1078
1389
  var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
1079
1390
 
@@ -1082,6 +1393,9 @@ class SharedToolError extends Error {
1082
1393
  function target3(p) {
1083
1394
  return { id: p.id, url: p.url, title: p.title };
1084
1395
  }
1396
+ function backendOf2(driver) {
1397
+ return driver.scheme === "bidi" ? "firefox" : "chrome";
1398
+ }
1085
1399
  async function withPage2(driver, target, fn) {
1086
1400
  const page = await driver.page(target);
1087
1401
  try {
@@ -1092,6 +1406,11 @@ async function withPage2(driver, target, fn) {
1092
1406
  }
1093
1407
  async function resolvePage(driver, selector) {
1094
1408
  const pages = await driver.listPages();
1409
+ const hit = await pickPage(driver, pages, selector);
1410
+ await assertLeaseOk(backendOf2(driver), hit.id, { url: hit.url, title: hit.title });
1411
+ return hit;
1412
+ }
1413
+ async function pickPage(driver, pages, selector) {
1095
1414
  if (selector === undefined || selector === "active") {
1096
1415
  if (!pages.length)
1097
1416
  throw new SharedToolError("no page targets open");
@@ -1139,18 +1458,27 @@ async function listPages(driver, args = {}) {
1139
1458
  }
1140
1459
  async function newPage(driver, args = {}) {
1141
1460
  const p = await driver.newPage(args.url);
1142
- return { targetId: p.id, url: p.url };
1461
+ if (args.claim !== true)
1462
+ return { targetId: p.id, url: p.url };
1463
+ const { record, token } = await claimLease(backendOf2(driver), p.id, {
1464
+ label: typeof args.label === "string" && args.label.length ? args.label : `pid-${process.pid}`,
1465
+ ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs()
1466
+ });
1467
+ return { targetId: p.id, url: p.url, lease: token, label: record.label, expiresAt: record.lastUsedAt + record.ttlMs };
1143
1468
  }
1144
1469
  async function closePage(driver, args) {
1145
1470
  if (args.target === undefined || args.target === "")
1146
1471
  throw new SharedToolError("close_page requires an explicit target; refusing to guess which page to close");
1147
1472
  const p = await resolvePage(driver, args.target);
1148
1473
  const res = await driver.closePage(p.id);
1149
- return { closed: p.id, success: res.success };
1474
+ if (!res.success)
1475
+ return { closed: p.id, success: false, leaseReleased: false };
1476
+ const released = await releaseLeaseFor(backendOf2(driver), p.id);
1477
+ return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
1150
1478
  }
1151
1479
  async function writeSelectedFile(id) {
1152
- await mkdir(STATE_DIR, { recursive: true });
1153
- await writeFile(join(STATE_DIR, "selected"), id, "utf8");
1480
+ await mkdir2(STATE_DIR, { recursive: true });
1481
+ await writeFile2(join2(STATE_DIR, "selected"), id, "utf8");
1154
1482
  }
1155
1483
  async function selectPage(driver, args) {
1156
1484
  if (args.target === undefined || args.target === "")
@@ -1305,10 +1633,10 @@ async function takeScreenshot(driver, args = {}) {
1305
1633
  const { data, format: outFormat } = await page.screenshot({ format, quality: args.quality, fullPage: args.fullPage, ...clip ? { clip } : {} });
1306
1634
  if (data.byteLength === 0)
1307
1635
  throw new SharedToolError("captureScreenshot returned empty data");
1308
- await mkdir(ARTIFACT_DIR, { recursive: true });
1636
+ await mkdir2(ARTIFACT_DIR, { recursive: true });
1309
1637
  const ext = outFormat === "jpeg" ? "jpg" : "png";
1310
- const path = args.savePath ?? join(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
1311
- await writeFile(path, Buffer.from(data));
1638
+ const path = args.savePath ?? join2(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
1639
+ await writeFile2(path, Buffer.from(data));
1312
1640
  const result = {
1313
1641
  path,
1314
1642
  bytes: data.byteLength,
@@ -1371,10 +1699,10 @@ var SHARED_TOOLS = {
1371
1699
  };
1372
1700
 
1373
1701
  // src/tools/console.ts
1374
- import { readFile } from "node:fs/promises";
1702
+ import { readFile as readFile2 } from "node:fs/promises";
1375
1703
 
1376
1704
  // src/tools/recorder.ts
1377
- import { mkdir as mkdir2, appendFile, writeFile as writeFile2, copyFile } from "node:fs/promises";
1705
+ import { mkdir as mkdir3, appendFile, writeFile as writeFile3, copyFile } from "node:fs/promises";
1378
1706
  var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
1379
1707
  function recFile(targetId) {
1380
1708
  return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
@@ -1395,11 +1723,11 @@ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Lo
1395
1723
  async function startRecorder(target, opts = {}) {
1396
1724
  const network = opts.network ?? true;
1397
1725
  const consoleCap = opts.console ?? true;
1398
- await mkdir2(ARTIFACT_DIR2, { recursive: true });
1726
+ await mkdir3(ARTIFACT_DIR2, { recursive: true });
1399
1727
  const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
1400
1728
  const file = opts.file ?? recFile(resolved.id);
1401
1729
  if (opts.truncate) {
1402
- await writeFile2(file, "");
1730
+ await writeFile3(file, "");
1403
1731
  }
1404
1732
  let chain = Promise.resolve();
1405
1733
  let dropped = 0;
@@ -1541,7 +1869,7 @@ function lineToEntry(line, index) {
1541
1869
  async function readConsoleEntries(file) {
1542
1870
  let raw;
1543
1871
  try {
1544
- raw = await readFile(file, "utf8");
1872
+ raw = await readFile2(file, "utf8");
1545
1873
  } catch {
1546
1874
  return [];
1547
1875
  }
@@ -1597,7 +1925,7 @@ async function getConsoleMessage(args = {}) {
1597
1925
  }
1598
1926
 
1599
1927
  // src/tools/network.ts
1600
- import { readFile as readFile2 } from "node:fs/promises";
1928
+ import { readFile as readFile3 } from "node:fs/promises";
1601
1929
  var DEFAULT_CAPTURE_MS2 = 2500;
1602
1930
  function asHeaders(v) {
1603
1931
  if (!v || typeof v !== "object")
@@ -1664,7 +1992,7 @@ function correlate(lines) {
1664
1992
  async function readNetworkRequests(file) {
1665
1993
  let raw;
1666
1994
  try {
1667
- raw = await readFile2(file, "utf8");
1995
+ raw = await readFile3(file, "utf8");
1668
1996
  } catch {
1669
1997
  return [];
1670
1998
  }
@@ -1746,8 +2074,8 @@ async function getNetworkRequest(args = {}) {
1746
2074
  }
1747
2075
 
1748
2076
  // src/tools/performance.ts
1749
- import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3, stat, unlink } from "node:fs/promises";
1750
- import { join as join2 } from "node:path";
2077
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4, stat, unlink as unlink2 } from "node:fs/promises";
2078
+ import { join as join3 } from "node:path";
1751
2079
  var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
1752
2080
  var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
1753
2081
  var DEFAULT_CATEGORIES = [
@@ -1765,7 +2093,7 @@ function shortId(id) {
1765
2093
  return id.slice(0, 8);
1766
2094
  }
1767
2095
  function stateFilePath() {
1768
- return join2(STATE_DIR2, "trace-state.json");
2096
+ return join3(STATE_DIR2, "trace-state.json");
1769
2097
  }
1770
2098
  function bufferTraceData(conn, sink) {
1771
2099
  return conn.on("Tracing.dataCollected", (params) => {
@@ -1787,10 +2115,10 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
1787
2115
  return sink;
1788
2116
  }
1789
2117
  async function writeTrace(events, targetId) {
1790
- await mkdir3(ARTIFACT_DIR3, { recursive: true });
1791
- const path = join2(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
2118
+ await mkdir4(ARTIFACT_DIR3, { recursive: true });
2119
+ const path = join3(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
1792
2120
  const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
1793
- await writeFile3(path, body, "utf8");
2121
+ await writeFile4(path, body, "utf8");
1794
2122
  const { size } = await stat(path);
1795
2123
  return { path, bytes: size };
1796
2124
  }
@@ -1810,7 +2138,7 @@ async function performanceStartTrace(args = {}) {
1810
2138
  throw err;
1811
2139
  }
1812
2140
  liveTraces.set(target.id, { conn, target, events, startedAt: Date.now() });
1813
- await mkdir3(STATE_DIR2, { recursive: true });
2141
+ await mkdir4(STATE_DIR2, { recursive: true });
1814
2142
  const state = {
1815
2143
  targetId: target.id,
1816
2144
  url: target.url,
@@ -1818,7 +2146,7 @@ async function performanceStartTrace(args = {}) {
1818
2146
  pid: process.pid,
1819
2147
  categories
1820
2148
  };
1821
- await writeFile3(stateFilePath(), JSON.stringify(state), "utf8");
2149
+ await writeFile4(stateFilePath(), JSON.stringify(state), "utf8");
1822
2150
  return {
1823
2151
  target: { id: target.id, url: target.url, title: target.title },
1824
2152
  categories,
@@ -1831,7 +2159,7 @@ async function performanceStopTrace(args = {}) {
1831
2159
  if (!entry) {
1832
2160
  let hint = "";
1833
2161
  try {
1834
- const raw = await readFile3(stateFilePath(), "utf8");
2162
+ const raw = await readFile4(stateFilePath(), "utf8");
1835
2163
  const state = JSON.parse(raw);
1836
2164
  hint = state.pid === process.pid ? ` (state file lists target ${shortId(state.targetId)} but its live connection is gone)` : ` (trace was started by a DIFFERENT process pid=${state.pid}; live recordings cannot cross process boundaries)`;
1837
2165
  } catch {}
@@ -1843,7 +2171,7 @@ async function performanceStopTrace(args = {}) {
1843
2171
  } finally {
1844
2172
  conn.close();
1845
2173
  liveTraces.delete(target.id);
1846
- await unlink(stateFilePath()).catch(() => {});
2174
+ await unlink2(stateFilePath()).catch(() => {});
1847
2175
  }
1848
2176
  const written = await writeTrace(events, target.id);
1849
2177
  const metrics = analyzeEvents(events);
@@ -1961,7 +2289,7 @@ async function performanceAnalyzeInsight(args = {}) {
1961
2289
  }
1962
2290
  let raw;
1963
2291
  try {
1964
- raw = await readFile3(args.tracePath, "utf8");
2292
+ raw = await readFile4(args.tracePath, "utf8");
1965
2293
  } catch (err) {
1966
2294
  throw new CdpError(`cannot read trace file '${args.tracePath}': ${err.message}`);
1967
2295
  }
@@ -1978,8 +2306,8 @@ async function performanceAnalyzeInsight(args = {}) {
1978
2306
  }
1979
2307
 
1980
2308
  // src/tools/heap.ts
1981
- import { mkdir as mkdir4, writeFile as writeFile4 } from "node:fs/promises";
1982
- import { join as join3 } from "node:path";
2309
+ import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
2310
+ import { join as join4 } from "node:path";
1983
2311
  var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
1984
2312
  function stamp3(targetId) {
1985
2313
  const short = targetId.slice(0, 8);
@@ -2004,9 +2332,9 @@ async function takeHeapsnapshot(args = {}) {
2004
2332
  if (snapshot.length === 0) {
2005
2333
  throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
2006
2334
  }
2007
- await mkdir4(ARTIFACT_DIR4, { recursive: true });
2008
- const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join3(ARTIFACT_DIR4, args.savePath) : join3(ARTIFACT_DIR4, `take_heapsnapshot-${stamp3(target.id)}.heapsnapshot`);
2009
- await writeFile4(path, snapshot, "utf8");
2335
+ await mkdir5(ARTIFACT_DIR4, { recursive: true });
2336
+ const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join4(ARTIFACT_DIR4, args.savePath) : join4(ARTIFACT_DIR4, `take_heapsnapshot-${stamp3(target.id)}.heapsnapshot`);
2337
+ await writeFile5(path, snapshot, "utf8");
2010
2338
  const bytes = Buffer.byteLength(snapshot, "utf8");
2011
2339
  return {
2012
2340
  path,
@@ -2019,7 +2347,7 @@ async function takeHeapsnapshot(args = {}) {
2019
2347
 
2020
2348
  // src/tools/lighthouse.ts
2021
2349
  import { spawn } from "node:child_process";
2022
- import { mkdir as mkdir5, readFile as readFile4, stat as stat2 } from "node:fs/promises";
2350
+ import { mkdir as mkdir6, readFile as readFile5, stat as stat2 } from "node:fs/promises";
2023
2351
  var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
2024
2352
  function cdpPort() {
2025
2353
  const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
@@ -2080,7 +2408,7 @@ async function lighthouseAudit(args) {
2080
2408
  const url = args.url.trim();
2081
2409
  const timeoutMs = args.timeoutMs ?? 120000;
2082
2410
  const port = cdpPort();
2083
- await mkdir5(ARTIFACT_DIR5, { recursive: true });
2411
+ await mkdir6(ARTIFACT_DIR5, { recursive: true });
2084
2412
  const outPath = args.savePath ?? `${ARTIFACT_DIR5}/lighthouse-${shortHost(url)}-${stamp4()}.json`;
2085
2413
  const formFactor = args.formFactor ?? "desktop";
2086
2414
  const lhArgs = [
@@ -2109,7 +2437,7 @@ async function lighthouseAudit(args) {
2109
2437
  }
2110
2438
  let raw;
2111
2439
  try {
2112
- raw = await readFile4(outPath, "utf8");
2440
+ raw = await readFile5(outPath, "utf8");
2113
2441
  } catch {
2114
2442
  raw = undefined;
2115
2443
  }
@@ -2117,7 +2445,7 @@ async function lighthouseAudit(args) {
2117
2445
  if (res.signal === "SIGKILL") {
2118
2446
  throw new CdpError(`lighthouse_audit timed out after ${timeoutMs}ms (no report written). stderr: ${truncate(res.stderr)}`);
2119
2447
  }
2120
- const hint = /not found|could not determine executable|npm ERR|E404|ERR_MODULE/i.test(res.stderr) ? " The Lighthouse CLI could not be fetched/installed via npx — ensure network access and that 'lighthouse' is installable." : "";
2448
+ const hint = /not found|could not determine executable|npm ERR|E404|ERR_MODULE/i.test(res.stderr) ? " The Lighthouse CLI could not be fetched/installed via npx; ensure network access and that 'lighthouse' is installable." : "";
2121
2449
  throw new CdpError(`lighthouse_audit produced no report (exit code ${res.code}).${hint} stderr: ${truncate(res.stderr)}`);
2122
2450
  }
2123
2451
  let report;
@@ -2191,7 +2519,10 @@ var TOOLS = {
2191
2519
  lighthouse_audit: lighthouseAudit,
2192
2520
  mock_request: mockRequest,
2193
2521
  list_mocks: listMocks,
2194
- clear_mocks: clearMocks
2522
+ clear_mocks: clearMocks,
2523
+ claim_page: onCdp(LEASE_TOOLS.claim_page),
2524
+ release_page: onCdp(LEASE_TOOLS.release_page),
2525
+ list_leases: onCdp(LEASE_TOOLS.list_leases)
2195
2526
  };
2196
2527
  var TOOL_NAMES = Object.keys(TOOLS);
2197
2528
 
@@ -2208,6 +2539,10 @@ var MANIFEST = [
2208
2539
  inputSchema: {
2209
2540
  type: "object",
2210
2541
  properties: {
2542
+ lease: {
2543
+ type: "string",
2544
+ 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."
2545
+ },
2211
2546
  all: {
2212
2547
  type: "boolean",
2213
2548
  description: "Include non-page targets (service/shared workers, background pages) when true; otherwise only page-type tabs are listed."
@@ -2223,9 +2558,25 @@ var MANIFEST = [
2223
2558
  inputSchema: {
2224
2559
  type: "object",
2225
2560
  properties: {
2561
+ lease: {
2562
+ type: "string",
2563
+ 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."
2564
+ },
2226
2565
  url: {
2227
2566
  type: "string",
2228
2567
  description: "URL to open in the new tab; defaults to about:blank."
2568
+ },
2569
+ claim: {
2570
+ type: "boolean",
2571
+ 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."
2572
+ },
2573
+ label: {
2574
+ type: "string",
2575
+ description: "Agent label recorded on the lease when claim:true, surfaced in conflict errors and list_leases. Defaults to pid-<pid>."
2576
+ },
2577
+ ttlMs: {
2578
+ type: "number",
2579
+ description: "How long the lease taken by claim:true survives without use before it is reclaimable. Defaults to CDP_LEASE_TTL_MS, else 900000 (15 minutes). Every checked call refreshes it, so an active agent never expires. Ignored without claim:true."
2229
2580
  }
2230
2581
  },
2231
2582
  required: [],
@@ -2238,6 +2589,10 @@ var MANIFEST = [
2238
2589
  inputSchema: {
2239
2590
  type: "object",
2240
2591
  properties: {
2592
+ lease: {
2593
+ type: "string",
2594
+ 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."
2595
+ },
2241
2596
  target: {
2242
2597
  type: "string",
2243
2598
  description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
@@ -2255,6 +2610,10 @@ var MANIFEST = [
2255
2610
  inputSchema: {
2256
2611
  type: "object",
2257
2612
  properties: {
2613
+ lease: {
2614
+ type: "string",
2615
+ 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."
2616
+ },
2258
2617
  target: {
2259
2618
  type: "string",
2260
2619
  description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
@@ -2272,6 +2631,10 @@ var MANIFEST = [
2272
2631
  inputSchema: {
2273
2632
  type: "object",
2274
2633
  properties: {
2634
+ lease: {
2635
+ type: "string",
2636
+ 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."
2637
+ },
2275
2638
  target: {
2276
2639
  type: "string",
2277
2640
  description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
@@ -2306,10 +2669,14 @@ var MANIFEST = [
2306
2669
  },
2307
2670
  {
2308
2671
  name: "wait_for",
2309
- description: "Poll a target page until the given substring appears in document.body.innerText (Runtime.evaluate on a fixed interval), or throw on timeout. Text-substring waiting only — no aria/role/selector or event variants; throws rather than returning {found:false}.",
2672
+ description: "Poll a target page until the given substring appears in document.body.innerText (Runtime.evaluate on a fixed interval), or throw on timeout. Text-substring waiting only: no aria/role/selector or event variants; throws rather than returning {found:false}.",
2310
2673
  inputSchema: {
2311
2674
  type: "object",
2312
2675
  properties: {
2676
+ lease: {
2677
+ type: "string",
2678
+ 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."
2679
+ },
2313
2680
  target: {
2314
2681
  type: "string",
2315
2682
  description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
@@ -2339,6 +2706,10 @@ var MANIFEST = [
2339
2706
  inputSchema: {
2340
2707
  type: "object",
2341
2708
  properties: {
2709
+ lease: {
2710
+ type: "string",
2711
+ 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."
2712
+ },
2342
2713
  target: {
2343
2714
  type: "string",
2344
2715
  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."
@@ -2364,10 +2735,14 @@ var MANIFEST = [
2364
2735
  },
2365
2736
  {
2366
2737
  name: "take_snapshot",
2367
- description: "Capture the page's accessibility tree (Accessibility.getFullAXTree) as a compact indented text tree where each line is prefixed with [uid] — the node's CDP backendDOMNodeId. These uids are the stateless element references that every interaction tool (click/hover/fill/etc.) feeds back to resolve a live DOM node (via DOM.resolveNode({ backendNodeId: uid })), so run this first to discover uids.",
2738
+ description: "Capture the page's accessibility tree (Accessibility.getFullAXTree) as a compact indented text tree where each line is prefixed with [uid], the node's CDP backendDOMNodeId. These uids are the stateless element references that every interaction tool (click/hover/fill/etc.) feeds back to resolve a live DOM node (via DOM.resolveNode({ backendNodeId: uid })), so run this first to discover uids.",
2368
2739
  inputSchema: {
2369
2740
  type: "object",
2370
2741
  properties: {
2742
+ lease: {
2743
+ type: "string",
2744
+ 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."
2745
+ },
2371
2746
  target: {
2372
2747
  type: "string",
2373
2748
  description: "Target page selector: 'active' (default, = first page-type target) | 'index:N' (Nth page-type target, 0-based) | 'url:<substring>' | 'title:<substring>' | '<32-hex targetId>' (exact target by id)."
@@ -2387,6 +2762,10 @@ var MANIFEST = [
2387
2762
  inputSchema: {
2388
2763
  type: "object",
2389
2764
  properties: {
2765
+ lease: {
2766
+ type: "string",
2767
+ 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."
2768
+ },
2390
2769
  target: {
2391
2770
  type: "string",
2392
2771
  description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
@@ -2423,6 +2802,10 @@ var MANIFEST = [
2423
2802
  inputSchema: {
2424
2803
  type: "object",
2425
2804
  properties: {
2805
+ lease: {
2806
+ type: "string",
2807
+ 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."
2808
+ },
2426
2809
  target: {
2427
2810
  type: "string",
2428
2811
  description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
@@ -2446,6 +2829,10 @@ var MANIFEST = [
2446
2829
  inputSchema: {
2447
2830
  type: "object",
2448
2831
  properties: {
2832
+ lease: {
2833
+ type: "string",
2834
+ 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."
2835
+ },
2449
2836
  target: {
2450
2837
  type: "string",
2451
2838
  description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
@@ -2494,6 +2881,10 @@ var MANIFEST = [
2494
2881
  inputSchema: {
2495
2882
  type: "object",
2496
2883
  properties: {
2884
+ lease: {
2885
+ type: "string",
2886
+ 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."
2887
+ },
2497
2888
  target: {
2498
2889
  type: "string",
2499
2890
  description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
@@ -2523,6 +2914,10 @@ var MANIFEST = [
2523
2914
  inputSchema: {
2524
2915
  type: "object",
2525
2916
  properties: {
2917
+ lease: {
2918
+ type: "string",
2919
+ 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."
2920
+ },
2526
2921
  target: {
2527
2922
  type: "string",
2528
2923
  description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
@@ -2565,6 +2960,10 @@ var MANIFEST = [
2565
2960
  inputSchema: {
2566
2961
  type: "object",
2567
2962
  properties: {
2963
+ lease: {
2964
+ type: "string",
2965
+ 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."
2966
+ },
2568
2967
  target: {
2569
2968
  type: "string",
2570
2969
  description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
@@ -2594,6 +2993,10 @@ var MANIFEST = [
2594
2993
  inputSchema: {
2595
2994
  type: "object",
2596
2995
  properties: {
2996
+ lease: {
2997
+ type: "string",
2998
+ 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."
2999
+ },
2597
3000
  target: {
2598
3001
  type: "string",
2599
3002
  description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
@@ -2622,6 +3025,10 @@ var MANIFEST = [
2622
3025
  inputSchema: {
2623
3026
  type: "object",
2624
3027
  properties: {
3028
+ lease: {
3029
+ type: "string",
3030
+ 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."
3031
+ },
2625
3032
  target: {
2626
3033
  type: "string",
2627
3034
  description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
@@ -2653,10 +3060,14 @@ var MANIFEST = [
2653
3060
  },
2654
3061
  {
2655
3062
  name: "take_screenshot",
2656
- description: "Capture the viewport (default), the full scrollable page (fullPage), or a single element (uid or selector — exactly one, mutually exclusive) via raw CDP Page.captureScreenshot. Writes a PNG/JPEG under /tmp/cdp-toolkit (override with savePath) and returns {path,bytes,format,target}; raw base64 is only included when returnBase64 is set. quality applies to jpeg only.",
3063
+ description: "Capture the viewport (default), the full scrollable page (fullPage), or a single element (uid or selector, exactly one and mutually exclusive) via raw CDP Page.captureScreenshot. Writes a PNG/JPEG under /tmp/cdp-toolkit (override with savePath) and returns {path,bytes,format,target}; raw base64 is only included when returnBase64 is set. quality applies to jpeg only.",
2657
3064
  inputSchema: {
2658
3065
  type: "object",
2659
3066
  properties: {
3067
+ lease: {
3068
+ type: "string",
3069
+ 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."
3070
+ },
2660
3071
  target: {
2661
3072
  type: "string",
2662
3073
  description: "Target page selector. undefined or 'active' -> first page target; '<32-hex targetId>' -> exact target by id; 'index:N' -> Nth page target (0-based); 'url:<substring>' -> first page whose url contains substring; 'title:<substring>' -> first page whose title contains substring."
@@ -2679,7 +3090,7 @@ var MANIFEST = [
2679
3090
  },
2680
3091
  uid: {
2681
3092
  type: "number",
2682
- description: "Element to clip to — a CDP backendDOMNodeId obtained from take_snapshot. Mutually exclusive with selector."
3093
+ description: "Element to clip to: a CDP backendDOMNodeId obtained from take_snapshot. Mutually exclusive with selector."
2683
3094
  },
2684
3095
  selector: {
2685
3096
  type: "string",
@@ -2704,6 +3115,10 @@ var MANIFEST = [
2704
3115
  inputSchema: {
2705
3116
  type: "object",
2706
3117
  properties: {
3118
+ lease: {
3119
+ type: "string",
3120
+ 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."
3121
+ },
2707
3122
  target: {
2708
3123
  type: "string",
2709
3124
  description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
@@ -2800,6 +3215,10 @@ var MANIFEST = [
2800
3215
  inputSchema: {
2801
3216
  type: "object",
2802
3217
  properties: {
3218
+ lease: {
3219
+ type: "string",
3220
+ 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."
3221
+ },
2803
3222
  target: {
2804
3223
  type: "string",
2805
3224
  description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
@@ -2834,6 +3253,10 @@ var MANIFEST = [
2834
3253
  inputSchema: {
2835
3254
  type: "object",
2836
3255
  properties: {
3256
+ lease: {
3257
+ type: "string",
3258
+ 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."
3259
+ },
2837
3260
  target: {
2838
3261
  type: "string",
2839
3262
  description: "Target page selector: 'active' (or omitted) for the first page-type target | 'index:N' (0-based) | 'url:<substring>' | 'title:<substring>' | a 32-hex <targetId>."
@@ -2867,6 +3290,10 @@ var MANIFEST = [
2867
3290
  inputSchema: {
2868
3291
  type: "object",
2869
3292
  properties: {
3293
+ lease: {
3294
+ type: "string",
3295
+ 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."
3296
+ },
2870
3297
  target: {
2871
3298
  type: "string",
2872
3299
  description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
@@ -2886,10 +3313,14 @@ var MANIFEST = [
2886
3313
  },
2887
3314
  {
2888
3315
  name: "get_console_message",
2889
- description: "Return a single console entry by zero-based index from the target's existing console buffer. Throws if the index is out of range — run list_console_messages (optionally with reload:true) first to populate the buffer.",
3316
+ description: "Return a single console entry by zero-based index from the target's existing console buffer. Throws if the index is out of range; run list_console_messages (optionally with reload:true) first to populate the buffer.",
2890
3317
  inputSchema: {
2891
3318
  type: "object",
2892
3319
  properties: {
3320
+ lease: {
3321
+ 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."
3323
+ },
2893
3324
  target: {
2894
3325
  type: "string",
2895
3326
  description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
@@ -2909,6 +3340,10 @@ var MANIFEST = [
2909
3340
  inputSchema: {
2910
3341
  type: "object",
2911
3342
  properties: {
3343
+ lease: {
3344
+ type: "string",
3345
+ 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."
3346
+ },
2912
3347
  target: {
2913
3348
  type: "string",
2914
3349
  description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
@@ -2932,17 +3367,21 @@ var MANIFEST = [
2932
3367
  },
2933
3368
  {
2934
3369
  name: "get_network_request",
2935
- 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 — it returns metadata plus bodyUnavailableReason).",
3370
+ 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).",
2936
3371
  inputSchema: {
2937
3372
  type: "object",
2938
3373
  properties: {
3374
+ lease: {
3375
+ type: "string",
3376
+ 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."
3377
+ },
2939
3378
  target: {
2940
3379
  type: "string",
2941
3380
  description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
2942
3381
  },
2943
3382
  requestId: {
2944
3383
  type: "string",
2945
- description: "Match by exact requestId (metadata only — cannot fetch a body, since reload re-mints requestIds)."
3384
+ description: "Match by exact requestId (metadata only, cannot fetch a body, since reload re-mints requestIds)."
2946
3385
  },
2947
3386
  url: {
2948
3387
  type: "string",
@@ -2967,6 +3406,10 @@ var MANIFEST = [
2967
3406
  inputSchema: {
2968
3407
  type: "object",
2969
3408
  properties: {
3409
+ lease: {
3410
+ type: "string",
3411
+ 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."
3412
+ },
2970
3413
  target: {
2971
3414
  type: "string",
2972
3415
  description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
@@ -2985,10 +3428,14 @@ var MANIFEST = [
2985
3428
  },
2986
3429
  {
2987
3430
  name: "performance_stop_trace",
2988
- description: "Stop the in-process trace started by performance_start_trace, drain buffered Tracing.dataCollected events, write the trace JSON under /tmp/cdp-toolkit, and return {path,bytes,events,metrics}. Throws if no live trace exists in this process (e.g. start ran in a different process) — use performance_trace instead. The 'target' arg is accepted only for API symmetry; at most one trace is ever live per process.",
3431
+ description: "Stop the in-process trace started by performance_start_trace, drain buffered Tracing.dataCollected events, write the trace JSON under /tmp/cdp-toolkit, and return {path,bytes,events,metrics}. Throws if no live trace exists in this process (e.g. start ran in a different process); use performance_trace instead. The 'target' arg is accepted only for API symmetry; at most one trace is ever live per process.",
2989
3432
  inputSchema: {
2990
3433
  type: "object",
2991
3434
  properties: {
3435
+ lease: {
3436
+ type: "string",
3437
+ 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."
3438
+ },
2992
3439
  target: {
2993
3440
  type: "string",
2994
3441
  description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
@@ -3000,10 +3447,14 @@ var MANIFEST = [
3000
3447
  },
3001
3448
  {
3002
3449
  name: "performance_analyze_insight",
3003
- description: "CDP-native approximation of the DevTools insight analyzer: read a trace JSON file (bare array or {traceEvents:[...]}) at the given tracePath and return headline metrics (FCP/LCP/CLS/TBT, long tasks, layout shifts). Requires an explicit tracePath returned by performance_trace/performance_stop_trace — there is no implicit 'latest trace'. Numbers approximate DevTools (no main-thread attribution or frame-scoped LCP).",
3450
+ description: "CDP-native approximation of the DevTools insight analyzer: read a trace JSON file (bare array or {traceEvents:[...]}) at the given tracePath and return headline metrics (FCP/LCP/CLS/TBT, long tasks, layout shifts). Requires an explicit tracePath returned by performance_trace/performance_stop_trace; there is no implicit 'latest trace'. Numbers approximate DevTools (no main-thread attribution or frame-scoped LCP).",
3004
3451
  inputSchema: {
3005
3452
  type: "object",
3006
3453
  properties: {
3454
+ lease: {
3455
+ type: "string",
3456
+ 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."
3457
+ },
3007
3458
  tracePath: {
3008
3459
  type: "string",
3009
3460
  description: "Filesystem path to a trace JSON file written by performance_trace or performance_stop_trace (bare array OR {traceEvents:[...]}). Required."
@@ -3015,10 +3466,14 @@ var MANIFEST = [
3015
3466
  },
3016
3467
  {
3017
3468
  name: "performance_trace",
3018
- description: "PRIMARY one-shot trace: start tracing, optionally reload or navigate the page, wait durationMs (default 3000), end the trace, write the trace JSON under /tmp/cdp-toolkit, and return {path,bytes,events,metrics,target}. Holds one connection open for the whole window, so it is immune to the cross-process limitation of start/stop — the recommended entry point.",
3469
+ description: "PRIMARY one-shot trace: start tracing, optionally reload or navigate the page, wait durationMs (default 3000), end the trace, write the trace JSON under /tmp/cdp-toolkit, and return {path,bytes,events,metrics,target}. Holds one connection open for the whole window, so it is immune to the cross-process limitation of start/stop, and it is the recommended entry point.",
3019
3470
  inputSchema: {
3020
3471
  type: "object",
3021
3472
  properties: {
3473
+ lease: {
3474
+ type: "string",
3475
+ 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."
3476
+ },
3022
3477
  target: {
3023
3478
  type: "string",
3024
3479
  description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
@@ -3053,6 +3508,10 @@ var MANIFEST = [
3053
3508
  inputSchema: {
3054
3509
  type: "object",
3055
3510
  properties: {
3511
+ lease: {
3512
+ type: "string",
3513
+ 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."
3514
+ },
3056
3515
  target: {
3057
3516
  type: "string",
3058
3517
  description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
@@ -3072,9 +3531,13 @@ var MANIFEST = [
3072
3531
  inputSchema: {
3073
3532
  type: "object",
3074
3533
  properties: {
3534
+ lease: {
3535
+ type: "string",
3536
+ 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."
3537
+ },
3075
3538
  url: {
3076
3539
  type: "string",
3077
- description: "The URL to audit. Required — never points at a user tab implicitly."
3540
+ description: "The URL to audit. Required, and never points at a user tab implicitly."
3078
3541
  },
3079
3542
  categories: {
3080
3543
  type: "array",
@@ -3108,10 +3571,14 @@ var MANIFEST = [
3108
3571
  },
3109
3572
  {
3110
3573
  name: "mock_request",
3111
- description: "Register a mock rule on a target's persistent fake-backend session (CDP Fetch domain): intercept requests whose URL matches urlPattern and fulfill them with a canned response, fail (abort) them, or continue them — optionally with fault injection (delayMs/failRate). The session survives reloads and navigations and lives until clear_mocks. Call repeatedly to mock several endpoints on the same target. Pass reload:true to apply immediately. Persistent across calls only via the MCP server (not the one-shot CLI).",
3574
+ description: "Register a mock rule on a target's persistent fake-backend session (CDP Fetch domain): intercept requests whose URL matches urlPattern and fulfill them with a canned response, fail (abort) them, or continue them, optionally with fault injection (delayMs/failRate). The session survives reloads and navigations and lives until clear_mocks. Call repeatedly to mock several endpoints on the same target. Pass reload:true to apply immediately. Persistent across calls only via the MCP server (not the one-shot CLI).",
3112
3575
  inputSchema: {
3113
3576
  type: "object",
3114
3577
  properties: {
3578
+ lease: {
3579
+ type: "string",
3580
+ 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."
3581
+ },
3115
3582
  target: {
3116
3583
  type: "string",
3117
3584
  description: "Target selector: active | index:N | url:<substr> | title:<substr> | <targetId>. Defaults to the active page."
@@ -3178,6 +3645,10 @@ var MANIFEST = [
3178
3645
  inputSchema: {
3179
3646
  type: "object",
3180
3647
  properties: {
3648
+ lease: {
3649
+ type: "string",
3650
+ 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."
3651
+ },
3181
3652
  target: {
3182
3653
  type: "string",
3183
3654
  description: "Unused filter placeholder; list_mocks returns all active sessions."
@@ -3193,6 +3664,10 @@ var MANIFEST = [
3193
3664
  inputSchema: {
3194
3665
  type: "object",
3195
3666
  properties: {
3667
+ lease: {
3668
+ type: "string",
3669
+ 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."
3670
+ },
3196
3671
  target: {
3197
3672
  type: "string",
3198
3673
  description: "Target selector whose mock session to clear. Defaults to the active page. Ignored when all:true."
@@ -3205,6 +3680,60 @@ var MANIFEST = [
3205
3680
  required: [],
3206
3681
  additionalProperties: false
3207
3682
  }
3683
+ },
3684
+ {
3685
+ name: "claim_page",
3686
+ 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.",
3687
+ inputSchema: {
3688
+ type: "object",
3689
+ properties: {
3690
+ targetId: {
3691
+ type: "string",
3692
+ description: "Claim this already-open page target instead of opening a new tab. Must be an exact target id, not a selector."
3693
+ },
3694
+ url: {
3695
+ type: "string",
3696
+ description: "When opening a fresh tab, navigate it here. Ignored when targetId is given."
3697
+ },
3698
+ label: {
3699
+ type: "string",
3700
+ description: "Agent label recorded on the lease and surfaced in conflict errors and list_leases. Defaults to pid-<pid>."
3701
+ },
3702
+ ttlMs: {
3703
+ type: "number",
3704
+ description: "How long the lease survives without use before it is reclaimable. Defaults to CDP_LEASE_TTL_MS, else 900000 (15 minutes). Every checked call refreshes it, so an active agent never expires."
3705
+ }
3706
+ },
3707
+ required: [],
3708
+ additionalProperties: false
3709
+ }
3710
+ },
3711
+ {
3712
+ name: "release_page",
3713
+ 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.",
3714
+ inputSchema: {
3715
+ type: "object",
3716
+ properties: {
3717
+ lease: {
3718
+ type: "string",
3719
+ description: "The opaque token claim_page (or new_page with claim:true) returned."
3720
+ }
3721
+ },
3722
+ required: [
3723
+ "lease"
3724
+ ],
3725
+ additionalProperties: false
3726
+ }
3727
+ },
3728
+ {
3729
+ name: "list_leases",
3730
+ 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.',
3731
+ inputSchema: {
3732
+ type: "object",
3733
+ properties: {},
3734
+ required: [],
3735
+ additionalProperties: false
3736
+ }
3208
3737
  }
3209
3738
  ];
3210
3739
 
@@ -3963,12 +4492,17 @@ async function safeTitle(conn, contextId) {
3963
4492
  async function pageInfoOf(conn, ctx) {
3964
4493
  return { id: ctx.context, url: ctx.url, title: await safeTitle(conn, ctx.context) };
3965
4494
  }
3966
- async function resolveContext(conn, selector) {
4495
+ async function resolveContext(conn, selector, opts = {}) {
3967
4496
  const { contexts } = await conn.send("browsingContext.getTree", {}).catch((e) => {
3968
4497
  throw mapBidiError(e);
3969
4498
  });
3970
4499
  if (!contexts.length)
3971
4500
  throw driverError2("no-such-target", "no browsing contexts available");
4501
+ const hit = await pickContext(conn, contexts, selector);
4502
+ await assertLeaseOk("firefox", hit.context, { lease: opts.lease, url: hit.url });
4503
+ return hit;
4504
+ }
4505
+ async function pickContext(conn, contexts, selector) {
3972
4506
  if (selector === undefined || selector === "active")
3973
4507
  return contexts[0];
3974
4508
  if (selector.startsWith("index:")) {
@@ -4489,7 +5023,7 @@ import { spawn as spawn2 } from "node:child_process";
4489
5023
  import { mkdtempSync, rmSync } from "node:fs";
4490
5024
  import { createConnection, createServer } from "node:net";
4491
5025
  import { tmpdir } from "node:os";
4492
- import { join as join4 } from "node:path";
5026
+ import { join as join5 } from "node:path";
4493
5027
  function resolveBinary() {
4494
5028
  const candidates = [
4495
5029
  process.env.FIREFOX_BIN,
@@ -4563,7 +5097,7 @@ async function launchFirefox(opts = {}) {
4563
5097
  const binary = resolveBinary();
4564
5098
  const port = await pickPort(opts.port);
4565
5099
  const isThrowaway = opts.profilePath === undefined;
4566
- const profilePath = opts.profilePath ?? mkdtempSync(join4(tmpdir(), "cdp-toolkit-ff-profile-"));
5100
+ const profilePath = opts.profilePath ?? mkdtempSync(join5(tmpdir(), "cdp-toolkit-ff-profile-"));
4567
5101
  const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
4568
5102
  if (opts.headless)
4569
5103
  args.push("--headless");
@@ -4828,11 +5362,12 @@ var BIDI_ONLY_TOOLS = {
4828
5362
  // src/firefox-tools.ts
4829
5363
  var FIREFOX_TOOLS = {
4830
5364
  ...SHARED_TOOLS,
4831
- ...BIDI_ONLY_TOOLS
5365
+ ...BIDI_ONLY_TOOLS,
5366
+ ...LEASE_TOOLS
4832
5367
  };
4833
5368
 
4834
5369
  // src/mcp.ts
4835
- var VERSION = "1.1.0";
5370
+ var VERSION = "1.2.0";
4836
5371
  var BROWSER = resolveBrowserKind(stripBrowserFlag(process.argv.slice(2)));
4837
5372
  var AVAILABILITY = toolAvailability(BROWSER);
4838
5373
  var AVAILABLE_NAMES = new Set(AVAILABILITY.available);
@@ -4862,29 +5397,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4862
5397
  isError: true
4863
5398
  };
4864
5399
  }
4865
- if (BROWSER === "chrome") {
4866
- const fn = dispatch[name];
4867
- if (!fn)
5400
+ return withLeaseScope(leaseFromArgs(args), async () => {
5401
+ if (BROWSER === "chrome") {
5402
+ const fn = dispatch[name];
5403
+ if (!fn)
5404
+ return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
5405
+ try {
5406
+ const result = await fn(args);
5407
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
5408
+ } catch (err) {
5409
+ const message = err instanceof Error ? err.message : String(err);
5410
+ return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
5411
+ }
5412
+ }
5413
+ const neutralFn = neutralDispatch[name];
5414
+ if (!neutralFn)
4868
5415
  return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
4869
5416
  try {
4870
- const result = await fn(args);
5417
+ const session = await getOrCreateFirefoxSession();
5418
+ const result = await neutralFn(session.driver, args);
4871
5419
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
4872
5420
  } catch (err) {
4873
5421
  const message = err instanceof Error ? err.message : String(err);
4874
5422
  return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
4875
5423
  }
4876
- }
4877
- const neutralFn = neutralDispatch[name];
4878
- if (!neutralFn)
4879
- return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
4880
- try {
4881
- const session = await getOrCreateFirefoxSession();
4882
- const result = await neutralFn(session.driver, args);
4883
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
4884
- } catch (err) {
4885
- const message = err instanceof Error ? err.message : String(err);
4886
- return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
4887
- }
5424
+ });
4888
5425
  });
4889
5426
  auditCoverage();
4890
5427
  await server.connect(new StdioServerTransport);