cdp-toolkit 2.3.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cdp-toolkit",
3
- "version": "2.3.0",
4
- "description": "A lightweight chrome-devtools-mcp alternative: an MCP server + CLI that drives the tabs you name, one target per call over one direct WebSocket, with a bounded timeout on every call, so a stuck page can't wedge your agent or force a /mcp restart. Chrome over CDP and Firefox over WebDriver BiDi, with tools a backend can't run absent from tools/list rather than throwing mid-task. 45 tools including built-in network mocking, a cookie group, tab leases with human-activity detection, real HTML5 drag-and-drop, raw mouse/scroll dispatch, downloads, permission grants, and tab-to-video screen recording so several agents can drive one browser without stealing each other's tabs or a human's. No Puppeteer, no all-target fan-out.",
3
+ "version": "2.5.1",
4
+ "description": "A lightweight chrome-devtools-mcp alternative: an MCP server + CLI that drives the tabs you name, one target per call over one direct WebSocket, with a bounded timeout on every call, so a stuck page can't wedge your agent or force a /mcp restart. Chrome over CDP and Firefox over WebDriver BiDi, with tools a backend can't run absent from tools/list rather than throwing mid-task. 48 tools including built-in network mocking, a cookie group, tab leases with human-activity detection, real HTML5 drag-and-drop, raw mouse/scroll dispatch, downloads, permission grants, and tab-to-video screen recording so several agents can drive one browser without stealing each other's tabs or a human's. No Puppeteer, no all-target fan-out.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": {
@@ -85,7 +85,8 @@
85
85
  "staleness:smoke": "bun run ./test/staleness-smoke.ts",
86
86
  "input:smoke": "bun run ./test/input-smoke.ts",
87
87
  "extension:smoke": "bun run ./test/extension-smoke.ts",
88
- "focus:smoke": "bun run ./test/focus-emulation-smoke.ts"
88
+ "focus:smoke": "bun run ./test/focus-emulation-smoke.ts",
89
+ "extract:smoke": "bun run ./test/extract-smoke.ts"
89
90
  },
