cdp-toolkit 1.1.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +203 -9
- package/CONTRACT.md +7 -5
- package/README.md +113 -33
- package/dist/cli.js +764 -64
- package/dist/index.js +650 -42
- package/dist/mcp.js +1091 -77
- package/package.json +4 -3
- package/src/bidi/driver.ts +167 -4
- package/src/capabilities.ts +1 -1
- package/src/cdp/driver.ts +145 -1
- package/src/cli.ts +47 -22
- package/src/client.ts +25 -7
- package/src/driver.ts +107 -1
- package/src/firefox-tools.ts +6 -3
- package/src/index.ts +14 -2
- package/src/leases-tools.ts +107 -0
- package/src/leases.ts +541 -0
- package/src/manifest.ts +339 -15
- package/src/mcp.ts +27 -19
- package/src/origins.ts +266 -0
- package/src/shared-tools.ts +365 -14
- package/src/tools/console.ts +3 -3
- package/src/tools/evaluate.ts +2 -2
- package/src/tools/heap.ts +3 -3
- package/src/tools/lighthouse.ts +4 -4
- package/src/tools/navigation.ts +1 -1
- package/src/tools/network.ts +7 -7
- package/src/tools/network_mock.ts +8 -8
- package/src/tools/performance.ts +11 -11
- package/src/tools/recorder.ts +5 -5
- package/src/tools/snapshot.ts +2 -2
package/src/driver.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ADR-001: one browser-neutral Driver interface behind the
|
|
2
|
+
* ADR-001: one browser-neutral Driver interface behind the 36 tools.
|
|
3
3
|
*
|
|
4
4
|
* Context. Every tool in src/tools/ is a stateless one-shot on withPage():
|
|
5
5
|
* open a socket, act, always close. That is only correct because a CDP `Uid`
|
|
@@ -281,6 +281,80 @@ export interface HandledDialogInfo extends DialogInfo {
|
|
|
281
281
|
handled: true;
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
/**
|
|
285
|
+
* One cookie, neutral over CDP's `Network.Cookie` and BiDi's `network.Cookie`.
|
|
286
|
+
*
|
|
287
|
+
* Every flag a caller needs to reason about a session credential is present:
|
|
288
|
+
* an httpOnly cookie is exactly the one `document.cookie` cannot read, so a
|
|
289
|
+
* shape that dropped `httpOnly` would make the reason this exists invisible.
|
|
290
|
+
*
|
|
291
|
+
* Two normalizations, applied by both drivers so the shape does not change
|
|
292
|
+
* with the backend:
|
|
293
|
+
* - `expires` is a Unix timestamp in SECONDS, or -1 for a session cookie.
|
|
294
|
+
* CDP reports exactly that; BiDi reports an optional `expiry` and omits it
|
|
295
|
+
* for a session cookie, which the BiDi driver maps to -1.
|
|
296
|
+
* - `sameSite` is lowercased to BiDi's vocabulary ("strict" | "lax" | "none"
|
|
297
|
+
* | "default"). CDP capitalizes it and omits it entirely when unset, which
|
|
298
|
+
* the CDP driver maps to "default".
|
|
299
|
+
*/
|
|
300
|
+
export interface BrowserCookie {
|
|
301
|
+
name: string;
|
|
302
|
+
value: string;
|
|
303
|
+
domain: string;
|
|
304
|
+
path: string;
|
|
305
|
+
/** Unix seconds, or -1 for a session cookie. */
|
|
306
|
+
expires: number;
|
|
307
|
+
/** Byte length of name plus value, as the browser reports it. */
|
|
308
|
+
size: number;
|
|
309
|
+
httpOnly: boolean;
|
|
310
|
+
secure: boolean;
|
|
311
|
+
sameSite: "strict" | "lax" | "none" | "default";
|
|
312
|
+
/** True when the cookie expires with the browsing session (no expiry set). */
|
|
313
|
+
session: boolean;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* One cookie to WRITE, neutral over CDP's `Network.setCookie` and BiDi's
|
|
318
|
+
* `storage.setCookie`.
|
|
319
|
+
*
|
|
320
|
+
* `url` and `domain` are both optional here but not both omittable: the cookie
|
|
321
|
+
* has to be attributed to a site somehow, and every backend refuses without
|
|
322
|
+
* one. The tool layer enforces that before either driver is reached, so the
|
|
323
|
+
* refusal is one message rather than two different protocol errors.
|
|
324
|
+
*
|
|
325
|
+
* `expires` follows the read shape: Unix seconds. Omit it for a session cookie,
|
|
326
|
+
* which is what both backends do when no expiry is supplied.
|
|
327
|
+
*/
|
|
328
|
+
export interface SetCookieParams {
|
|
329
|
+
name: string;
|
|
330
|
+
value: string;
|
|
331
|
+
/** The URL the cookie is being set for. CDP derives domain, path and secure
|
|
332
|
+
* from it; BiDi has no url parameter, so its driver derives the domain. */
|
|
333
|
+
url?: string;
|
|
334
|
+
domain?: string;
|
|
335
|
+
path?: string;
|
|
336
|
+
/** Unix seconds. Omitted means a session cookie. */
|
|
337
|
+
expires?: number;
|
|
338
|
+
httpOnly?: boolean;
|
|
339
|
+
secure?: boolean;
|
|
340
|
+
sameSite?: "strict" | "lax" | "none" | "default";
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Which cookies to DELETE, neutral over CDP's `Network.deleteCookies` and
|
|
345
|
+
* BiDi's `storage.deleteCookies` filter.
|
|
346
|
+
*
|
|
347
|
+
* `name` is required and there is no wildcard: this deletes a named cookie, not
|
|
348
|
+
* a swathe of them. As with SetCookieParams, one of `url` or `domain` must be
|
|
349
|
+
* present, and the tool layer enforces it.
|
|
350
|
+
*/
|
|
351
|
+
export interface DeleteCookiesFilter {
|
|
352
|
+
name: string;
|
|
353
|
+
url?: string;
|
|
354
|
+
domain?: string;
|
|
355
|
+
path?: string;
|
|
356
|
+
}
|
|
357
|
+
|
|
284
358
|
export interface InterceptRule {
|
|
285
359
|
/** Glob: `*` any run, `?` one char. Both drivers normalize to this. */
|
|
286
360
|
urlPattern: string;
|
|
@@ -403,6 +477,38 @@ export interface PageDriver {
|
|
|
403
477
|
opts?: { timeoutMs?: number; autoMs?: number },
|
|
404
478
|
): Promise<HandledDialogInfo | { handled: HandledDialogInfo[]; count: number }>;
|
|
405
479
|
|
|
480
|
+
/* cookies */
|
|
481
|
+
/**
|
|
482
|
+
* Every cookie visible to THIS page, httpOnly ones included.
|
|
483
|
+
*
|
|
484
|
+
* Deliberately page-scoped, not browser-scoped. Both protocols offer a
|
|
485
|
+
* browser-wide jar (CDP's `Storage.getCookies` with no browserContextId,
|
|
486
|
+
* BiDi's storageKey partition), and both would hand back every credential
|
|
487
|
+
* for every site the browser has ever visited on a call that named one tab.
|
|
488
|
+
* A tool scoped to a target returns that target's cookies; a caller who
|
|
489
|
+
* wants another site's cookies points at a page on that site.
|
|
490
|
+
*/
|
|
491
|
+
getCookies(): Promise<BrowserCookie[]>;
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Write one cookie into the store this page reads from.
|
|
495
|
+
*
|
|
496
|
+
* Resolves only when the backend confirms the write. A backend that reports
|
|
497
|
+
* a refusal (CDP's `Network.setCookie` answers `{success:false}` for a cookie
|
|
498
|
+
* it will not accept) must throw rather than resolve, because a resolved
|
|
499
|
+
* promise here reads as "the cookie is set" and a caller will believe it.
|
|
500
|
+
*/
|
|
501
|
+
setCookie(params: SetCookieParams): Promise<void>;
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Remove the cookies matching the filter from the store this page reads from.
|
|
505
|
+
*
|
|
506
|
+
* Neither backend reports how many cookies it removed, so this returns
|
|
507
|
+
* nothing rather than an invented count. Deleting a cookie that was never
|
|
508
|
+
* there is not an error on either backend.
|
|
509
|
+
*/
|
|
510
|
+
deleteCookies(filter: DeleteCookiesFilter): Promise<void>;
|
|
511
|
+
|
|
406
512
|
/* network */
|
|
407
513
|
/** Begin retaining response bodies. REQUIRED before getResponseBody unless
|
|
408
514
|
* the driver declares "network.responseBodyPostHoc". Returns a stop fn. */
|
package/src/firefox-tools.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The complete Firefox tool registry: the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The complete Firefox tool registry: the 23 tools shared with Chrome (src/shared-tools.ts), the
|
|
3
|
+
* 7 Firefox-only console/network/mock tools (src/bidi-tools.ts), and the 3 lease tools
|
|
4
|
+
* (src/leases-tools.ts), which are backend-neutral by construction. This is what src/neutral.ts
|
|
5
|
+
* used to be; it now composes three files instead of reimplementing everything in one.
|
|
5
6
|
*/
|
|
6
7
|
import type { BrowserDriver } from "./driver.ts";
|
|
7
8
|
import type { ToolName } from "./index.ts";
|
|
8
9
|
import { SHARED_TOOLS } from "./shared-tools.ts";
|
|
9
10
|
import { BIDI_ONLY_TOOLS } from "./bidi-tools.ts";
|
|
11
|
+
import { LEASE_TOOLS } from "./leases-tools.ts";
|
|
10
12
|
|
|
11
13
|
export const FIREFOX_TOOLS: Partial<Record<ToolName, (driver: BrowserDriver, args: never) => Promise<unknown>>> = {
|
|
12
14
|
...SHARED_TOOLS,
|
|
13
15
|
...BIDI_ONLY_TOOLS,
|
|
16
|
+
...LEASE_TOOLS,
|
|
14
17
|
};
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// the byte-identical-Chrome-behavior contract this bind honors, and for the 7 tools that stay
|
|
20
20
|
// two implementations (console/network/mock, immediately below, unchanged from before).
|
|
21
21
|
import { createCdpDriver } from "./cdp/driver.ts";
|
|
22
|
+
import { LEASE_TOOLS } from "./leases-tools.ts";
|
|
22
23
|
import { SHARED_TOOLS } from "./shared-tools.ts";
|
|
23
24
|
// --- console + network (read the recorder buffer) ---
|
|
24
25
|
import { listConsoleMessages, getConsoleMessage } from "./tools/console.ts";
|
|
@@ -55,8 +56,11 @@ function onCdp<A>(fn: (driver: ReturnType<typeof createCdpDriver>, args: A) => P
|
|
|
55
56
|
* their canonical MCP (snake_case) name, plus one convenience superset tool
|
|
56
57
|
* (`performance_trace`, a robust single-call trace : the start/stop pair cannot
|
|
57
58
|
* span two stateless CLI processes), plus a 3-tool network-mocking group
|
|
58
|
-
* (mock_request/list_mocks/clear_mocks : a persistent per-target fake backend)
|
|
59
|
-
*
|
|
59
|
+
* (mock_request/list_mocks/clear_mocks : a persistent per-target fake backend),
|
|
60
|
+
* plus a 3-tool cookie group (list_cookies/set_cookie/delete_cookies : the
|
|
61
|
+
* cookie store including httpOnly cookies, which no page script can read or
|
|
62
|
+
* write).
|
|
63
|
+
* 39 entries total. Listed explicitly so the mapping is auditable at a glance and
|
|
60
64
|
* the CLI can `--list` it.
|
|
61
65
|
*
|
|
62
66
|
* 20 of the 29 MCP-parity tools (pages/navigation/evaluate/snapshot/interaction/
|
|
@@ -75,6 +79,10 @@ export const TOOLS = {
|
|
|
75
79
|
wait_for: onCdp(SHARED_TOOLS.wait_for),
|
|
76
80
|
// evaluate (1)
|
|
77
81
|
evaluate_script: onCdp(SHARED_TOOLS.evaluate_script),
|
|
82
|
+
// cookies (3) : toolkit addition, reads and writes httpOnly cookies that document.cookie cannot touch
|
|
83
|
+
list_cookies: onCdp(SHARED_TOOLS.list_cookies),
|
|
84
|
+
set_cookie: onCdp(SHARED_TOOLS.set_cookie),
|
|
85
|
+
delete_cookies: onCdp(SHARED_TOOLS.delete_cookies),
|
|
78
86
|
// snapshot (1)
|
|
79
87
|
take_snapshot: onCdp(SHARED_TOOLS.take_snapshot),
|
|
80
88
|
// interaction (8)
|
|
@@ -110,6 +118,10 @@ export const TOOLS = {
|
|
|
110
118
|
mock_request: mockRequest,
|
|
111
119
|
list_mocks: listMocks,
|
|
112
120
|
clear_mocks: clearMocks,
|
|
121
|
+
// tab leases (3) : opt-in ownership so many agents can share one browser
|
|
122
|
+
claim_page: onCdp(LEASE_TOOLS.claim_page),
|
|
123
|
+
release_page: onCdp(LEASE_TOOLS.release_page),
|
|
124
|
+
list_leases: onCdp(LEASE_TOOLS.list_leases),
|
|
113
125
|
} satisfies Record<string, (args: never) => Promise<unknown>>;
|
|
114
126
|
|
|
115
127
|
/** Canonical MCP tool name accepted by the registry. */
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three lease tools, shaped like src/shared-tools.ts entries
|
|
3
|
+
* ((driver, args) => Promise<result>) so ONE implementation serves Chrome and
|
|
4
|
+
* Firefox, exactly as the other 20 unified tools do.
|
|
5
|
+
*/
|
|
6
|
+
import type { BrowserDriver } from "./driver.ts";
|
|
7
|
+
import { newTrackedPage } from "./origins.ts";
|
|
8
|
+
import {
|
|
9
|
+
claimLease,
|
|
10
|
+
defaultLabel,
|
|
11
|
+
leaseTtlMs,
|
|
12
|
+
listLeases,
|
|
13
|
+
releaseLease,
|
|
14
|
+
type LeaseBackend,
|
|
15
|
+
type LeaseSummary,
|
|
16
|
+
type LeaseToken,
|
|
17
|
+
} from "./leases.ts";
|
|
18
|
+
|
|
19
|
+
class LeaseToolError extends Error {}
|
|
20
|
+
|
|
21
|
+
/** The driver's uid scheme is the one stable per-backend discriminator on the
|
|
22
|
+
* BrowserDriver surface ("cdp" for Chrome, "bidi" for Firefox). */
|
|
23
|
+
function backendOf(driver: BrowserDriver): LeaseBackend {
|
|
24
|
+
return driver.scheme === "bidi" ? "firefox" : "chrome";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ClaimPageResult {
|
|
28
|
+
lease: LeaseToken;
|
|
29
|
+
targetId: string;
|
|
30
|
+
url: string;
|
|
31
|
+
label: string;
|
|
32
|
+
ttlMs: number;
|
|
33
|
+
expiresAt: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Claim a tab. With no targetId, opens a fresh tab first (optionally at `url`)
|
|
38
|
+
* and claims that, so "give me my own tab" is one call. With a targetId, claims
|
|
39
|
+
* an already-open tab.
|
|
40
|
+
*/
|
|
41
|
+
export async function claimPage(
|
|
42
|
+
driver: BrowserDriver,
|
|
43
|
+
args: { targetId?: string; url?: string; label?: string; ttlMs?: number } = {},
|
|
44
|
+
): Promise<ClaimPageResult> {
|
|
45
|
+
const backend = backendOf(driver);
|
|
46
|
+
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
47
|
+
const pages = await driver.listPages();
|
|
48
|
+
let targetId = args.targetId;
|
|
49
|
+
let url = args.url ?? "about:blank";
|
|
50
|
+
if (targetId === undefined || targetId === "") {
|
|
51
|
+
// Creation ledger, not the lease: this record survives release_page and
|
|
52
|
+
// expiry, so the tab stays attributable to `label` after the lease is gone.
|
|
53
|
+
// Claiming an EXISTING tab (the else branch) records nothing, because this
|
|
54
|
+
// toolkit did not create it and provenance is not ownership.
|
|
55
|
+
const page = await newTrackedPage(driver, backend, { url: args.url, label });
|
|
56
|
+
targetId = page.id;
|
|
57
|
+
url = page.url;
|
|
58
|
+
} else {
|
|
59
|
+
const existing = pages.find((p) => p.id === targetId);
|
|
60
|
+
if (!existing) throw new LeaseToolError(`claim_page: no page target with id '${targetId}'`);
|
|
61
|
+
url = existing.url;
|
|
62
|
+
}
|
|
63
|
+
const liveIds = [...pages.map((p) => p.id), targetId];
|
|
64
|
+
const { record, token } = await claimLease(backend, targetId, {
|
|
65
|
+
label,
|
|
66
|
+
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
|
|
67
|
+
liveIds,
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
lease: token,
|
|
71
|
+
targetId,
|
|
72
|
+
url,
|
|
73
|
+
label: record.label,
|
|
74
|
+
ttlMs: record.ttlMs,
|
|
75
|
+
expiresAt: record.lastUsedAt + record.ttlMs,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Release a held lease. Idempotent: an already-released or reclaimed lease
|
|
80
|
+
* reports released:false rather than throwing. */
|
|
81
|
+
export async function releasePage(
|
|
82
|
+
_driver: BrowserDriver,
|
|
83
|
+
args: { lease?: LeaseToken },
|
|
84
|
+
): Promise<{ released: boolean }> {
|
|
85
|
+
if (typeof args.lease !== "string" || !args.lease.length) {
|
|
86
|
+
throw new LeaseToolError("release_page requires the 'lease' token returned by claim_page");
|
|
87
|
+
}
|
|
88
|
+
return releaseLease(args.lease);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Enumerate active leases for diagnosis. Requires no token by design. */
|
|
92
|
+
export async function listLeasesTool(
|
|
93
|
+
driver: BrowserDriver,
|
|
94
|
+
_args: Record<string, never> = {} as Record<string, never>,
|
|
95
|
+
): Promise<{ leases: LeaseSummary[]; count: number }> {
|
|
96
|
+
const liveIds = (await driver.listPages()).map((p) => p.id);
|
|
97
|
+
// liveBackend scopes the target-gone test to the browser these ids came from,
|
|
98
|
+
// so leases held on the OTHER backend are not mislabeled as reclaimable.
|
|
99
|
+
const leases = await listLeases({ liveIds, liveBackend: backendOf(driver) });
|
|
100
|
+
return { leases, count: leases.length };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const LEASE_TOOLS = {
|
|
104
|
+
claim_page: claimPage,
|
|
105
|
+
release_page: releasePage,
|
|
106
|
+
list_leases: listLeasesTool,
|
|
107
|
+
} satisfies Record<string, (driver: BrowserDriver, args: never) => Promise<unknown>>;
|