# Show HN: Turbo-Browser-MCP – Batch forms, DOM deltas and submit guards for MCP

> Source: <https://github.com/Tthanh2k6/turbo-browser-mcp>
> Published: 2026-11-15 04:09:11+00:00

`turbo-browser-mcp` is a Playwright-based Model Context Protocol (MCP) server built to cut LLM token waste and multi-turn latency in browser automation. It consolidates 25-33 fine-grained browser primitives into **10 batched tools** featuring delta DOM snapshots, single-turn React/Shadow DOM/iframe form prefill, direct disk file uploads (no Base64 in context), parallel tab sessions, network blocking presets, and a default human-in-the-loop Submit guard.

All figures below come from [`benchmarks/benchmark.mjs`](https://github.com/Tthanh2k6/turbo-browser-mcp/blob/main/benchmarks/benchmark.mjs) (`npm run benchmark`) running headless Chromium locally with exact OpenAI `o200k_base` tokenization (`tiktoken 0.14.0`):

| Capability | Typical Baseline | `turbo-browser-mcp` | Speed / Turn Gain | Token Savings ( `o200k_base` ) | 
|---|---|---|---|---|
| **Dense UI Snapshot** (140-btn SaaS console) | `18,654` tok HTML /`3,568` tok full ARIA | **`1,135` tok** compact (`28 ms` ) | `1` call | **`93.9%` vs HTML** (`68.2%` vs ARIA) | 
| **Post-Click Delta Snapshot** (`onlyChanged: true` ) | `3,582` tok full ARIA rescan | **`56` tok** changed lines (`18 ms` ) | `1` call | **`98.4%` vs ARIA** (`99.7%` vs HTML) | 
| **10-Field Complex Form + Snapshot** (`smart_fill_form` ) | `11` turns (`574 ms` ,`12,197` dir /`72,614` cum tok) | **`1` turn** (`82 ms` ,**`811` tok** ) | **`7x` faster** ,`11 -> 1` turns | **`93.4%` direct / `98.9%` cumulative** | 
| **48 KiB PDF Upload** (`49,152` bytes) | `44,728` tok inline Base64 tool arg | **`85` tok** disk path (`68 ms` ) | Zero Base64 generation | **`99.8%` saved** | 
| **6-Step Interactive UI Flow** (`batch_actions` ) | `6` turns (`411 ms` ,`955` dir /`3,162` cum tok) | **`1` turn** (`357 ms` ,**`406` tok** ) | **`1.15x` faster** ,`6 -> 1` turns | **`57.5%` direct / `87.2%` cumulative** | 
| **Open 6 Tabs Concurrently** (`open_many` ) | `6` turns sequential (`998 ms` ) | **`1` turn** batched (`420 ms` ) | **`2.38x` faster** ,`6x` fewer turns | `5` fewer prompt replays | 
| **Heavy Media Load** (`preset: "turbo"` ,`304 KiB` assets) | `252 ms` ,`9` media requests (`304 KiB` ) | **`165 ms`** ,` 0` media requests (`0 KiB` ) | **`1.53x` faster** | **`100%` media bandwidth cut** | 
| **Article Markdown Scraping** (`extract_clean_text` ) | `3,595` tok raw HTML (`10,844` chars) | **`156` tok** clean Markdown (`641` chars) | Clean reader output | **`95.7%` saved** | 
| **Diagnostics Triage** (`page_diagnostics` errors-only) | `3,460` tok unfiltered (`32` req /`26` console) | **`601` tok** default errors (`2` req /`6` console) | Zero noise scrolling | **`82.6%` saved** | 
| **Static MCP Tool Schema** (every turn overhead) | `3,917` tok Playwright /`6,737` tok DevTools | **`3,010` tok** stripped (`3,314` full w/ annotations) | `10` tools vs`25` /`33` | **`23.2%` vs Playwright / `55.3%` vs DevTools** | 

**Note on latency vs LLM inference turns:** Eliminating `10` round-trips on a 10-field form saves both `7x` in browser execution (`574 ms -> 82 ms`) and `10` separate LLM inference round-trips (`2-5s` per turn in agent loops) plus `~71,800` replayed history tokens.

Standard MCP servers charge an LLM round-trip for every snapshot, field fill, `<select>` choice, upload, and verification check. Each turn re-sends the growing conversation history back into the model.

```
sequenceDiagram
    autonumber
    participant Agent as LLM Agent
    participant Std as Standard MCP (Playwright / DevTools)
    participant Turbo as turbo-browser-mcp

    Note over Agent,Std: Standard MCP (12+ LLM turns, full ARIA tree + history replay each step)
    Agent->>Std: 1. snapshot() -> full ARIA (~3,568 tok)
    loop Repeats 10+ times across fields, selects, checkboxes, upload
        Agent->>Std: fill / select_option / click
        Std-->>Agent: full ARIA or confirmation re-added to history
    end
    Agent->>Std: 12. upload inline or click chooser

    Note over Agent,Turbo: turbo-browser-mcp (2 LLM turns total, delta & inline snapshot)
    Agent->>Turbo: 1. inspect_compact(mode: "forms") -> compact refs (1,135 tok)
    Agent->>Turbo: 2. smart_fill_form(fields: [...10 fields + disk PDF...], returnSnapshot: true)
    Turbo-->>Agent: Filled 10 fields + guarded before Submit + inline snapshot (811 tok)
```

- **4 connection modes (`--mode auto|cdp|persistent|launch`):**  - `auto` (default): probes`--cdp http://127.0.0.1:9222` first so agents can reuse an existing signed-in browser session, then falls back to launching Chromium.
  - `cdp` : attach directly to a running Chromium/Chrome/Edge remote debugging port.
  - `persistent` : launch with a reusable`--user-data-dir` profile across runs.
  - `launch` : start a clean browser session (`--headless` by default, or`--headed` ).
- **Auto binary discovery (`src/playwright-loader.mjs`):** checks` playwright-core` cache revisions across Windows, macOS, and Linux, system installs of Google Chrome, Microsoft Edge, Brave, and Chromium, or an explicit`--executable <path>` .
- **Per-tab isolation (`tabs_batch`):** open tabs inside named` isolatedContext` sandboxes with separate cookies and storage, batch-open up to 10 concurrent URLs (`open_many` ), activate, reload, go`back` /`forward` ,`resize` viewport, and emulate`colorScheme` ,`media` ,`reducedMotion` ,`contrast` ,`forcedColors` , or custom`userAgent` .

- **Snapshot modes:**`interactive` ,`forms` ,`article` , and`all` .
- **Cross-boundary traversal:** flattens main frame DOM, child iframes (`includeFrames` ,`frameIdx` ), and open Shadow DOM. Injects an init script hooking`Element.prototype.attachShadow` (`WeakMap` +`WeakRef` /`FinalizationRegistry` +`window.__tbmGetShadowRoot` ) to keep post-init closed Shadow roots inspectable and guarded, traverses`<slot>`` assignedNodes({ flatten: true })` , and supports Form-Associated Custom Elements.
- **Token controls:**`onlyChanged: true` emits only elements added or changed since the prior snapshot;`query` filters by regex or substring;`boxes: true` adds`[box=x,y,w,h]` only when needed;`maxChars` caps payload size;`savePath` writes full output to disk.

Prefills an entire form in one MCP call while flushing microtasks and macrotasks between controlled inputs:

- **Standard & framework inputs:** text, email, password, number, date, textarea,`<input type="range">` , checkboxes, radio groups (`check` ,`not` ,`overwrite` ). Triggers React-controlled state updates via native property setters and synthetic events.
- **Selects & custom widgets:**`<select>` and`<select multiple>` (`option` /`values` ), ARIA comboboxes, ARIA switches,`contenteditable` rich editors, and Gmail compose dialogs (`gmail: { to, subject, body }` ).
- **Frame & wildcard targeting:** target specific iframes via`frameIdx` , or set`all: true` to populate matching controls across frames.
- **Direct disk uploads:** pass`file` or`files` paths directly to Playwright`setInputFiles` without loading binary files or Base64 strings into LLM context.
- **Atomic verification:** pass`returnSnapshot: true` to inspect the updated form state in the same tool response.

- **`act_fast` actions:**`click` ,`dblclick` ,`hover` ,`focus` ,`clear` ,`fill` ,`type` (`slowly` ,`delayMs` ,`submit` ),`select` ,`check` ,`uncheck` ,`drag` ,`drop` (`file` ,`files` , or custom MIME`dropData` ),`press` (`key` +`modifiers` :`Alt` ,`Control` ,`ControlOrMeta` ,`Meta` ,`Shift` ),`scroll` (`dx` ,`dy` ),`wait` (`ms` ,`state` ,`loadState` ,`text` ,`textGone` ,`url` ), and`upload` .
- **Targeting options:** compact`[eN]`` ref` , CSS`selector` , visible`text` ,`label` , or viewport`(x, y)` coordinates, with`button` (`left` ,`right` ,`middle` ) and`frameIdx` .
- **`batch_actions`:** runs an ordered list of DOM actions and nested` smart_fill_form` steps in one MCP call, stopping safely if blocked by the Submit guard or optionally continuing on step errors (`continueOnError` ).

- **`extract_clean_text`:** extracts readable plain text or compact Markdown (` format: "text" | "markdown"` ) from main/article containers and iframes, stripped of script and style noise.
- **`evaluate_js`:** runs JS expressions or`(el, arg)` element functions in any frame (`frameIdx` ) with JSON-serialized results and optional`savePath` export.
- **`capture_page`:** full-page or element-scoped screenshots (` png` or`jpeg` with`quality` ,`css` or`device` scale, optional`returnImage` ) and PDF generation saved to a confined`savePath` .

- **`network_session` presets:**`turbo` ,`no-images` ,`no-media` , and`clean` (unblock all), alongside custom`blockResources` and`blockUrls` patterns.
- **Session & storage management:** read/write/clear browser`cookies` ,`localStorage` /`sessionStorage` , and export or reload full Playwright`storageState` JSON files (`saveStorageStatePath` ,`loadStorageStatePath` ).
- **`page_diagnostics`:** defaults to errors-only (` level: "errors"` ,`statusFilter: "errors"` ) to surface failures cleanly without flooding context, while supporting`all` ,`includeStatic` ,`urlFilter` , deep request/response inspection (`reqId` +`part` ), completed downloads, and dialog auto-handling (`dialogPolicy` :`accept` /`dismiss` ).

Run the benchmark suite yourself with Chromium and Playwright installed:

```
npm run benchmark
```

**Environment & methodology:** Headless Chromium via `playwright-core`, counting exact OpenAI `o200k_base` tokens via Python `tiktoken` (`approx-bpe` fallback if `tiktoken` is unavailable). Cumulative workflow tokens model standard chat-history replay across 

| Scenario | Raw HTML | Full Playwright ARIA | `turbo-browser-mcp` Output | Saved vs ARIA | Saved vs HTML | 
|---|---|---|---|---|---|
| **140-Button SaaS Console** (`inspect_compact` ) | `61,010` chars (`18,654` tok) | `12,771` chars (`3,568` tok) | **`3,968` chars (`1,135` tok)** | **`68.2%`** | **` 93.9%`** | 
| **Post-Click Delta** (`onlyChanged: true` ) | `61,010` chars (`18,654` tok) | `12,825` chars (`3,582` tok) | **`168` chars (`56` tok)** | **`98.4%`** | **` 99.7%`** | 
| **Article Markdown Extraction** (`extract_clean_text` ) | `10,844` chars (`3,595` tok) | n/a | **`641` chars (`156` tok)** | n/a | **`95.7%`** | 
| **Diagnostics Filter** (`32` reqs,`26` console logs) | `9,651` chars (`3,460` tok,`all` ) | n/a | **`1,761` chars (`601` tok, `errors`)** | n/a | **` 82.6%`** | 

| Scenario | Step-by-Step Baseline | `turbo-browser-mcp` | Execution Speedup | Token Savings | 
|---|---|---|---|---|
| **10-Field Complex Form + Verify** (`smart_fill_form` ) | `11` turns,`574 ms` ,`12,197` direct /`72,614` cum tok | **`1` turn** ,**`82 ms`** ,**` 811` tok** (`filled=10` ) | **`7x` faster** | **`93.4%` dir / `98.9%` cum** | 
| **48 KiB PDF Upload** (`49,152` bytes via`file` ) | `65,679` chars (`44,728` tok) Base64 payload | **`263` chars (`85` tok)** disk path (`68 ms` ) | Zero LLM Base64 encode | **`99.8%`** | 
| **6-Step UI Flow** (`batch_actions` ) | `6` turns,`411 ms` ,`955` dir /`3,162` cum tok | **`1` turn** ,**`357 ms`** ,**` 406` tok** | **`1.15x` faster** | **`57.5%` dir / `87.2%` cum** | 
| **Open 6 Tabs** (`tabs_batch`` open_many` ,`60 ms` TTFB) | `6` turns sequential (`998 ms` ) | **`1` turn** batched (`420 ms` ) | **`2.38x` faster** (`6 -> 1` turns) | Eliminates `5` turns | 
| **Heavy Media Load** (`8x32 KiB` imgs +`48 KiB` font) | `252 ms` ,`9` media hits (`304 KiB` ) | **`165 ms`** ,` 0` media hits (`0 KiB` ) | **`1.53x` faster** | **`100%` media saved** | 

Measured against installed MCP JSON schemas on disk (compact parameter schemas without `annotations`):

| MCP Server | Tool Count | Compact Schema Chars | Exact `o200k_base` Tokens | Savings with `turbo-browser-mcp` | 
|---|---|---|---|---|
| `turbo-browser-mcp` | **`10`** (` 14,186` full /`12,796` compact) | **`12,796`** | **` 3,010`** (` 3,314` full w/ annotations) | Baseline ( `10` tools) | 
| `@playwright/mcp` | `25` | `18,457` | `3,917` | **`23.2%` fewer static tokens** | 
| `chrome-devtools-mcp` | `33` | `30,252` | `6,737` | **`55.3%` fewer static tokens** | 

| Feature | `turbo-browser-mcp` | `@playwright/mcp` | `chrome-devtools-mcp` | 
|---|---|---|---|
| **Tool Surface Size** | **10 batched tools** | 25 granular tools | 33 granular tools | 
| **Delta Snapshots (`onlyChanged`)** | Built-in | Full tree every snapshot | Full tree every snapshot | 
| **Closed Shadow DOM Hook** | Post-init `attachShadow` hook +`<slot>` flattening | Open Shadow DOM | CDP / standard DOM access | 
| **Single-Call Smart Form + Contenteditable** | Yes (React, Shadow DOM, iframe, Gmail, ranges) | Multi-turn fill + separate select calls | Basic `fill_form` | 
| **Disk File Upload (Zero Base64)** | Native `file` /`files` path in`smart_fill_form` &`act_fast` | Dedicated `browser_file_upload` turn | Dedicated `upload_file` turn | 
| **Default Human-in-the-Loop Submit Guard** | Yes ( `allowSubmit=false` , form/iframe/Enter hooks) | No | No | 
| **Parallel `open_many` + `isolatedContext`** | Single batched call | Separate sequential tab calls | Separate page calls | 
| **One-Call `turbo` Media Blocker** | Built-in ( `turbo` ,`no-images` ,`no-media` ) | Manual JS / routing | Network emulation | 
| **Performance / Heap Profiling** | Not included | Not included | **Trace, Lighthouse, Heap Snapshot** | 

**When to keep Chrome DevTools MCP alongside `turbo-browser-mcp`:** `chrome-devtools-mcp` provides Lighthouse audits, performance trace analysis, CSS computed style panels, and V8 heap snapshots. `turbo-browser-mcp` focuses on fast multi-step UI automation, forms, and scraping with minimal token overhead.

| Capability | `turbo-browser-mcp` | `@playwright/mcp` | `chrome-devtools-mcp` | 
|---|---|---|---|
| Multi-tab open, list, switch, resize, emulate | `tabs_batch` | `browser_tabs` ,`browser_navigate` ,`browser_resize` , ... | `new_page` ,`list_pages` ,`select_page` ,`resize_page` , ... | 
| DOM / accessibility inspection | `inspect_compact` | `browser_snapshot` | `take_snapshot` | 
| Multi-field form & upload | `smart_fill_form` | `browser_fill_form` +`browser_select_option` +`browser_file_upload` | `fill_form` +`upload_file` | 
| Click, type, drag, press, scroll, wait | `act_fast` | `browser_click` ,`browser_type` ,`browser_drag` , ... | `click` ,`type_text` ,`drag` ,`press_key` ,`wait_for` | 
| Multi-step sequential macro | `batch_actions` | `browser_run_code_unsafe` or N tool calls | N separate tool calls | 
| Clean article extraction | `extract_clean_text` | `browser_evaluate` custom JS | `evaluate_script` custom JS | 
| Frame/element JS evaluation | `evaluate_js` | `browser_evaluate` | `evaluate_script` | 
| Errors, network & console triage | `page_diagnostics` | `browser_console_messages` ,`browser_network_requests` | `list_console_messages` ,`list_network_requests` , ... | 
| Screenshots & PDF export | `capture_page` | `browser_take_screenshot` | `take_screenshot` | 
| Blocking presets, cookies & storage | `network_session` | `browser_evaluate` + context config | Partial via `evaluate_script` /`emulate` | 

1. **Human-in-the-loop Submit Guard (`allowSubmit=false` by default):**  - `smart_fill_form` (`stopBeforeSubmit: true` ),`act_fast` , and`batch_actions` inspect matching elements, W3C AccName labels (`isSafeSubmitter` ), implicit HTML form Enter submissions, key chords (`Shift+Enter` ,`Enter+Shift` ),`HTMLFormElement.prototype.submit` /`requestSubmit` , and same-origin iframe`contentWindow` /`contentDocument` proxies.
  - Final actions resembling `Submit` ,`Publish` ,`Send` ,`Post` ,`Apply` ,`Confirm` , or payment triggers are blocked unless`allowSubmit: true` is explicitly passed or`--allow-submit` is enabled at startup.
  - Trade-off: pre-existing closed Shadow roots on already-open CDP tabs (attached before the init script ran) or strict cross-origin sandboxed frames fall back to Playwright frame handles, coordinate actions, and keyboard input.
2. **Workspace Path Confinement (`TURBO_BROWSER_SAVE_ROOT`):**  - File outputs (` savePath` ,`saveStorageStatePath` ) and state inputs (`loadStorageStatePath` ) are strictly confined to the current working directory or`TURBO_BROWSER_SAVE_ROOT` .
  - Lexical checks, `lstatSync` /`readlinkSync` symlink verification, and`realpathSync` validation block`..` traversal, UNC network paths, dangling symlinks, cyclic symlinks, and Windows junctions before reading or writing.
3. File outputs (

| Tool | Purpose | Key Parameters | 
|---|---|---|
| `tabs_batch` | Ordered tab & browser lifecycle | `operations[]` (`list` ,`open` ,`open_many` ,`navigate` ,`activate` ,`resize` ,`emulate` ,`reload` ,`back` ,`forward` ,`close` ,`close_browser` ),`isolatedContext` ,`concurrency` (1-10) | 
| `inspect_compact` | Token-capped element/form/article snapshot | `mode` (`interactive` ,`forms` ,`article` ,`all` ),`onlyChanged` ,`query` ,`boxes` ,`maxChars` ,`includeFrames` ,`savePath` | 
| `smart_fill_form` | Batch form, editor & disk file prefill | `fields[]` (`ref` ,`selector` ,`value` ,`option` ,`values` ,`check` ,`not` ,`keys` ,`file` ,`files` ,`frameIdx` ,`all` ),`gmail` ,`overwrite` ,`stopBeforeSubmit` ,`returnSnapshot` | 
| `act_fast` | Single targeted page action | `action` (`16` actions),`ref` /`selector` /`text` /`label` /`(x, y)` ,`modifiers` ,`dropData` ,`allowSubmit` ,`returnSnapshot` | 
| `batch_actions` | Ordered chain of actions + form steps | `steps[]` ,`continueOnError` ,`allowSubmit` ,`returnSnapshot` ,`snapshotMaxChars` | 
| `extract_clean_text` | Clean text or compact Markdown reader | `format` (`text` ,`markdown` ),`selector` ,`includeFrames` ,`maxChars` ,`savePath` | 
| `evaluate_js` | Scoped JS expression or `(el, arg)` call | `expression` ,`arg` ,`frameIdx` ,`ref` /`selector` /`text` ,`savePath` | 
| `page_diagnostics` | Console, network, downloads & dialogs | `level` (`errors` ,`all` ),`statusFilter` (`errors` ,`all` ),`includeStatic` ,`urlFilter` ,`reqId` ,`part` ,`dialogPolicy` ,`clear` ,`savePath` | 
| `capture_page` | PNG/JPEG screenshots & PDF export | `mode` (`screenshot` ,`pdf` ),`type` ,`quality` ,`fullPage` ,`scale` (`css` ,`device` ),`ref` /`selector` /`text` ,`returnImage` ,`savePath` | 
| `network_session` | Resource blockers, cookies & storage | `preset` (`turbo` ,`no-images` ,`no-media` ,`clean` ),`blockResources` ,`blockUrls` ,`cookieAction` ,`storageAction` ,`loadStorageStatePath` ,`saveStorageStatePath` | 

```
{
  "name": "smart_fill_form",
  "arguments": {
    "fields": [
      { "selector": "#full_name", "value": "Alex Nguyen" },
      { "selector": "#email", "value": "alex@example.com" },
      { "selector": "#country", "option": "United States" },
      { "selector": "#skills", "values": ["TypeScript", "Playwright"] },
      { "selector": "#agree_terms", "check": true },
      { "selector": "#resume_pdf", "file": "./assets/resume.pdf" }
    ],
    "stopBeforeSubmit": true,
    "returnSnapshot": true
  }
}
{
  "name": "inspect_compact",
  "arguments": {
    "mode": "interactive",
    "onlyChanged": true,
    "maxChars": 3000
  }
}
{
  "name": "tabs_batch",
  "arguments": {
    "operations": [
      { "action": "open", "url": "https://example.com/app", "isolatedContext": "acct-a" },
      { "action": "open_many", "urls": ["https://example.com/docs", "https://example.com/pricing"], "concurrency": 4 }
    ]
  }
}
git clone https://github.com/Tthanh2k6/turbo-browser-mcp.git
cd turbo-browser-mcp
npm install
npm run doctor
```

If you need Playwright's bundled Chromium (or have no system Chrome/Edge/Brave installed):

```
npx playwright-core install chromium
```

Using local clone:

```
{
  "mcpServers": {
    "turbo-browser": {
      "command": "node",
      "args": ["/absolute/path/to/turbo-browser-mcp/src/index.mjs"],
      "env": {
        "TURBO_BROWSER_SAVE_ROOT": "/absolute/path/to/allowed/workspace"
      }
    }
  }
}
```

Using `npx` / binary entrypoint:

```
{
  "mcpServers": {
    "turbo-browser": {
      "command": "npx",
      "args": ["-y", "turbo-browser-mcp"]
    }
  }
}
```

| CLI Flag | Environment Variable | Default | Description | 
|---|---|---|---|
| `--mode <auto\|cdp\|persistent\|launch>` | `TURBO_BROWSER_MODE` | `auto` | Connection strategy ( `cdp` first then launch in`auto` ) | 
| `--cdp <url>` | `TURBO_BROWSER_CDP_URL` | `http://127.0.0.1:9222` | Chrome DevTools Protocol HTTP endpoint | 
| `--user-data-dir <path>` | `TURBO_BROWSER_USER_DATA_DIR` | `""` | Persistent profile directory path | 
| `--executable <path>` | - | auto-detected | Custom Chromium, Chrome, Edge, or Brave executable | 
| `--timeout <ms>` | `TURBO_BROWSER_TIMEOUT_MS` | `15000` | Default Playwright action/navigation timeout in ms | 
| `--headless` /`--headed` | `TURBO_BROWSER_HEADLESS` | `true` (headless) | Run headless or visible browser window ( `"false"` for headed) | 
| `--allow-submit` | - | `false` | Disable default Submit/Publish confirmation guard | 
| `--doctor` | - | - | Print Node, Playwright, Chromium, and CDP diagnostics as JSON | 
| `--version` /`-v` | - | - | Print server version ( `0.4.0` ) | 
| `--help` /`-h` | - | - | Print CLI usage summary | 
| - | `TURBO_BROWSER_SAVE_ROOT` | `process.cwd()` | Root directory confining `savePath` and`storageState` files | 
| - | `PLAYWRIGHT_CORE_PATH` | auto-resolved | Explicit path to `playwright-core/index.mjs` | 

**Tip - Reusing your logged-in Chrome tabs:** Enable remote debugging on port `9222` (via `chrome://inspect/#remote-debugging` or launching Chrome with `--remote-debugging-port=9222`). In default `--mode auto`, `turbo-browser-mcp` attaches to your open tabs directly instead of launching a fresh unauthenticated browser.

```
npm test            # 405 passing tests across 85 suites (25 test files under test/*.test.mjs)
npm run doctor      # Environment check for Node, Playwright, Chromium, and CDP
npm run benchmark   # Local Chromium speed & exact o200k_base token benchmark
```

Tested automatically on GitHub Actions (`ubuntu-latest`) across **Node 20** and **Node 22**.