90
91
  "devDependencies": {
91
92
  "@modelcontextprotocol/client": "^2.0.0",
@@ -1,17 +1,20 @@
1
1
  /**
2
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.
3
+ * 7 Firefox-only console/network/mock tools (src/bidi-tools.ts), the 3 lease tools
4
+ * (src/leases-tools.ts), and extract_page (src/tools/extract.ts), which is backend-neutral by
5
+ * construction (its page work runs entirely through Driver.evaluate). This is what src/neutral.ts
6
+ * used to be; it now composes four files instead of reimplementing everything in one.
6
7
  */
7
8
  import type { BrowserDriver } from "./driver.ts";
8
9
  import type { ToolName } from "./index.ts";
9
10
  import { SHARED_TOOLS } from "./shared-tools.ts";
10
11
  import { BIDI_ONLY_TOOLS } from "./bidi-tools.ts";
11
12
  import { LEASE_TOOLS } from "./leases-tools.ts";
13
+ import { extractPage } from "./tools/extract.ts";
12
14
 
13
15
  export const FIREFOX_TOOLS: Partial<Record<ToolName, (driver: BrowserDriver, args: never) => Promise<unknown>>> = {
14
16
  ...SHARED_TOOLS,
15
17
  ...BIDI_ONLY_TOOLS,
16
18
  ...LEASE_TOOLS,
19
+ extract_page: extractPage,
17
20
  };
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Two exports:
5
5
  * 1. `TOOLS` : the registry mapping every chrome-devtools-mcp tool name
6
6
  * (snake_case string, all 29 of them) plus the toolkit's own superset
7
- * tools, 45 entries in all, to its raw-CDP implementation function. The
7
+ * tools, 48 entries in all, to its raw-CDP implementation function. The
8
8
  * CLI and any embedder dispatch through this single table.
9
9
  * 2. The client primitives, re-exported so consumers can build their own
10
10
  * flows on the same connection/timeout machinery the tools use.
@@ -48,6 +48,8 @@ import { focusEmulation, clickFocusGated } from "./tools/focus-emulation.ts";
48
48
  // --- downloads + permissions (browser-endpoint state on a standing connection) : chrome-only, 1.8.0 Track P3 ---
49
49
  import { waitForDownload } from "./tools/downloads.ts";
50
50
  import { grantPermissions } from "./tools/permissions.ts";
51
+ // --- structured extraction (cleaned page HTML -> OpenAI-compatible endpoint, both backends) ---
52
+ import { extractPage } from "./tools/extract.ts";
51
53
 
52
54
  /** A toolkit tool: a single typed-args function returning JSON-serializable data. */
53
55
  export type ToolFn = (args: never) => Promise<unknown>;
@@ -83,8 +85,8 @@ function onCdp<A>(fn: (driver: ReturnType<typeof createCdpDriver>, args: A) => P
83
85
  * named path, and `grant_permissions`, which answers permission prompts for an origin; both
84
86
  * chrome-only and both driven from a standing browser-endpoint connection, see
85
87
  * tools/browser-session.ts).
86
- * 29 + 1 + 3 + 3 + 3 + 2 + 2 + 2 = 45 entries total. Listed explicitly so the mapping is auditable at a glance and
87
- * the CLI can `--list` it.
88
+ * 29 + 1 + 3 + 3 + 3 + 2 + 2 + 2 + 1 (extract_page) + 2 (focus pair) = 48 entries total. Listed explicitly so the mapping is auditable at a glance and
89
+ * the CLI can `--list` it.
88
90
  *
89
91
  * 20 of the 29 MCP-parity tools (pages/navigation/evaluate/snapshot/interaction/
90
92
  * screenshot+emulation/dialogs) now route through shared-tools.ts's single Driver-based
@@ -157,6 +159,11 @@ export const TOOLS = {
157
159
  // connection rather than a per-call socket : see that module's header for the measurements.
158
160
  wait_for_download: waitForDownload,
159
161
  grant_permissions: grantPermissions,
162
+ // structured extraction (1) : toolkit addition, both backends — a page's cleaned HTML
163
+ // POSTed to an OpenAI-compatible /chat/completions endpoint (default: loopback
164
+ // llm-ferry) with the caller's JSON Schema as a strict response_format, returning
165
+ // schema-conformant JSON + token usage. API key is env-only (CDP_EXTRACT_API_KEY).
166
+ extract_page: onCdp(extractPage),
160
167
  // tab leases (3) : opt-in ownership so many agents can share one browser
161
168
  claim_page: onCdp(LEASE_TOOLS.claim_page),
162
169
  release_page: onCdp(LEASE_TOOLS.release_page),
package/src/manifest.ts CHANGED
@@ -1763,5 +1763,79 @@ export const MANIFEST: ToolSpec[] = [
1763
1763
  "required": [],
1764
1764
  "additionalProperties": false
1765
1765
  }
1766
+ },
1767
+ {
1768
+ "name": "extract_page",
1769
+ "description": "Extract schema-conformant JSON from the target page's cleaned HTML via an OpenAI-compatible endpoint; default endpoint is local loopback.",
1770
+ "inputSchema": {
1771
+ "type": "object",
1772
+ "properties": {
1773
+ "lease": {
1774
+ "type": "string",
1775
+ "description": "Lease token; omit for a tab this process already holds. See server instructions."
1776
+ },
1777
+ "target": {
1778
+ "type": "string",
1779
+ "description": "Page selector (index:/url:/title:/label:/targetId, default active); grammar in server instructions."
1780
+ },
1781
+ "schema": {
1782
+ "type": "object",
1783
+ "description": "JSON Schema (type object) the extracted JSON must conform to; extraction instructions live in its property descriptions."
1784
+ },
1785
+ "selector": {
1786
+ "type": "string",
1787
+ "description": "CSS selector scoping extraction to a subtree."
1788
+ },
1789
+ "source": {
1790
+ "type": "string",
1791
+ "enum": [
1792
+ "dom",
1793
+ "served"
1794
+ ],
1795
+ "description": "Page source to read; 'served' currently returns not-implemented."
1796
+ },
1797
+ "clean": {
1798
+ "type": "string",
1799
+ "enum": [
1800
+ "standard",
1801
+ "aggressive",
1802
+ "none"
1803
+ ],
1804
+ "description": "HTML cleaning level."
1805
+ },
1806
+ "maxChars": {
1807
+ "type": "integer",
1808
+ "description": "Character cap on the cleaned HTML; default 300000."
1809
+ },
1810
+ "timeoutMs": {
1811
+ "type": "integer",
1812
+ "description": "Timeout in milliseconds; default 90000, max 300000."
1813
+ },
1814
+ "prompt": {
1815
+ "type": "string",
1816
+ "enum": [
1817
+ "html",
1818
+ "schematron"
1819
+ ],
1820
+ "description": "Prompt shape: 'html' (default) sends cleaned HTML only; 'schematron' uses the open-weight Schematron model-card prompt with the schema inline (or CDP_EXTRACT_PROMPT)."
1821
+ },
1822
+ "model": {
1823
+ "type": "string",
1824
+ "description": "Extraction model name; default 'schematron' (or CDP_EXTRACT_MODEL)."
1825
+ },
1826
+ "baseUrl": {
1827
+ "type": "string",
1828
+ "description": "OpenAI-compatible endpoint base URL; default local loopback (or CDP_EXTRACT_BASE_URL)."
1829
+ },
1830
+ "savePath": {
1831
+ "type": "string",
1832
+ "description": "Output file path."
1833
+ }
1834
+ },
1835
+ "required": [
1836
+ "schema"
1837
+ ],
1838
+ "additionalProperties": false
1839
+ }
1766
1840
  }
1767
1841
  ];
package/src/toolDocs.ts CHANGED
@@ -484,5 +484,22 @@ export const TOOL_DOCS: Record<string, ToolDoc> = {
484
484
  "list_leases": {
485
485
  "description": "Enumerate every active lease for diagnosis - backend, target id, label, pid, createdAt, lastUsedAt, ttlMs, whether the owning process is alive, and whether the lease is reclaimable and why. No token required; it never returns the lease nonce, so it cannot be used to forge one. An unreadable or unparseable lease row instead carries unreadable, with label, pid and timestamps zeroed and stale forced false (an unreadable lease must never read as free to take). Under CDP_REQUIRE_LEASE it reaps abandoned agent tabs exactly as list_pages does, into an additive reaped array, omitting their rows. Where the tab can answer, a row also carries humanActiveMs, ms since input this server did not dispatch - note lastUsedAt tracks TOOLKIT calls only, so a tab a person has typed in for ten minutes looks idle, which is what this exposes; the field is absent, never null, when the tab cannot answer, and that absence means 'no answer', never 'nobody is there'. idleMs (now - lastUsedAt) and expiresAt (lastUsedAt + ttlMs) are computed fresh each call and omitted on an unreadable row.",
486
486
  "params": {}
487
+ },
488
+ "extract_page": {
489
+ "description": "Structured extraction: take the target page's HTML, clean it, and send it to an OpenAI-compatible /chat/completions endpoint along with your JSON Schema, getting back schema-conformant JSON instead of prose. THE DEFAULT ENDPOINT IS LOOPBACK - llm-ferry at http://127.0.0.1:8090/v1 with model 'schematron' - so page content stays ON THIS MACHINE unless the operator overrides baseUrl (or sets CDP_EXTRACT_BASE_URL). Configuration: CDP_EXTRACT_BASE_URL (endpoint override), CDP_EXTRACT_MODEL (model override), CDP_EXTRACT_API_KEY (bearer token; never logged and REDACTED from every error), CDP_EXTRACT_TIMEOUT_MS, CDP_EXTRACT_MAX_CHARS, CDP_EXTRACT_PROMPT. PROMPT SHAPE: by default only the cleaned HTML is sent and the schema rides in response_format, which is what the HOSTED Schematron API expects (it injects the schema server-side); set prompt:'schematron' when baseUrl points at a LOCALLY SERVED open-weight Schematron (e.g. an MLX schematron8B), because that model was fine-tuned on a prompt with the schema INSIDE the user message and an HTML-only prompt returns garbage from it even under constrained decoding. In 'schematron' mode NO response_format is sent at all - the schema already lives in the prompt, and the client validates only that the response parses as JSON. THE SCHEMA-DESCRIPTION RULE: this is prompt-driven extraction, and the extraction INSTRUCTIONS live in the schema's property descriptions - write them like prompts ('the job title, exactly as printed', 'ISO date, not 'yesterday'). A schema whose properties carry no descriptions is REJECTED up front with an error saying so, because it would produce garbage extraction with no hint why. Source: 'dom' (live DOM, the rendered, JS-executed page) is the default; 'served' (the raw server response before rendering) currently returns a not-implemented error rather than guessing. Size guard: cleaned HTML over maxChars (default 300000) fails with html_too_large naming the actual size - shrink the payload with 'selector' to scope to a subtree, clean:'aggressive' to strip harder (standard already drops scripts/styles/hidden), or a larger maxChars if you truly need it all. Endpoint failures surface as upstream_error (with status and body, API key redacted) or timeout; the response never contains the key. A completion the endpoint cut off at its 8192-token output cap fails naming finish_reason=length and the fix (narrow with 'selector', or ask the schema for less) - it is NOT reported as invalid JSON, because a truncated tail is a size problem, not a model one. COST IS ALWAYS VISIBLE: every successful call reports the endpoint's usage token counts inline, so a slow/bloated extraction is visible in the answer itself, not discovered on a bill later.",
490
+ "params": {
491
+ "lease": "Lease token from claim_page. Omit for a tab this process holds; required for a tab held by another process or one claimed explicitly. Auto-acquired under CDP_REQUIRE_LEASE (then pass target, not lease). See server instructions.",
492
+ "target": "Page selector: 'active' (default), 'index:N', 'url:<substr>', 'title:<substr>', 'label:<name>', or a 32-hex '<targetId>'. Grammar: see server instructions.",
493
+ "schema": "REQUIRED JSON Schema object (type object) describing the JSON you want back. This is both the contract AND the prompt: each property's 'description' tells the extractor what to put there, so make them specific instructions. A schema with no property descriptions at all is rejected before any page work happens - empty descriptions extract garbage and the error says exactly what to fix. Nested objects and arrays of objects work; their inner descriptions count too.",
494
+ "selector": "CSS selector scoping extraction to that subtree's HTML only (pierces open shadow roots). The cheapest fix for html_too_large: extract the article or table, not the whole page. Omit to send the full cleaned document.",
495
+ "source": "'dom' (default) reads the live, rendered DOM - what the page shows after JS ran. 'served' would read the raw HTTP response before rendering; it currently returns a not-implemented error naming the arm, so callers can detect and fall back rather than guessing.",
496
+ "clean": "HTML cleaning level before the payload is measured and sent. 'standard' (default) strips scripts, styles, hidden elements and other non-content noise. 'aggressive' additionally collapses whitespace, drops nav/footer/boilerplate - use when html_too_large bites or the extractor chases junk. 'none' sends the raw HTML; almost never what you want.",
497
+ "maxChars": "Character cap on the cleaned HTML payload. Default 300000 (also settable via CDP_EXTRACT_MAX_CHARS). Exceeding it fails with html_too_large reporting actual vs cap - it does NOT silently truncate, because a half page extracted as though it were whole is a wrong answer that looks right.",
498
+ "timeoutMs": "Total budget in ms for the endpoint call. Default 90000, hard max 300000 (CDP_EXTRACT_TIMEOUT_MS overrides the default). Expiry fails with a timeout error, never a hang.",
499
+ "prompt": "Prompt shape sent upstream. 'html' (default, also CDP_EXTRACT_PROMPT=html) sends the cleaned HTML as the only user message and lets response_format carry the schema - correct for the hosted Schematron API, which injects the schema server-side. 'schematron' sends the open-weight Schematron model card's own messages: a 'You are a helpful assistant' system message plus a user message with the compact JSON-stringified schema inline, then the HTML, then 'MAKE SURE ITS VALID JSON.' - use it when baseUrl points at a locally served open-weight Schematron. It adds roughly the stringified schema's length to the prompt (maxChars caps the HTML only); 'schematron' mode sends NO response_format - the schema lives in the prompt and the client validates only that the response parses as JSON. The arg wins over CDP_EXTRACT_PROMPT, and any other value is rejected up front.",
500
+ "model": "Model name for the endpoint call. Default 'schematron' on the loopback llm-ferry; override per-call or via CDP_EXTRACT_MODEL.",
501
+ "baseUrl": "OpenAI-compatible endpoint base URL (the server appends /chat/completions). DEFAULT IS http://127.0.0.1:8090/v1 - loopback llm-ferry, page content never leaves the machine. Setting this (or CDP_EXTRACT_BASE_URL) to a remote host is an operator decision about where page content may travel; the response never echoes the URL with credentials.",
502
+ "savePath": "Write the extracted JSON here instead of returning it inline: the response then carries the file path and usage counts only, keeping large payloads out of the transcript. An absolute path (starting with /) is used as-is; a relative path resolves under the artifact dir (/tmp/cdp-toolkit); missing parents are created."
503
+ }
487
504
  }
488
505
  };
package/src/toolGroups.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export const TOOL_GROUPS = [
2
2
  "core", "input", "cookies", "network", "console", "mocking",
3
3
  "emulation", "performance", "recording", "leases", "permissions",
4
- "dialogs", "downloads",
4
+ "dialogs", "downloads", "extraction",
5
5
  ] as const;
6
6
 
7
7
  export type ToolGroup = (typeof TOOL_GROUPS)[number];
@@ -31,6 +31,7 @@ export const GROUP_TOOLS: Record<ToolGroup, readonly string[]> = {
31
31
  permissions: ["grant_permissions"],
32
32
  dialogs: ["handle_dialog"],
33
33
  downloads: ["wait_for_download"],
34
+ extraction: ["extract_page"],
34
35
  } as const;
35
36
 
36
37
  export const TOOL_GROUP: Record<string, ToolGroup> = {};