@automatebrowser/mcp 0.3.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/README.md ADDED
@@ -0,0 +1,2092 @@
1
+ <h3 align="center">AutomateBrowser</h3>
2
+
3
+ <p align="center">
4
+ Automate your <strong>real</strong> browser with AI — debugger-free, logged-in, and shareable across IDEs.
5
+ <br />
6
+ <a href="https://farazpawle.github.io/automatebrowser/"><strong>Website</strong></a>
7
+ </p>
8
+
9
+ ---
10
+
11
+ ## About
12
+
13
+ **AutomateBrowser** is an [MCP](https://modelcontextprotocol.io) server paired with a Chrome/Edge
14
+ browser extension. Instead of launching a fresh, sterile browser instance like most automation
15
+ tools, it drives **your actual browser tab** — with your logins, cookies, sessions, and fingerprint
16
+ intact — from AI clients such as Claude Code, Claude Desktop, Cursor, VS Code, and Windsurf.
17
+
18
+ - **MCP server** (`@automatebrowser/mcp`) — a stdio server that exposes 46 browser tools to your AI client.
19
+ - **Browser extension** (`Chrome-extension/`, v1.4.0) — a Manifest V3 extension that connects to the
20
+ server over a local WebSocket and runs the automation inside your tab.
21
+ - **Terminal CLI** (`automate-browser`) — the same tools from a shell, sharing the browser with
22
+ whatever IDEs are connected. See [Terminal CLI](#terminal-cli).
23
+
24
+ The two halves talk over a loopback WebSocket on `127.0.0.1:9009`. Nothing about your browsing
25
+ leaves your machine. The server makes exactly two outbound calls, both optional: a once-a-day
26
+ check for a newer release, which sends nothing about you and can be switched off
27
+ (→ [here](#finding-out-your-copy-is-out-of-date)), and the field-data tool, which is inert
28
+ unless you supply an API key.
29
+
30
+ ## Why AutomateBrowser
31
+
32
+ - ⚡ **Fast** — automation runs locally; no remote round-trips.
33
+ - 🔒 **Private** — your browsing stays on your device.
34
+ - 👤 **Logged in** — uses your real profile, so you stay signed into every service.
35
+ - 🥷 **Stealthy** — your genuine browser fingerprint sidesteps most bot-detection and CAPTCHAs.
36
+ - 🧩 **Debugger-free by default** — interactions go through `chrome.scripting`, so there is **no
37
+ "started debugging this browser" banner**. CDP is per-tab and used only where nothing else can do
38
+ the job: when you ask for it, for a full-page screenshot, and for a screenshot of a background tab,
39
+ where it is the only way to get the right pixels without stealing your focus. It detaches again
40
+ straight after. → [What can take your focus](#what-can-take-your-focus)
41
+ - 🪶 **Token-efficient** — interactions return short confirmations; full accessibility snapshots are
42
+ fetched only when an agent asks for fresh element refs.
43
+ - 🤝 **Multi-IDE sharing & per-tab control** — a single shared relay lets every IDE see and drive the
44
+ same browsers. Ownership is **per tab**, so two agents (e.g. two IDEs) can drive two *different* tabs
45
+ of the same browser at the same time; only same-tab access is serialized, via a soft-claim with a
46
+ named handoff.
47
+
48
+ ## What it can do
49
+
50
+ A tour of the whole surface. Each line links to the detail further down; the full per-tool tables are
51
+ under [Tools](#tools).
52
+
53
+ **Drive the page**
54
+ - Navigate, reload (including a **hard reload that bypasses the cache**), back, forward, and wait for
55
+ a load or for a condition to become true. A navigation reports itself finished only when **the
56
+ navigation you asked for** finished — never because the page you were leaving had already loaded.
57
+ → [What `settled` means on a navigation](#what-settled-means-on-a-navigation)
58
+ - A navigation that **did not happen says so**, in the snapshot reply too, instead of handing you the
59
+ previous page under a success message.
60
+ → [A navigation that did not happen says so](#a-navigation-that-did-not-happen-says-so)
61
+ - Click, type, hover, drag, press keys, select options, scroll, and **fill a whole form in one call**.
62
+ Checkboxes and radios take a **boolean and nothing else** — a value that is neither is refused rather
63
+ than guessed at. → [Checkboxes and radios take a boolean](#checkboxes-and-radios-take-a-boolean-and-nothing-else)
64
+ - A form that **spans an embedded widget and the page around it** fills in one call, in the order you
65
+ wrote it. → [Filling a form that spans frames](#filling-a-form-that-spans-frames)
66
+ - Click by **element** or by **coordinate**, for canvases, maps and PDF viewers that a snapshot cannot
67
+ name. → [Clicking something a snapshot cannot name](#clicking-something-a-snapshot-cannot-name)
68
+ - Interactions report the same result **whether or not the debugger is attached** — same fields, same
69
+ waiting. → [Advanced mode does not change what interactions return](#advanced-mode-does-not-change-what-interactions-return)
70
+ - Upload files, and answer `alert`/`confirm`/`prompt` dialogs — including the native "Leave site?"
71
+ prompt that page JavaScript cannot even see.
72
+ - Every interaction **waits for the element to be genuinely ready** — visible, enabled, still, and not
73
+ covered by an overlay — then waits for the page to settle before returning.
74
+ → [Actionability](#actionability-interactions-wait-for-the-element-to-be-ready)
75
+ - Choose per call what **"finished" means** — return at once, wait for load, or wait for the network
76
+ to go quiet — and cap how long that wait may take.
77
+ → [Deciding when an action is finished](#deciding-when-an-action-is-finished)
78
+
79
+ **Read the page**
80
+ - Accessibility snapshots that stay **lean by default**, with a full mode and a write-to-file mode for
81
+ pages that would otherwise fill the context window. → [Snapshots](#snapshots-lean-full-or-to-a-file)
82
+ - Clean text or Markdown extraction, raw HTML, and a `find` that returns just the elements you asked
83
+ for instead of a whole tree.
84
+ - **Element refs survive a re-render**, and recover themselves once if the page swapped the element
85
+ out underneath you. → [Element refs survive a re-render](#element-refs-survive-a-re-render)
86
+ - Reads reach **inside iframes, including cross-origin ones**, with empty frames dropped and a cap so
87
+ an ad-heavy page stays readable. → [Frames](#frames-including-cross-origin-ones)
88
+ - Ask a page for the **actions it declares about itself**, and call one directly instead of hunting
89
+ for the controls. Almost no site declares any yet, so the honest answer is usually "none".
90
+ → [Tools the page declares about itself](#tools-the-page-declares-about-itself)
91
+
92
+ **See what went wrong**
93
+ - Console logs, **uncaught errors with their stacks**, and service-worker lifecycle in one place.
94
+ - A **browser issues feed** for the problems that produce no console error at all: content blocked by
95
+ a security policy, deprecated APIs, browser interventions, and failed or 4xx/5xx requests. This is
96
+ usually the answer when something silently did nothing.
97
+ - Network request list, and the **response body** for any single request, addressable when the same
98
+ URL was fetched more than once.
99
+ - Both logs **survive the browser shutting the extension down** to save memory, and carry over the
100
+ previous pages so a redirect can still be debugged. → [Log durability](#log-durability)
101
+ - Both lists are **paged, newest first**, so a chatty page cannot spend your whole context in one
102
+ call, and a footer names the exact call for the page before it.
103
+ → [Paged logs](#the-console-and-network-lists-are-paged-newest-first)
104
+ - A short **footer** on each action telling you how many new console errors and browser issues that
105
+ action caused. → [Console-error and issue footers](#console-error-and-issue-footers)
106
+ - An **accessibility audit** of the page you are on, grouped worst-first and mapped back to the refs
107
+ you can act on. It catches about a third of real barriers, and says so every time.
108
+ → [Accessibility: a floor, not a pass](#accessibility-a-floor-not-a-pass)
109
+ - Failures carry a **typed code and the tool that fixes them**.
110
+ → [Errors name the next step](#errors-name-the-next-step-not-just-the-problem)
111
+ - A result says **which of five things happened** — refused, done, partly done, failed, or *nobody
112
+ knows* — instead of one yes/no flag covering all of them. A form that filled two of three fields
113
+ says so, and an action whose reply was lost says it **may already have happened** rather than
114
+ inviting you to do it twice.
115
+ → [Five outcomes, not two](#five-outcomes-not-two)
116
+ - When the connection is down, `browser_status` says **whether it is coming back** — retrying, waiting
117
+ for a browser, or stopped for good — instead of one line covering all three.
118
+ → [`browser_status` says whether it is coming back](#browser_status-says-whether-it-is-coming-back)
119
+
120
+ **Measure speed**
121
+ - Record a performance trace and get **Core Web Vitals** — LCP, FCP, CLS, INP — rated against Google's
122
+ thresholds, plus the long tasks that blocked the main thread. One call profiles a whole page load.
123
+ - **Find out where the LCP time went**, not just how long it was: the server, discovering the image,
124
+ downloading it, or painting it — with the cause and the fix named in a line each, and the
125
+ render-blocking resources listed. → [Where the LCP time actually went](#where-the-lcp-time-actually-went)
126
+ - Re-analyse a saved trace later without recording again.
127
+ - **Watch the JS heap** over a window and get the trend, with no debugger and no banner.
128
+ → [Memory: is the page leaking?](#memory-is-the-page-leaking)
129
+ - Ask what **real visitors** experience, from Google's public field data, rather than just this
130
+ machine. → [Performance](#performance-this-machine-and-what-real-visitors-get)
131
+
132
+ **Capture**
133
+ - Screenshots of the viewport, the **full scrollable page**, or **one element**, as PNG/JPEG/WebP, and
134
+ straight to a file so the image never costs you a context window.
135
+ → [Screenshots](#screenshots-that-dont-cost-a-context-window)
136
+ - A **strip of stills** on a timer, to show a person what happened instead of describing it. Stills,
137
+ not video. → [A strip of stills](#a-strip-of-stills-for-showing-a-person-what-happened)
138
+ - An inline screenshot is **held under 1536 px wide**, so a 2K or 4K display does not quietly cost
139
+ three times the context for the same picture. → [The size ceiling](#the-size-ceiling-on-inline-screenshots)
140
+
141
+ **Pretend to be somewhere or something else**
142
+ - Fake a **location**, a user agent, extra headers, or a colour scheme.
143
+ - Emulate a **phone viewport with touch**, a throttled network, or a slow CPU.
144
+ → [Emulation](#emulation-location-headers-and-the-responsive-testing-set)
145
+ - Run your own script **before the page's own code**, to stub a clock or mock a fetch.
146
+ → [Reload, and the two options that need the debugger](#reload-and-the-two-options-that-need-the-debugger)
147
+
148
+ **Manage state**
149
+ - Cookies, localStorage and sessionStorage, downloads (with their **path on disk**, and an option to
150
+ wait for a transfer to finish), and tabs.
151
+ - **Stored values come back hidden by default** — an agent reading cookies, storage or a captured
152
+ request's headers gets the names, not your session tokens, until it explicitly asks for them.
153
+ → [Cookie, storage and header values are hidden by default](#cookie-storage-and-header-values-are-hidden-by-default)
154
+
155
+ **Work alongside you, and alongside other agents**
156
+ - **The agent works in its own tab, never yours.** It opens one in the background on first use and
157
+ leaves the tab you are reading completely alone. → [The agent's own tab](#the-agents-own-tab)
158
+ - **That holds through a disconnect too.** If the browser drops mid-action, the automatic retry goes
159
+ back to the same browser and the same tab, or refuses — it never falls through to the tab you are
160
+ looking at. → [A retry never changes browser or tab](#a-retry-never-changes-browser-or-tab)
161
+ - **The connection repairs itself.** Lose the relay, or start up before it is ready, and the server
162
+ keeps dialling back with a growing delay instead of sitting disconnected until you act.
163
+ → [The link to the relay repairs itself](#the-link-to-the-relay-repairs-itself)
164
+ - **Nothing steals your focus** except the one tool whose job is to show you something.
165
+ → [What can take your focus](#what-can-take-your-focus)
166
+ - **A clean, logged-out session on demand**, in a private window, for checking what a first-time
167
+ visitor sees without logging out of anything.
168
+ → [A clean, logged-out session on demand](#a-clean-logged-out-session-on-demand)
169
+ - **Per-tab ownership**, so two editors can drive two tabs of the same browser at once, with a named
170
+ handoff when they want the same one.
171
+ → [Sharing a browser with another agent](#sharing-a-browser-with-another-agent)
172
+ - One shared relay across every IDE, and a **terminal CLI** with `--json` for scripting.
173
+ → [Terminal CLI](#terminal-cli)
174
+ - **Tells you when your copy is out of date** — one line on startup, at most one registry
175
+ lookup a day, and an env var that stops it entirely.
176
+ → [Finding out your copy is out of date](#finding-out-your-copy-is-out-of-date)
177
+
178
+ **Keep it safe**
179
+ - A **file-path sandbox** on every write, and a way to name your own folders when your editor does
180
+ not. → [File-path sandbox](#file-path-sandbox)
181
+ - **Certificate checking is never switched off.** The one switch that claimed to do it never worked
182
+ on any build and was removed; the section names the two routes that do.
183
+ → [Loading a site with a bad certificate](#loading-a-site-with-a-bad-certificate)
184
+ - Only **http(s) pages** can be navigated to — a `javascript:`, `file:` or `chrome://` URL is refused
185
+ by name. → [Where a navigation may go](#where-a-navigation-may-go)
186
+ - The local socket **only accepts the extension**, and hangs up on a peer that floods it or sends
187
+ nonsense. → [What can reach the local socket](#what-can-reach-the-local-socket)
188
+ - The optional shared secret is **never sent over the socket**; both ends sign a challenge with it
189
+ instead. → [Driving a browser on another machine](#driving-a-browser-on-another-machine)
190
+ - **The server leaves when your editor does**, so a closed window never strands a phantom agent in
191
+ the roster. → [The server leaves when your editor does](#the-server-leaves-when-your-editor-does)
192
+ - **Allow / deny lists of sites** the agent may touch, enforced at the network layer as well as the
193
+ tool layer, plus a global read-only mode.
194
+ → [Which sites the agent may touch](#which-sites-the-agent-may-touch)
195
+ - A switch that lets the agent **click and read a logged-in session but never run code you did not
196
+ write**. → [Forbidding JavaScript the agent wrote](#forbidding-javascript-the-agent-wrote)
197
+ - An **audit log** of every call with its target, with secrets recorded as a length and never a value.
198
+ → [What the agent did](#what-the-agent-did-the-action-audit-log)
199
+ - **Fewer tools on demand** — serve a `core` or `slim` profile so the schemas cost less of every
200
+ request. → [Configuration](#configuration)
201
+
202
+ **Reach further**
203
+ - Drive a browser **on another machine**, with a mandatory token off loopback.
204
+ → [Driving a browser on another machine](#driving-a-browser-on-another-machine)
205
+ - Set a **proxy** for the whole browser. → [Proxy control](#proxy-control-whole-browser)
206
+
207
+ ## Architecture
208
+
209
+ AutomateBrowser uses a **singleton relay** model. One lightweight relay process owns the WebSocket
210
+ port; each IDE's MCP server connects to it as a *controller*, and each browser extension connects as
211
+ a *browser*. The relay routes tool calls between them and enforces one-driver-**per-tab** leases — so two agents can
212
+ drive two different tabs of one browser at once.
213
+
214
+ ```
215
+ IDE 1 (Claude Code) IDE 2 (Cursor) IDE 3 (VS Code)
216
+ node dist/index.js node dist/index.js node dist/index.js
217
+ (controller) (controller) (controller)
218
+ \ | /
219
+ \ | /
220
+ +-----------------> 127.0.0.1:9009 <----------------+
221
+ Relay (dist/relay.js)
222
+ |
223
+ +----------------------+----------------------+
224
+ | | |
225
+ Chrome ext Edge ext Brave ext
226
+ (your tab) (your tab) (your tab)
227
+ ```
228
+
229
+ - **Relay** (`dist/relay.js`) — a single long-lived WebSocket host bound to the lowest free port in
230
+ `9009–9013`. Spawned on demand, idle-exits ~5 min after the last peer leaves. It speaks the browser
231
+ wire protocol byte-for-byte (the extension can't tell it apart from a direct host) and holds the
232
+ live browser registry.
233
+ - **Controller** (`dist/index.js`) — the MCP server an IDE runs. It does **not** bind a port; it dials
234
+ the relay, registers with a human name, and forwards resolved tool calls. Any number of controllers
235
+ can run at once and share the same browsers.
236
+ - **Browser** (the extension) — connects on startup (no "Connect" button), races ports `9009–9013`,
237
+ caches the last-good port, and executes commands in your tab.
238
+
239
+ > **Soft claims (per tab):** the first action on a tab auto-claims *that tab* for 60 s (renewed on every
240
+ > action), so two agents can drive two different tabs of one browser concurrently. With no explicit
241
+ > `browser_select_tab`, a drive defaults to the browser's **focused tab** (not the whole browser), so two
242
+ > agents working on two different tabs coexist automatically; an agent keeps its own tab instead of
243
+ > following your focus onto a tab another agent drives. (A whole-browser lease is taken only as a last
244
+ > resort when no tab id is known, e.g. a `chrome://` active tab.) An agent targeting a tab (or browser)
245
+ > another agent holds is politely refused **by name** and can take over with `browser_force_claim`. Claims
246
+ > free on idle, on `browser_release_client`, or on disconnect.
247
+
248
+ Deeper detail lives in the skill files under [.claude/skills/](.claude/skills/) — `server-structure`,
249
+ `extension`, `messaging-protocol`, `connection-debugging`, `build-and-deps`, and `mcp-config`.
250
+
251
+ ## Repository layout
252
+
253
+ ```
254
+ AutomateBrowser/
255
+ ├── src/ # MCP server — source of truth
256
+ │ ├── index.ts # stdio MCP entry; composes the 46 tools
257
+ │ ├── cli.ts # `automate-browser` terminal entry (same tools, one shot)
258
+ │ ├── server.ts # MCP Server + ListTools/CallTool handlers
259
+ │ ├── context.ts # Relay-backed facade tools call into
260
+ │ ├── relay-link.ts # Controller <-> relay socket
261
+ │ ├── relay-ensure.ts # Discover/spawn the relay
262
+ │ ├── relay/ # The relay host (index, relay, control, browsers, ...)
263
+ │ ├── tools/ # The 46 MCP tools (snapshot, content, forms, tabs, ...)
264
+ │ └── vendor/ # Vendored former-monorepo deps (config, messaging, types)
265
+ ├── Chrome-extension/ # MV3 browser extension (WXT) — source of truth
266
+ │ ├── wxt.config.ts # Manifest config + permissions
267
+ │ ├── entrypoints/ # background service worker, content script, popup
268
+ │ ├── lib/ # connection, tab targeting, identity, automation engine
269
+ │ └── .output/chrome-mv3/ # BUILD OUTPUT — load THIS folder unpacked
270
+ ├── dist/ # Built bundle (index.js + relay.js + cli.js)
271
+ ├── scripts/connection-smoke.cjs # Connection smoke test
272
+ ├── server.json # Distribution manifests — MCP registry,
273
+ ├── gemini-extension.json # Gemini CLI, and the Agent Plugins pair.
274
+ ├── plugin.json # All version-gated against package.json
275
+ ├── mcp.json # by verify:release and by npm test
276
+ └── .claude/skills/ # Curated, in-depth project knowledge
277
+ ```
278
+
279
+ ## Quick start
280
+
281
+ ### 1. Build and load the browser extension
282
+
283
+ ```bash
284
+ cd Chrome-extension
285
+ npm install
286
+ npm run build # WXT build -> Chrome-extension/.output/chrome-mv3/
287
+ ```
288
+
289
+ Then in Chrome or Edge (v137+):
290
+
291
+ 1. Open `chrome://extensions` (or `edge://extensions`).
292
+ 2. Turn on **Developer mode** (top-right).
293
+ 3. Click **Load unpacked**.
294
+ 4. Select the build output folder:
295
+ `Chrome-extension/.output/chrome-mv3/`
296
+
297
+ A card titled **"AutomateBrowser"** appears. The extension keeps a fixed ID
298
+ (`bjfgambnhccakkhmkepdoekmckoijdlc`) and connects automatically — no button to press. Click the
299
+ toolbar icon (or **Alt+J**) to open the popup, where you can pin a specific tab, set an optional
300
+ label, see the live connection status, and see **every agent connected to the server** — each listed
301
+ by name and tagged with the tab it's driving (or *idle* when it holds no active tab). The list is the
302
+ full roster, so a connected agent stays visible even between actions (it doesn't vanish when its lease
303
+ lapses), and you can tell at a glance that Claude Code, Codex, and Copilot are all connected — and who
304
+ is working in which tab.
305
+
306
+ > Point **Load unpacked** at `.output/chrome-mv3/` — **not** at the `Chrome-extension/` source root.
307
+ > Chrome can only load the compiled output.
308
+
309
+ > **Updating from an earlier build re-prompts you.** This release adds two permissions —
310
+ > `downloads` (so `browser_downloads` can report a file's final path) and `declarativeNetRequest`
311
+ > (so a denied origin is blocked at the network layer, not only at the tool). Chrome disables an
312
+ > extension until new permissions are approved, so re-enable it once after updating. Both ship
313
+ > together on purpose: one re-approval, not two.
314
+
315
+ ### 2. Build the MCP server
316
+
317
+ ```bash
318
+ # from the repo root
319
+ npm install
320
+ npm run build # tsup -> dist/index.js (server) + dist/relay.js (relay) + dist/cli.js (terminal client)
321
+ ```
322
+
323
+ The server vendors its former monorepo dependencies under [src/vendor/](src/vendor/), so it builds
324
+ standalone — no external workspace required.
325
+
326
+ ### 3. Register the server in your AI client
327
+
328
+ Point your MCP client at the **local build** (`node dist/index.js`), using the absolute path to
329
+ your own checkout. Example (Claude Code / Claude Desktop / Cursor `mcpServers`, VS Code uses
330
+ `servers`); on Windows, escape the backslashes:
331
+
332
+ ```json
333
+ {
334
+ "mcpServers": {
335
+ "automatebrowser": {
336
+ "command": "node",
337
+ "args": ["C:\\path\\to\\AutomateBrowser\\dist\\index.js"],
338
+ "type": "stdio",
339
+ "env": {
340
+ "AUTOMATE_BROWSER_CLIENT_NAME": "Claude"
341
+ }
342
+ }
343
+ }
344
+ }
345
+ ```
346
+
347
+ Or via the Claude Code CLI:
348
+
349
+ ```bash
350
+ claude mcp add automatebrowser -- node "/absolute/path/to/AutomateBrowser/dist/index.js"
351
+ ```
352
+
353
+ Restart / reconnect the client after editing config. You don't configure a port — controllers
354
+ auto-discover the relay. See [.claude/skills/mcp-config/SKILL.md](.claude/skills/mcp-config/SKILL.md)
355
+ for per-client config locations (VS Code, Codex, etc.).
356
+
357
+ #### Installing it from a manifest instead
358
+
359
+ Four manifests in this repository describe the same server to four listing formats, so a client that
360
+ speaks one of them can install it without you writing any JSON:
361
+
362
+ | Manifest | Platform | How it is installed |
363
+ |---|---|---|
364
+ | `server.json` | The MCP registry | Published with the package; clients that browse the registry find it there |
365
+ | `gemini-extension.json` | Gemini CLI | `gemini extensions install https://github.com/farazpawle/automatebrowser` |
366
+ | `plugin.json` + `mcp.json` | Any client implementing [Agent Plugins 1.0.0](https://agent-plugins.org/) | Point the client at this repository; it reads both files plus `skills/` from the root |
367
+ | `.claude-plugin/plugin.json` + `marketplace.json` | Claude Code | `/plugin marketplace add` — see [Skills that ship with it](#skills-that-ship-with-it) |
368
+
369
+ **The honest limit: three of the four run `npx @automatebrowser/mcp@<version>`, and that package is not
370
+ on npm yet.** Until it is published they install nothing. The Claude plugin pair is the exception — it
371
+ installs the skill from a checkout, which is why it is the one the skill section documents. For now,
372
+ the local build above is the supported path, and it is the better one anyway when several editors share
373
+ one browser: every manifest here launches the published package, and the
374
+ [mcp-config skill](.claude/skills/mcp-config/SKILL.md) explains why a local `node dist/index.js` entry
375
+ is preferred while that is still true.
376
+
377
+ All five version claims across those files are gated: `npm run verify:release` and `npm test` both fail
378
+ if any one of them disagrees with `package.json`, so a listing cannot quietly advertise a version that
379
+ was never released.
380
+
381
+ ## Tools
382
+
383
+ All 46 tools are prefixed `browser_`. Interactions are **lean by default** — they return a short
384
+ confirmation, not a snapshot. Call `browser_snapshot` to get element `ref`s, then pass `{ element, ref }`
385
+ to the interaction tools (a raw CSS selector is not accepted).
386
+
387
+ The navigating and interacting tools also take an optional **`timeout`** (milliseconds) so an agent can
388
+ give up early instead of waiting out the server default; `0` or omitting it uses the default. Calls from
389
+ one client run **one at a time, in the order they were issued**, so a click can never overtake the
390
+ navigation that was meant to precede it. Different clients are not serialized against each other — that
391
+ is what the per-tab claim is for.
392
+
393
+ <!-- AUTO-GENERATED:tools START — do not edit by hand; run `npm run docs:generate` -->
394
+
395
+ **Tool profiles** — `full` (46) · `core` (19) · `slim` (7). Set `AUTOMATE_BROWSER_TOOLS` to a profile name or a comma-separated category list to serve fewer schemas; `browser_status`, `browser_list_clients` and `browser_select_client` are always served.
396
+
397
+ ### Navigation & history
398
+ | Tool | Description |
399
+ |------|-------------|
400
+ | `browser_navigate` | Navigate to a URL in YOUR OWN tab, opened in the background if you have none |
401
+ | `browser_go_back` | Go back to the previous page |
402
+ | `browser_go_forward` | Go forward to the next page |
403
+
404
+ ### Snapshot & interaction
405
+ | Tool | Description |
406
+ |------|-------------|
407
+ | `browser_snapshot` | Capture accessibility snapshot of the current page |
408
+ | `browser_click` | Click an element by ref, or a viewport point by x/y |
409
+ | `browser_hover` | Hover over element on page |
410
+ | `browser_type` | Type text into editable element |
411
+ | `browser_select_option` | Select an option in a dropdown |
412
+ | `browser_drag` | Perform drag and drop between two elements |
413
+
414
+ ### Input & timing
415
+ | Tool | Description |
416
+ |------|-------------|
417
+ | `browser_press_key` | Press a key or modifier combo (e.g. Enter, Tab, "Control+A", "Shift+Tab") on the focused element |
418
+ | `browser_wait` | Wait for a specified time in seconds |
419
+ | `browser_wait_for` | Wait for a page condition to become true (element appears/disappears, text appears, URL changes) |
420
+
421
+ ### Reading content
422
+ | Tool | Description |
423
+ |------|-------------|
424
+ | `browser_read_page` | Read the page's main content as clean text or Markdown (strips nav/scripts/styles) |
425
+ | `browser_get_html` | Get the raw outerHTML of the page (or of a specific element by `ref`) |
426
+ | `browser_find` | Find elements by text, role, and/or CSS selector and return fresh refs WITHOUT a full snapshot |
427
+
428
+ ### Page-declared tools
429
+ Actions the PAGE publishes about itself, which an agent can call directly instead of finding and clicking controls for. Forward-looking: the standard is a draft and almost no live site declares anything yet, so `list` normally comes back empty with the reason.
430
+
431
+ | Tool | Description |
432
+ |------|-------------|
433
+ | `browser_page_tools` | List and call actions a page declares about itself (WebMCP) |
434
+
435
+ ### Forms & scrolling
436
+ | Tool | Description |
437
+ |------|-------------|
438
+ | `browser_fill_form` | Fill multiple form fields (inputs, textareas, selects, checkboxes, radios, contenteditable) in ONE call |
439
+ | `browser_clear` | Clear the value of an input, textarea, or contenteditable element by `ref` |
440
+ | `browser_scroll` | Scroll the page or an element |
441
+
442
+ ### State: cookies, storage, network, downloads, dialogs
443
+ | Tool | Description |
444
+ |------|-------------|
445
+ | `browser_get_cookies` | List cookies for the URL of the tab you are driving (optionally filter by `name`) |
446
+ | `browser_set_cookie` | Set (create/overwrite) a cookie on the URL of the tab you are driving |
447
+ | `browser_storage` | Read or write the page's localStorage/sessionStorage |
448
+ | `browser_network_requests` | List network requests the tab you are driving made on the CURRENT page (method, URL, status, type, timing) — pass includePreserved for the pages before it |
449
+ | `browser_handle_dialog` | Control JS dialogs (alert/confirm/prompt) |
450
+ | `browser_downloads` | Recent downloads: final path on disk, URL, mime, size, state |
451
+ | `browser_proxy` | Route the browser through a proxy |
452
+
453
+ ### Performance
454
+ `browser_perf_trace` measures THIS machine on THIS run and needs `browser_advanced_mode` — except `action: "memory"`, which samples the JS heap with no debugger and no banner. `browser_perf_field_data` needs no browser at all - it reads Google's Chrome UX Report for what real visitors experienced, and sends the URL you ask about to that public API.
455
+
456
+ | Tool | Description |
457
+ |------|-------------|
458
+ | `browser_perf_field_data` | Real-user Core Web Vitals (p75 LCP/INP/CLS/FCP/TTFB) for a URL, from Google's Chrome UX Report |
459
+
460
+ ### Capture & evaluation
461
+ | Tool | Description |
462
+ |------|-------------|
463
+ | `browser_screenshot` | Capture the visible viewport of the tab you are driving — including a background tab, which is rendered via the debugger (brief banner) rather than refused |
464
+ | `browser_get_console_logs` | Console logs, uncaught errors with stacks, and service-worker lifecycle (register/state/messages) |
465
+ | `browser_issues` | Problems the browser detected that produce NO console error: blocked content (CSP), deprecated API use, browser interventions, and failed or 4xx/5xx network requests |
466
+ | `browser_eval` | Evaluate JavaScript in the tab you are driving and return the result |
467
+
468
+ ### Tabs
469
+ | Tool | Description |
470
+ |------|-------------|
471
+ | `browser_list_tabs` | List the connected browser's open tabs |
472
+ | `browser_new_tab` | Open a new tab IN THE BACKGROUND and drive it — no focus stealing |
473
+ | `browser_switch_tab` | STEALS THE USER'S FOCUS: brings a tab to the front and drives it, by `tabId` (preferred) or `index` |
474
+ | `browser_select_tab` | TAKE OVER a tab the user already has open, WITHOUT focusing it — the tool for "pick up the testing I started" |
475
+ | `browser_close_tab` | Close a tab by `tabId` or `index` |
476
+
477
+ ### Multi-IDE / clients
478
+ | Tool | Description |
479
+ |------|-------------|
480
+ | `browser_list_clients` | List every browser connected to the shared AutomateBrowser relay (e.g. Chrome and Edge when both have the extension connected), across all IDEs |
481
+ | `browser_select_client` | Choose which connected browser your subsequent tools act on |
482
+ | `browser_force_claim` | Forcibly take over a browser that another agent is currently driving, and make it active for your tools |
483
+ | `browser_release_client` | Release your claim on the browser you are currently driving so another agent can take it |
484
+ | `browser_status` | Diagnostics for the AutomateBrowser relay |
485
+
486
+ ### Advanced (opt-in CDP)
487
+ Attach the Chrome debugger only when you need full-fidelity input or network bodies. Enable with
488
+ `browser_advanced_mode` first; a debugging banner shows only while it's attached.
489
+
490
+ | Tool | Description |
491
+ |------|-------------|
492
+ | `browser_advanced_mode` | Enable/disable opt-in debugger (CDP) mode for the tab you are driving |
493
+ | `browser_upload_file` | Set files on a file input (real upload) |
494
+ | `browser_get_network_request` | Get a network request's response BODY, status and headers by URL substring |
495
+ | `browser_perf_trace` | Record a performance trace (requires advanced/debugger mode) |
496
+ | `browser_emulate` | Emulate location, headers, colour scheme, viewport, user agent, network or CPU |
497
+
498
+ <!-- AUTO-GENERATED:tools END -->
499
+
500
+ ## Terminal CLI
501
+
502
+ `automate-browser` runs any one of the tools above from a shell. It joins the relay exactly like an
503
+ IDE does, so it drives the **same** browser your editors are driving and appears in their
504
+ `browser_status` peer list as `cli`.
505
+
506
+ ```bash
507
+ npm link # or, once published: npx -p @automatebrowser/mcp automate-browser
508
+ # (-p is required: the package ships two bins, so a
509
+ # bare `npx @automatebrowser/mcp` cannot pick one)
510
+
511
+ automate-browser status # who is connected, which relay, which browsers
512
+ automate-browser navigate https://example.com
513
+ automate-browser read-page --args '{"format":"markdown"}'
514
+ automate-browser eval 'document.title'
515
+ automate-browser screenshot # writes a PNG to the temp dir and prints the path
516
+ automate-browser tabs --json # machine-readable, for scripting
517
+ ```
518
+
519
+ Aliases cover the common cases (`navigate`, `read-page`, `snapshot`, `screenshot`, `status`, `eval`,
520
+ `tabs`); anything else is called by tool name, with arguments as JSON:
521
+
522
+ ```bash
523
+ automate-browser browser_click --args '{"element":"Sign in button","ref":"e7k2f"}'
524
+ ```
525
+
526
+ The tool name may be given with or without the `browser_` prefix. `--json` prints the whole result
527
+ (including `structuredContent`) instead of the human-readable text. Exit status is `0` on success and
528
+ `1` on any failure, so it composes in scripts. A failure prints the **same text an agent gets** — the
529
+ `CODE: message` line and the `Recover: call <tool>` line — on stderr; it used to print the bare
530
+ sentence, so the terminal was the one caller that never saw the code. Unlike the MCP server the CLI always offers every tool,
531
+ regardless of `AUTOMATE_BROWSER_TOOLS` — profiles exist to save an agent's tokens, and a terminal
532
+ pays none.
533
+
534
+ ## Configuration
535
+
536
+ All settings are environment variables on the **server** side (set them in the MCP client's `env`):
537
+
538
+ <!-- AUTO-GENERATED:config START — do not edit by hand; run `npm run docs:generate` -->
539
+
540
+ | Variable | Purpose | Default |
541
+ |----------|---------|---------|
542
+ | `AUTOMATE_BROWSER_CLIENT_NAME` | Human name for this IDE in the relay roster | `mcp-<pid>` (CLI: `cli`) |
543
+ | `AUTOMATE_BROWSER_TOKEN` | Optional auth token (set the same value in the extension popup) | unset |
544
+ | `AUTOMATE_BROWSER_TOOLS` | Tool profile (`full`, `core`, `slim`) or a comma-separated category list | `full` |
545
+ | `AUTOMATE_BROWSER_CONNECT_WAIT_MS` | How long a tool waits for a browser to appear | `30000` |
546
+ | `AUTOMATE_BROWSER_LEASE_TTL_MS` | Soft-claim lease duration | `60000` |
547
+ | `AUTOMATE_BROWSER_INSTANCE_ID` | Identity used to replace this agent's own previous roster entry when it restarts | derived from parent process + folder |
548
+ | `AUTOMATE_BROWSER_CONTROLLER_STALE_MS` | Drop an agent that has sent nothing for this long | `45000` |
549
+ | `AUTOMATE_BROWSER_WS_PORT_RANGE` | Port scan range (e.g. `9109-9116` to isolate) | `9009-9013` |
550
+ | `AUTOMATE_BROWSER_RELAY_IDLE_MS` | Idle time before the relay exits | `300000` |
551
+ | `AUTOMATE_BROWSER_RELAY_HOST` | Bind the relay past loopback so a browser on **another machine** can connect. Refuses to start without `AUTOMATE_BROWSER_TOKEN` | `127.0.0.1` |
552
+ | `AUTOMATE_BROWSER_RELAY_FOREGROUND` | `1` also tees the relay's log to stderr instead of the file only | unset |
553
+ | `AUTOMATE_BROWSER_SNAPSHOT_EACH_ACTION` | Bundle a snapshot after every interaction | unset |
554
+ | `AUTOMATE_BROWSER_DELTA_FOOTER` | `off` disables the console-error footer below | on |
555
+ | `AUTOMATE_BROWSER_DELTA_FOOTER_MS` | Hard ceiling on the footer's console probe | `2000` |
556
+ | `AUTOMATE_BROWSER_NAV_CONFIRM_MS` | How long a navigation that reported no movement is re-checked before it is called a failure | `2000` |
557
+ | `AUTOMATE_BROWSER_SCREENSHOT_MAX_WIDTH` | Widest an **inline** screenshot may come back. A `filePath` capture is never downscaled; `0` = off | `1536` |
558
+ | `AUTOMATE_BROWSER_SCREENSHOT_MAX_HEIGHT` | Tallest an **inline** screenshot may come back, aspect ratio preserved; `0` = off | `4096` |
559
+ | `AUTOMATE_BROWSER_ACTIONABILITY` | `off` disables the pre-action checks and post-action settle below | on |
560
+ | `AUTOMATE_BROWSER_WS_MAX_PAYLOAD_BYTES` | Max WebSocket frame size. A whole Chrome trace is never under 1 MiB, and an oversized frame closes the socket rather than truncating | `67108864` (64 MiB) |
561
+ | `AUTOMATE_BROWSER_WS_RATE_MAX` | Frames one socket may send per window before it is closed | `120` |
562
+ | `AUTOMATE_BROWSER_WS_RATE_WINDOW_MS` | The window that ceiling is counted over | `1000` |
563
+ | `AUTOMATE_BROWSER_EXTENSION_ORIGINS` | Comma-separated browser-extension origins allowed to open a socket | the AutomateBrowser extension |
564
+ | `AUTOMATE_BROWSER_LOG_LEVEL` | `debug`, `info`, `warn` or `error`. Diagnostics go to stderr, never stdout | `info` |
565
+ | `AUTOMATE_BROWSER_WORKSPACE` | Extra folders the file-path sandbox accepts, `;`-separated on Windows and `:`-separated elsewhere. Adds to the roots your client sends; never replaces them | unset (client roots, or the working directory when it sends none) |
566
+ | `AUTOMATE_BROWSER_ALLOW_UNRESTRICTED_PATHS` | `1` disables the file-path sandbox below | unset |
567
+ | `AUTOMATE_BROWSER_ALLOW_ORIGINS` | Comma-separated origin patterns; when set, **only** these may be driven | unset (unrestricted) |
568
+ | `AUTOMATE_BROWSER_DENY_ORIGINS` | Origins that may never be driven, also blocked at the network layer | unset |
569
+ | `AUTOMATE_BROWSER_SENSITIVE_ORIGINS` | Origins that stay readable but can never be acted on | unset |
570
+ | `AUTOMATE_BROWSER_READ_ONLY` | `1` refuses every page-changing tool, everywhere | unset |
571
+ | `AUTOMATE_BROWSER_NO_EVAL` | `1` refuses every tool that runs JavaScript you wrote — `browser_eval` and `browser_navigate`'s `initScript` | unset |
572
+ | `AUTOMATE_BROWSER_AUDIT` | `off` stops the action audit log below | on |
573
+ | `AUTOMATE_BROWSER_AUDIT_FILE` | Where the audit trail is written | `~/.automate-browser/automate-browser-audit.log` |
574
+ | `AUTOMATE_BROWSER_NO_UPDATE_CHECK` | Any value stops the daily check for a newer release below | unset (check runs) |
575
+ | `AUTOMATE_BROWSER_CRUX_KEY` | Google Chrome UX Report API key; `browser_perf_field_data` is inert without it | unset |
576
+
577
+ <!-- AUTO-GENERATED:config END -->
578
+
579
+ ### Console-error and issue footers
580
+
581
+ After a page-changing tool (click, type, navigate, …) the server counts what went wrong since the
582
+ last check and, when there is anything, appends a line for each kind:
583
+
584
+ ```
585
+ ⚠ 2 new console errors since this action — call browser_get_console_logs for details.
586
+ ⚠ 1 new browser issue since this action (blocked content or failed requests) — call browser_issues for details.
587
+ ```
588
+
589
+ The two are deliberately **separate lines**: a blocked script or a failed request produces no console
590
+ error at all, so counting them as one would point you at the tool that cannot show them.
591
+
592
+ Read-only tools never get it, and the count starts from the moment the server launched, so errors
593
+ the page logged before the session are not blamed on the first action. The probe costs one extra
594
+ round trip (measured at ~2 ms against a real browser) and is capped by
595
+ `AUTOMATE_BROWSER_DELTA_FOOTER_MS`; if it fails for any reason it is silently dropped rather than
596
+ failing the action. A navigation replaces the page's log buffer, so errors from the previous document
597
+ are not counted late — read them with `browser_get_console_logs { includePreserved: true }` (see **Log
598
+ durability** below). The issue count covers the current page only.
599
+
600
+ ### Asking for the fresh state in the same reply
601
+
602
+ The footer above says *that* something broke. `include` is the half that shows *what*: pass
603
+ `include: "snapshot"`, `"console"`, `"network"` (or several, comma-separated) to `browser_click`,
604
+ `browser_type` or `browser_navigate` and each arrives as its own labelled block in the same reply —
605
+ no second and third round-trip that an agent usually doesn't make. A section that fails to load is a
606
+ footnote, never a failure of the click that worked — each one is fetched and labelled independently,
607
+ so a console read that times out does not cancel the network block beside it. Omit it and nothing
608
+ changes.
609
+
610
+ **A section name it does not recognise is refused before the action runs.** `include: "consoel"`
611
+ returns an error and the click never happens, so retrying the corrected call clicks once, not twice.
612
+ That ordering is the whole point: an option that only shapes the reply is checked while checking it
613
+ is still free.
614
+
615
+ It is on those three tools only. On hover, drag, scroll or a key press the answer is almost always
616
+ "nothing worth a payload", and the param would be paid for on every request forever.
617
+
618
+ **`includeSnapshot` is the older, blunter version of the same idea, and its default is not the same
619
+ everywhere.** A navigation returns a **full snapshot by default**, because arriving on a new page
620
+ invalidates every element reference you were holding. Clicks, typing, hovers, drags and option
621
+ selects return a **one-line confirmation** instead. Pass `includeSnapshot: true` to add one to an
622
+ interaction, or `includeSnapshot: false` to strip it from a navigation you only wanted to perform —
623
+ that is the single cheapest saving available on a page-heavy task. Setting
624
+ `AUTOMATE_BROWSER_SNAPSHOT_EACH_ACTION` turns it on everywhere at once, which is the pre-lean
625
+ behaviour and costs accordingly.
626
+
627
+ ### Uncaught errors keep their stacks
628
+
629
+ An error the page throws is reported with the trail back to the throw site, in its own section below
630
+ the ordinary console lines, rather than as a bare "something went wrong" string. The plain log stays
631
+ chronological and unchanged; the stacks are kept out of it so it stays readable.
632
+
633
+ ### Service workers show up in the console
634
+
635
+ A page's service worker is invisible in ordinary logs, so a broken one looks like the site simply not
636
+ working. `browser_get_console_logs` folds worker **lifecycle** into the same log, tagged `[sw]`: the
637
+ registration attempt and whether it succeeded, state changes (`installing` → `activated` →
638
+ `redundant`), `controllerchange`, and messages the worker posts to the page. A FAILED registration is
639
+ reported as an error; a routine update is not, so a healthy worker never triggers the console-error
640
+ footer below.
641
+
642
+ It attaches to a worker that was **already registered before the page loaded**, which is the common
643
+ case — otherwise every second visit would look worker-free.
644
+
645
+ **The honest limit:** a `console.log` written *inside* the worker is not included. No debugger-free
646
+ API exposes it, and the tool's own description says so rather than letting you assume otherwise.
647
+
648
+ ### The console and network lists are paged, newest first
649
+
650
+ A busy single-page app produces hundreds of console lines and hundreds of requests, and both lists
651
+ used to arrive whole. `browser_get_console_logs` and `browser_network_requests` now return **50
652
+ entries per call**. On `browser_network_requests`, `limit` sets that page size; on
653
+ `browser_get_console_logs` it is fixed.
654
+
655
+ **Page 1 is the newest, and `page: 2` goes further back in time**, not forward. That direction is
656
+ deliberate: both tools already returned "the most recent N", so a call that does not mention `page`
657
+ returns what it always returned.
658
+
659
+ When more remains, a footer says which page you are on, how many entries exist in total, and the
660
+ exact next call:
661
+
662
+ ```
663
+ — page 1/4 of 173 requests, newest page first. Older: browser_network_requests {"page":2}
664
+ ```
665
+
666
+ Ask for a page past the end and you get **page 1 with a line saying the page you asked for does not
667
+ exist** — never an error, and never an empty result you have to interpret.
668
+
669
+ **The honest limits.** Everything that fits on one page prints no footer at all, so a quiet page looks
670
+ exactly as it did before. Thrown-error stacks are printed for **the page you asked for**, so an error
671
+ further back needs its page fetched. And the network log is the extension's ring buffer of the **1000
672
+ most recent** requests for the tab; paging cannot reach past what the browser still holds.
673
+
674
+ The default network page was **100** before this change and is now **50**.
675
+
676
+ ### Snapshots: lean, full, or to a file
677
+
678
+ `browser_snapshot` returns the lean tree by default. Pass `verbose: true` for the full tree — all text
679
+ and structure, not just interactive elements — or `filePath` to write it out and get a summary back,
680
+ for pages whose snapshot would otherwise fill the context window. **Element refs are identical in
681
+ both modes**, so you can switch mid-task and keep using refs you already hold. `filePath` goes through
682
+ the same sandbox as every other write path.
683
+
684
+ ### Frames, including cross-origin ones
685
+
686
+ Ad slots, embedded checkouts and payment fields usually live in an `<iframe>` from another origin, and
687
+ page JavaScript cannot reach across that boundary. Snapshots do: a cross-origin frame comes back as
688
+ its own `- frame <url>` block with working refs, and those refs can be handed straight back to
689
+ `browser_click` or `browser_type` — frame identity is resolved inside the extension, so an agent never
690
+ has to think about it.
691
+
692
+ **Every frame is its own block, same-origin ones included.** In the page tree an `<iframe>` appears as
693
+ a bare `- iframe` marker and the snapshot does not descend into it; its contents come back below,
694
+ under their own `- frame <url>` heading with `fN:` refs. A `srcdoc` frame, which has no url of its
695
+ own, is reached the same way.
696
+
697
+ Same-origin frames used to be printed inline instead, with unprefixed refs. That read more naturally
698
+ and was quietly wrong: such a frame was walked **twice** — once by its parent and once as itself —
699
+ and the two passes tag the same element with two different refs, so the one printed could be the one
700
+ the page no longer carried. It showed up plainly on a frame nested inside a cross-origin frame, whose
701
+ fields were listed twice under two spellings, only one of which resolved. Walking each document
702
+ exactly once removes the whole class.
703
+
704
+ Two rules keep a frame-heavy page readable, since a real ad page can carry dozens:
705
+
706
+ - **Frames with nothing interactive in them are dropped**, so empty slots never reach the agent.
707
+ - **At most 10 frames** are included, and when the cap bites the snapshot says so rather than
708
+ silently truncating. Same-origin frames now count toward that ten, where before they were inline
709
+ and free — the deliberate cost of the change above.
710
+
711
+ Refs are namespaced per frame, so two frames can never mint the same one.
712
+
713
+ **One call acts inside one frame.** Every ref in a single `browser_click`, `browser_hover`,
714
+ `browser_type`, `browser_select_option`, `browser_drag` or `browser_eval` has to come from the same
715
+ frame, and a ref with **no prefix means the top page** — never "whichever frame the other ref named".
716
+ A mixed pair is refused with `BAD_ARGS` *before* anything is injected, so nothing is clicked, typed,
717
+ dragged or re-tagged on the way to finding out. Dragging from one frame into another is not
718
+ supported, and the same check refuses it rather than performing half of it.
719
+
720
+ A prefix that does not parse — `f3:` with nothing after it, `fx:e1a2`, `frame3:e1a2` — is refused the
721
+ same way. It used to be looked up as a literal ref in the top page, which meant a mistyped frame
722
+ number quietly aimed the call at a different document and reported success.
723
+
724
+ ### Filling a form that spans frames
725
+
726
+ `browser_fill_form` is the deliberate exception to the rule above: one call **may** mix frames, because
727
+ a real checkout puts the card number in an embedded widget and the delivery address in the page around
728
+ it, and making an agent split that into three calls would defeat the tool.
729
+
730
+ **Order is preserved exactly as you wrote it.** The batch is split into consecutive runs of one frame,
731
+ not gathered per frame — so `[top, widget, top]` is filled in that order, three injections deep. This
732
+ matters when fields depend on each other: a country select that reveals a state select that feeds a
733
+ postcode back in the top page is filled in the order a person would.
734
+
735
+ **Every address is checked before the first field is written.** A malformed prefix anywhere in the
736
+ batch refuses the whole call with nothing written, rather than being discovered on field nine with
737
+ eight already set and no way to tell how far it got.
738
+
739
+ **Results name the ref you passed.** A per-field failure reports the prefixed ref, and a frame that
740
+ cannot be reached at all fails only *its* fields — the rest of the batch still reports its own result.
741
+ `browser_clear` follows the same reference rules.
742
+
743
+ Both tools also reach a field inside a **shadow root** now, which a plain top-level lookup missed even
744
+ with a perfectly good ref.
745
+
746
+ ### Checkboxes and radios take a boolean, and nothing else
747
+
748
+ A checkbox or radio field takes `"true"` or `"false"` — plus the obvious synonyms `1`/`0`, `on`/`off`,
749
+ `yes`/`no`, `checked`/`unchecked`. **Any other value is refused for those two**, as a per-field error
750
+ that names the convention, while the rest of the batch fills normally.
751
+
752
+ That refusal exists because of what the alternative did. A radio ignored the value entirely and was
753
+ always selected, so `{ value: "false" }` chose the option it had been told to leave alone, displaced
754
+ whatever the group had selected, and reported full success. Simply honouring the boolean would fix that
755
+ and open a quieter hole in its place: `{ value: "Express" }` is how a `<select>` is filled by the same
756
+ tool, and it would now silently do nothing at all. So a value that is neither true nor false is treated
757
+ as the mistake it is.
758
+
759
+ **To choose one option of a group, set that option's own ref to `"true"`** — the browser unselects its
760
+ siblings, as it does for a person. Setting a radio to `"false"` clears it and leaves the group with
761
+ nothing selected; it never promotes another option. Events follow the change: `input` and `change` fire
762
+ when the state actually changed, and not when it did not, because a radio's `onChange` handler
763
+ commonly re-selects `event.target.value` and firing it on a no-op would undo the clear you just asked
764
+ for.
765
+
766
+ ### Frame limits worth knowing
767
+
768
+ Three tools cannot follow a ref into a **cross-origin** frame. Each now says the frame is the reason,
769
+ instead of reporting the ref missing — which used to send an agent back for a fresh snapshot that
770
+ handed it the very same ref. A **same-origin** frame works in all three, at any depth.
771
+
772
+ | Tool | Cross-origin frame | Same-origin frame |
773
+ |---|---|---|
774
+ | `browser_screenshot { ref }` | Refused. The capture covers the whole tab, and the frame's position in it cannot be measured from outside — cropping anyway would return a confidently wrong region. Capture the viewport instead. | Works. The rect is measured inside the frame and translated up through each parent, so the crop lands on the element |
775
+ | `browser_upload_file` | Refused. It drives one debugger session, which does not extend into another origin's frame. Use the default tools for that frame. | Works, including inside a shadow root |
776
+ | `browser_click` in advanced (debugger) mode | Refused, for the same reason. The default click path injects into the frame directly and does reach it. | Works, with the frame's offset applied so the click lands on the element rather than on whatever sits at that point in the top page |
777
+
778
+ The dividing line in every row is the same one: a same-origin frame can be measured and read from its
779
+ parent, and a cross-origin frame cannot be — by design, not by omission.
780
+
781
+ ### Element refs survive a re-render
782
+
783
+ A ref is derived from the element's own identity — its tag, id, name, type, role, label and text —
784
+ not from its position in the page walk. Two consequences worth relying on:
785
+
786
+ - **The same element keeps the same ref** across snapshots of a page that has merely re-rendered, and
787
+ adding an element no longer renumbers every ref after it.
788
+ - **`browser_find` no longer invalidates your snapshot refs.** It used to clear and renumber every
789
+ ref on the page, so a read-only query threw away your whole map of it.
790
+
791
+ If a ref does go stale — the node was replaced, not just re-rendered — the server re-tags the page
792
+ and retries the action **once**, and says so in the result:
793
+
794
+ ```
795
+ Clicked "Sign in" — note: that ref had gone stale, so the page was re-tagged and the ref
796
+ re-resolved. Take a fresh browser_snapshot before relying on other refs.
797
+ ```
798
+
799
+ If it still cannot be found you get `STALE_REF` naming `browser_snapshot` as the fix, rather than a
800
+ click that lands somewhere unintended.
801
+
802
+ ### Clicking something a snapshot cannot name
803
+
804
+ A canvas, a map widget, a PDF viewer and an image map have no element a snapshot can hand you a
805
+ reference to. `browser_click` therefore takes a **point** as an alternative address — `{x, y}` in
806
+ viewport coordinates instead of a reference — and reports what was actually under it, since a
807
+ coordinate click that lands on the wrong thing is otherwise silent. Give one address or the other,
808
+ never both. `dblClick` works with either.
809
+
810
+ ### Running your own JavaScript, against elements you already found
811
+
812
+ `browser_eval` takes either an `expression` for a quick page-state check (`document.title`, a count, a
813
+ computed style) or a **`function` plus `args` of element refs** — so you can write
814
+ `(el) => el.getBoundingClientRect()` and pass a ref from a snapshot, instead of re-finding the element
815
+ with a selector that may not match what the snapshot saw. A `filePath` writes a large result out
816
+ rather than inlining it, and `dialogAction` answers a dialog the code itself raises instead of hanging
817
+ on it.
818
+
819
+ An `await`ed result is resolved before it is returned — a promise used to come back as an empty
820
+ object, which looked like a successful call that produced nothing.
821
+
822
+ ### What this server costs your context, measured
823
+
824
+ Two numbers, both taken on 2026-09-18 across **348 real Claude Code sessions, 89,258 model turns and
825
+ 3,811 browser calls** — not estimated.
826
+
827
+ **The tool list: 9,435 tokens, on every single request.** Every MCP client re-sends every tool's name,
828
+ description and schema with every message, used or not. Multiplied out across those sessions that came
829
+ to **934 million tokens — 80% of everything this server has ever cost**, and 92 million of it was paid
830
+ by 36 sessions that never called a browser tool at all. `npm run tokens` gates the per-request number
831
+ and CI fails when it creeps.
832
+
833
+ It used to be 10,469. Two things went, neither of them a capability: the `$schema` dialect key that
834
+ `zod-to-json-schema` stamps on all 46 input and 9 output schemas and the protocol never reads (770
835
+ tokens), and three parameter descriptions that were being restated six times over — `includeSnapshot`
836
+ alone cost 29 tokens on each of six tools. Those explanations now live in the shipped skill, which an
837
+ agent reads once per **task** instead of once per **turn**.
838
+
839
+ **A screenshot: about 1,531 tokens, and it never leaves.** That was the average inline capture across
840
+ 249 real ones, and an MCP result is re-read on every later turn until the session ends or is compacted
841
+ — so on a heavy browser day screenshots were **half** of this server's entire footprint. Of 445
842
+ captures, only 20 scoped themselves to one element with `ref`. If you are checking one button, pass
843
+ its `ref`; if you are checking a value, `browser_eval` or `browser_find` answer without a picture at
844
+ all.
845
+
846
+ **If your client has no tool search, use `AUTOMATE_BROWSER_TOOLS`.** Claude Code defers MCP tool
847
+ definitions until they are needed (`ENABLE_TOOL_SEARCH`, on by default), so the 9,435 is mostly not
848
+ charged there. Cursor, VS Code and Claude Desktop have no equivalent and pay it on every message —
849
+ set the variable to `core` (4,145) or a category list and pay only for what you drive. Note that
850
+ `browser_eval`, the tab tools and `browser_emulate` are **outside** `core`; a category list such as
851
+ `navigation,snapshot,content,capture,tabs` is usually the better fit for real work.
852
+
853
+ ### Screenshots that don't cost a context window
854
+
855
+ A full-viewport PNG inlined into the reply is the most expensive thing this server can do to a
856
+ context window, and usually you wanted one button. `browser_screenshot` therefore takes a `ref` to
857
+ capture **just that element** (it is scrolled into view first, and cropped from the real capture at
858
+ the right device-pixel scale), `webp` alongside `png`/`jpeg`, and a `filePath` — which writes the
859
+ image and returns **the path instead of the bytes**, through the same sandbox as every other write.
860
+
861
+ Element cropping was verified on a **1.5x** display on 2026-09-02, not only at 1x: a 300x150 element
862
+ comes back **450x225**, and the crop lands on the right region as well as the right size — the scale
863
+ is derived from the capture itself (`bitmap.width / viewportWidth`) rather than read from
864
+ `devicePixelRatio`. **The one honest limit:** an element whose device size lands on a half pixel
865
+ rounds **outward** (451.5 becomes 452), so at most one pixel of surrounding page can appear at the
866
+ right and bottom edges. Rounding inward would shave the element instead, which is the worse trade for
867
+ a picture of it.
868
+
869
+ ### The size ceiling on inline screenshots
870
+
871
+ A screenshot that comes back **inline** is downscaled to fit inside **1536 x 4096** device pixels,
872
+ aspect ratio preserved, before it leaves the browser. `AUTOMATE_BROWSER_SCREENSHOT_MAX_WIDTH` and
873
+ `AUTOMATE_BROWSER_SCREENSHOT_MAX_HEIGHT` change it; either set to `0` switches that half off. When it
874
+ bites, the reply says so — the size before and after — because an agent measuring coordinates off a
875
+ silently shrunk picture measures the wrong page.
876
+
877
+ Why a picture is worth less than it costs above that size: a 2K viewport capture is 2560x1440, and a
878
+ model's vision pipeline resizes it to roughly 1568 px on the long edge anyway. You pay for the pixels
879
+ and then they are thrown away. Held at 1536 wide the same capture is about **1 770 image tokens
880
+ instead of 4 900** — the same picture, 2.8x cheaper, on every single capture.
881
+
882
+ **Two ceilings rather than one, because a full page is not a viewport.** The box is applied with the
883
+ aspect ratio kept, so a matching 1536 height would take a 2545x6362 full-page capture down to **613 px
884
+ wide** — legibility traded away for nothing. At 4096 the width binds first for any page shorter than
885
+ about 6 800 device pixels, and a page taller than that is an overview whatever you do.
886
+
887
+ **The honest limit, and it is the interesting one.** This does **not** apply to a capture written to
888
+ `filePath`, which includes every frame of a strip. Measured on Edge 152, 2026-09-09, one Wikipedia
889
+ article:
890
+
891
+ | Capture | Chrome's PNG | Downscaled to 1536 wide |
892
+ |---|---|---|
893
+ | Viewport, 1080p — 1912x914 | 241 KB | png **446 KB** · jpeg60 103 KB · webp60 75 KB |
894
+ | Viewport, 2K — 2560x1440 | 332 KB | png **529 KB** · jpeg60 100 KB · webp60 74 KB |
895
+ | Full page, 2K — 2545x6362 | 1 198 KB | png **2 287 KB** |
896
+
897
+ Downscaling a screenshot and re-encoding it as PNG makes the **file bigger** — 60% bigger for a
898
+ viewport, 91% for a full page. Chrome's capture is flat colour and sharp edges, which PNG compresses
899
+ extremely well; resampling turns every edge into a gradient of unique pixels a lossless codec then has
900
+ to store in full. Only the lossy formats shrink, and they shrink hard (332 KB → 74 KB as webp). So a
901
+ capture headed for disk is left at full size: there the cost genuinely is the bytes, the ceiling would
902
+ add to them, and the detail it spends is not recoverable. If you want a small **file**, ask for
903
+ `format: "webp"` with a `quality`, which is the thing that actually works.
904
+
905
+ **`format: "webp"` works on a full page too, and until 2026-09-14 it did not.** Chrome's capture APIs
906
+ produce PNG and JPEG only, so webp is always a re-encode of a capture — the viewport path did that
907
+ re-encode, and the full-page path, which renders through the debugger, silently ignored the format and
908
+ handed back a PNG labelled `image/png`. That is why the full-page row above lists no webp figure: it
909
+ was measured while the option did nothing. Measured after the fix on a small local page, full page,
910
+ quality 60: **18 917 bytes as png against 12 380 as webp**, and the reply now says `image/webp`.
911
+
912
+ ### A strip of stills, for showing a person what happened
913
+
914
+ `browser_screenshot { frames: 10, intervalMs: 150, filePath: "out/strip.png" }` captures ten stills in
915
+ a row and writes them as `strip-01.png` … `strip-10.png`, numbered so they sort in capture order. The
916
+ reply is the paths and one summary line — **never the images**, because ten pictures inline is the
917
+ most expensive thing this server could put in a reply. A strip with no `filePath` is refused for
918
+ exactly that reason. Each generated name goes through the path sandbox in its own right, not just the
919
+ one you passed.
920
+
921
+ The audience for this is the human, not the agent: hand someone a sequence of the bug instead of
922
+ describing it, or review a flow that scrolled past.
923
+
924
+ **Say the limit out loud: this is a strip of stills. No audio, no video file, no encoding.** Real video
925
+ would need `chrome.tabCapture`, which demands a user gesture on every single capture — the agent would
926
+ have to ask you to click the extension icon each time, which is not automation. That is why this is
927
+ deliberately the simple version rather than a feature waiting to be finished.
928
+
929
+ Two measured facts decide whether a strip is worth taking (Chrome 152, 2026-09-05, 10 frames at 100ms,
930
+ jpeg q60):
931
+
932
+ | The tab is | Achieved | Useful for |
933
+ |---|---|---|
934
+ | In the foreground | **~110 ms per frame**, about 9 per second | A transition, a fade, a flicker |
935
+ | In the background | **~3 900 ms per frame** | Slow changes only — a spinner that never stops, a progress bar |
936
+
937
+ The gap is not ours to close. **Chrome does not draw a tab nobody is looking at**, so every frame waits
938
+ for one to be rendered — and since the agent works in a background tab by default, that is the case you
939
+ will hit first. The reply says so when it happens, quoting both the interval you asked for and the one
940
+ it got, and names `browser_switch_tab` as the way to change it. Un-freezing the tab through the
941
+ debugger was tried and made no difference: 3.76 s versus 3.89 s, which is noise.
942
+
943
+ Every frame is rendered through the debugger, foreground or not, so Chrome shows its "being debugged"
944
+ banner for the length of the strip. That is deliberate: the cheap capture path Chrome offers extensions
945
+ is **rate-limited to 2 calls per second** and rejects the third outright, so a strip taken that way
946
+ would fail on frame three. The debugger attaches once and detaches at the end, not per frame.
947
+
948
+ ### Capturing a tab Chrome has stopped drawing
949
+
950
+ Screenshotting a background tab is a headline feature here — it is how an agent checks its work
951
+ without taking your screen. The honest limit is that Chrome **backgrounds the renderer of a tab
952
+ nobody is looking at and stops producing frames**, so the capture can wait on a frame that never
953
+ arrives. Measured on 2026-09-01, before this was handled: **3 stalls in 8 captures**, each of which
954
+ succeeded when simply asked again. Re-measured on 2026-09-02 with the handling in place: **0 stalls
955
+ in 11 captures** — eight back-to-back, then two more after 75-second idle spells, because
956
+ back-to-back captures keep the renderer warm and never exercise the throttled case. That proves no
957
+ stall reached the caller, which is the promise; a stall absorbed by the internal retry is invisible
958
+ from outside by design.
959
+
960
+ Neither remedy usually recommended for this is open to us — bringing the tab to the front steals
961
+ your focus, and the `--disable-renderer-backgrounding` family are Chrome launch flags an extension
962
+ cannot set. So each capture is now bounded at **8 seconds**; on a stall the page is nudged out of
963
+ its backgrounded state and the capture is tried once more. If both attempts stall you get
964
+ `CAPTURE_STALLED`, **flagged retryable** — a screenshot has no side effect, so re-issuing it is a
965
+ genuine retry rather than a repeated action, which is why it is the one timeout here treated that
966
+ way. `browser_switch_tab` always captures, at the cost of your focus.
967
+
968
+ Before this, a stall settled nothing for up to 22 seconds and then surfaced as a bare "Socket message
969
+ timeout" that named no cause — the extension had no ceiling of its own, so the failure was always
970
+ reported by whichever caller gave up first.
971
+
972
+ ### Emulation: location, headers, and the responsive-testing set
973
+
974
+ `browser_emulate` fakes a **location** and adds **extra request headers** with no debugger involved.
975
+ The rest — **dark mode**, a **device viewport** (this is where responsive testing lives), a faithful
976
+ **user agent**, and **network / CPU throttling** — needs `browser_advanced_mode`, because no
977
+ debugger-free API can do them: a JS override of `userAgent` only fools reads, and
978
+ `prefers-color-scheme` cannot be overridden from page JS at all. Name options in `clear` to switch
979
+ them off, or call with no arguments to see what is in force.
980
+
981
+ Three honest limits. The faked location only affects requests the page makes **after** the call — a
982
+ site that asks while it is still loading has already asked, and the answer for that case is
983
+ `browser_navigate {initScript}`. While CPU or network throttling is on, every internal wait is
984
+ scaled to match, so a throttled page is not mistaken for a broken one.
985
+
986
+ And **a page loaded before you emulated a phone still feature-detects as a desktop.** Whether
987
+ `ontouchstart` exists is decided when the document is created, so a page already open keeps the
988
+ answer it was born with, however convincingly the viewport now says otherwise. The touch-point count
989
+ updates immediately, which is what makes this confusing rather than obvious. Reload after emulating,
990
+ and the result of any call that switches touch on says so.
991
+
992
+ ### Actionability: interactions wait for the element to be ready
993
+
994
+ Before a click, type, select or drag, the element must be **visible**, **enabled**, **stable** (its
995
+ box unchanged across an animation frame) and — for pointer actions — **hit-testable** (nothing is
996
+ covering it). A failing check is waited out for up to a second; after that the action is refused,
997
+ naming the check and what was in the way:
998
+
999
+ ```
1000
+ Element "e7k2f" is not actionable: failed the "hit-testable" check after 1000ms.
1001
+ It is covered by <div#cookie-banner> "We use cookies".
1002
+ ```
1003
+
1004
+ Hit-testing applies to `browser_click` and `browser_drag` only — `browser_type` and
1005
+ `browser_select_option` go through focus, so something covering the element cannot misdirect them.
1006
+
1007
+ After the action the page is given ~100 ms of mutation-free time (capped at 1 s) before the tool
1008
+ returns, so the snapshot you take next is of the settled DOM rather than a page mid-re-render. Set
1009
+ `AUTOMATE_BROWSER_ACTIONABILITY=off` to skip both the checks and the settle.
1010
+
1011
+ **What it costs:** ~110 ms per click. Measured on a real Chrome (2026-08-30), 10 clicks on an
1012
+ ordinary page: **450 ms** per click with the checks on, **341 ms** with them off. The default stays
1013
+ on — a fast click on the wrong element is the fault this exists to prevent.
1014
+
1015
+ Two paths are **not** covered yet: `browser_fill_form` / `browser_clear`, and `browser_click` /
1016
+ `browser_press_key` while advanced (debugger) mode is attached.
1017
+
1018
+ **The honest limit — a CSS fade never completes in the agent's background tab.** Chrome does not
1019
+ advance a transition in a tab it is not drawing. The page's script sets the target opacity and the
1020
+ animation simply never runs, so the element stays at its starting value and the visible check fails no
1021
+ matter how long it waits. Measured 2026-09-01 mid-fade in a background tab: inline opacity `1`,
1022
+ computed opacity `0`. Waiting longer cannot fix it.
1023
+
1024
+ This is the most likely cause of `failed the "visible" check` on something a person would plainly see
1025
+ appear — a modal, a dropdown, a toast. The options are to read or act on what the transition reveals
1026
+ rather than the transition itself, or `browser_switch_tab` to bring the tab forward and accept taking
1027
+ the user's focus. Both fade states are pinned by `npm run test:integration`, in the background tab and
1028
+ the foreground one, so neither behaviour can change unnoticed.
1029
+
1030
+ ### Deciding when an action is finished
1031
+
1032
+ The settle above is the default, not the only choice. `browser_navigate`, `browser_click`,
1033
+ `browser_type`, `browser_press_key`, `browser_go_back` and `browser_go_forward` take **`waitUntil`**,
1034
+ which says what "finished" means for that one call:
1035
+
1036
+ | `waitUntil` | Returns when |
1037
+ |---|---|
1038
+ | `none` | immediately, without waiting for anything |
1039
+ | `auto` | the DOM stops changing — the default for a click, a keystroke or typing |
1040
+ | `load` | the page's load event has fired — the default for a navigation |
1041
+ | `networkidle` | the page has also stopped making requests |
1042
+
1043
+ **`settleMs`** caps that wait in milliseconds (up to 15000), and the separate **`timeout`** gives up
1044
+ on the whole call. They answer different questions: `settleMs` is how long to wait for the page to go
1045
+ quiet after the action worked, `timeout` is how long to wait for the action at all. Use `none` on a
1046
+ click whose result you are about to poll for anyway, and `networkidle` on the one that kicks off the
1047
+ fetch you actually care about.
1048
+
1049
+ ### What `settled` means on a navigation
1050
+
1051
+ `browser_navigate` answers with `settled` and `elapsedMs`, and `settled: true` means **the navigation
1052
+ you asked for** finished — the tab went into loading and came back out of it. The status the tab had
1053
+ before you asked does not count towards that: straight after a reload is issued, a tab still reports
1054
+ the document you are *leaving* as complete, and taking that for an answer is how a reload used to come
1055
+ back `settled: true` in 0 ms with the old page still on screen.
1056
+
1057
+ Three consequences worth knowing before you read one of these results:
1058
+
1059
+ - **A move that never loads settles as soon as the url changes.** A `#fragment` jump, or a history
1060
+ step that stays inside one document, fires no load event at all, so waiting for one would hold the
1061
+ call for its whole budget instead of returning.
1062
+ - **A transition that never begins gives up after about 1 second**, with `settled: false`. A url that
1063
+ turns out to be a download, a "Leave site?" prompt nobody answered, a forward entry that was not
1064
+ there — nothing loads, and saying so in a second beats blocking for fifteen. **But the tab gets
1065
+ asked before that is believed**: if it reports itself loading, the navigation is underway and the
1066
+ full budget applies, even though no event said so. Chrome does exactly this to a page it has
1067
+ refused before — a certificate warning is re-attempted after roughly **3 seconds**, and the second
1068
+ it was given used to be reported as a page that never loaded.
1069
+ - **`settled: false` is not an error.** It means the load was not seen to finish inside what you
1070
+ allowed — you asked for `waitUntil: "none"`, your `settleMs` ran out, the tab was closed, or the page
1071
+ is genuinely still loading. The call still tells you `urlBefore`, `urlAfter` and `navigated`.
1072
+
1073
+ The budget is 15 seconds for `browser_navigate` and 10 for `browser_go_back` / `browser_go_forward`,
1074
+ and `settleMs` only ever shortens it — a `settleMs` of 300 gives up at 300 ms, grace included.
1075
+
1076
+ ### A navigation that did not happen says so
1077
+
1078
+ `browser_navigate` used to answer `Navigated to <url>` whatever the browser did with it. The extension
1079
+ has always reported the truth — `navigated`, `settled`, and the url the tab is really on — but that
1080
+ only reached `structuredContent`, and **asking for a snapshot discards `structuredContent` entirely**.
1081
+ So the common call handed you the *previous* page's snapshot under a success message.
1082
+
1083
+ Now, when the tab is not where the call asked it to go, the reply says so instead — in the snapshot
1084
+ reply too, at the top, where it cannot be skipped:
1085
+
1086
+ > Did NOT reach `https://example.test/two` — after 2.0s the tab was still on `http://example.test/one`,
1087
+ > so anything you read now is that OLD page, not `https://example.test/two`.
1088
+
1089
+ **Read it as an observation, not a verdict**, because a verdict is not available: the browser is still
1090
+ moving while the reply is being written. Before saying anything, the server re-asks the tab where it
1091
+ is, every 200 ms for up to **2 seconds** (`AUTOMATE_BROWSER_NAV_CONFIRM_MS`) — a page the browser is
1092
+ slow to commit typically lands about **700 ms** after the call returns, and announcing failure on the
1093
+ first answer got it wrong on two visits out of three. That wait is paid **only** by a call already
1094
+ reported as having gone nowhere; a navigation that worked costs nothing extra.
1095
+
1096
+ **The honest limit.** This is a race against a browser that is still moving, so it is not exact.
1097
+ Measured over 16 real navigations to a host the browser refuses (a bad certificate, which reproduces
1098
+ the case reliably): **14 were reported correctly**, one warned about a page that arrived just after
1099
+ the window closed, and one stayed quiet about a page that bounced back to the old document after the
1100
+ window. Both disagreements are at that boundary. It is silent by design where an unchanged url is
1101
+ correct anyway — a reload, `waitUntil: "none"` (which asked not to wait, and so cannot know), a
1102
+ navigation to the page already open, and a redirect that lands somewhere other than the url you typed.
1103
+
1104
+ ### Waiting for a condition, not a duration
1105
+
1106
+ `browser_wait` sleeps for a fixed number of seconds and is almost always the wrong tool — it is too
1107
+ short on a slow run and wasted time on a fast one. `browser_wait_for` returns the moment the thing
1108
+ you are waiting for is true, and gives up after 15 seconds unless you say otherwise:
1109
+
1110
+ - **an element**, by CSS selector, in whichever state you mean — `visible` (the default), `hidden`,
1111
+ `attached` or `detached`. The last two are the ones that catch a spinner being removed rather than
1112
+ merely faded out.
1113
+ - **text**, as a case-sensitive substring anywhere in the page body.
1114
+ - **the URL**, as a substring or, wrapped in slashes, a regular expression — which is how you wait
1115
+ out a login redirect without guessing how long it takes.
1116
+
1117
+ ### Reading a page without a snapshot
1118
+
1119
+ A snapshot is for interacting; three cheaper tools are for reading, and each truncates rather than
1120
+ flooding the reply:
1121
+
1122
+ - `browser_read_page` strips the navigation, scripts and styling and returns the article as plain
1123
+ text or Markdown. It returns no element references, which is the point — it is the cheapest way to
1124
+ answer a question about what a page says.
1125
+ - `browser_get_html` returns raw `outerHTML`, for the attributes and structure a snapshot leaves out.
1126
+ Whole document by default, or one element by reference, capped at 50000 characters.
1127
+ - `browser_find` returns just the elements matching a text substring, an ARIA role, a CSS selector or
1128
+ any combination, with working references and a default cap of 20. It is the tool for "click the
1129
+ Sign in button" when you already know that is what you want.
1130
+
1131
+ The two that truncate say `…(truncated)` when a cap bit, and `maxLength` moves it; `find` simply
1132
+ returns at most `max` matches. None of the three throws away the element references you are already
1133
+ holding. → [Element refs survive a re-render](#element-refs-survive-a-re-render)
1134
+
1135
+ ### Tools the page declares about itself
1136
+
1137
+ Some pages publish the things they can do, so an agent can call the action directly rather than
1138
+ finding and clicking the controls for it. A call made that way does not break when the site is
1139
+ redesigned, and it works on pages an accessibility snapshot reads poorly. `browser_page_tools` reads
1140
+ two conventions in one pass: the **WebMCP** draft (`document.modelContext`, with the older
1141
+ `navigator.modelContext` as a fallback) and the **`devtoolstooldiscovery`** event that
1142
+ chrome-devtools-mcp introduced.
1143
+
1144
+ `action: "list"` returns what the page offers, with each tool's argument names. `action: "call"` runs
1145
+ one — `name` is the tool, `args` is a JSON object **string**. The result comes back JSON-encoded.
1146
+
1147
+ **The honest limit: expect an empty list.** This is a forward-looking capability, shipped so that
1148
+ pages adopting the standard work here the day they do, not a feature with sites to use it on today.
1149
+
1150
+ - The standard is a **draft**, and essentially no live site declares anything through either
1151
+ convention yet.
1152
+ - Availability differs **between builds of the same Chrome version**. Measured on 2026-09-05:
1153
+ `document.modelContext` was present in Chrome for Testing 152.0.7977.54 with no flags and absent
1154
+ from installed stable 152.0.7977.83. `navigator.modelContext` was gone in both. The discovery event
1155
+ needs no flag and answered everywhere.
1156
+ - A page that declares nothing returns an **empty list and a plain reason**, never an error. That is
1157
+ a fact about the page. Do not retry it.
1158
+ - A result that will not encode as JSON — a DOM node, a circular object — comes back as a note rather
1159
+ than a value. `browser_eval` and `window.__dtmcp.executeTool(name, args)` keep the live object if
1160
+ you need it.
1161
+ - Calling a page tool can do anything the page can, so it is refused in read-only mode like any other
1162
+ write.
1163
+
1164
+ ### Dialogs are armed in advance, not answered afterwards
1165
+
1166
+ `browser_handle_dialog` sets a **policy for the dialogs a page raises next** — `accept` (with
1167
+ `promptText` for a `window.prompt`), `dismiss`, or `native` to hand them back to the browser. Called
1168
+ with no arguments it reports the policy in force and the dialogs seen recently, without changing
1169
+ anything.
1170
+
1171
+ **The honest limit: a dialog that is already up cannot be answered this way.** An open `alert`,
1172
+ `confirm` or `prompt` pauses the page's renderer, so the override this tool installs is itself frozen
1173
+ along with everything else. Clear one with `browser_navigate {reload: true}`, or turn on
1174
+ `browser_advanced_mode` first — the debugger sits outside the renderer and is the only thing that can
1175
+ answer a modal already blocking it. A page-acting tool that times out names this as the likely cause
1176
+ rather than leaving you to guess.
1177
+
1178
+ A dialog fired synchronously during the very first moments of a page load can also be missed, because
1179
+ the override is installed at document start and that race is genuinely tight.
1180
+
1181
+ ### Where the LCP time actually went
1182
+
1183
+ "Your LCP was 3.2 seconds" is a number. "1.9 of those seconds were the server thinking before it sent
1184
+ the first byte" is a task. Every trace that carries an LCP now also carries the four spans that add up
1185
+ to it, each with its share of the total:
1186
+
1187
+ ```
1188
+ LCP breakdown — 1300ms total:
1189
+ time to first byte 200ms (15%)
1190
+ resource load delay 300ms (23%)
1191
+ resource load time 600ms (46%)
1192
+ render delay 200ms (15%)
1193
+ cause: the LCP image itself took 600ms to download (46%).
1194
+ fix: serve it in AVIF or WebP, sized for the viewport rather than the original upload.
1195
+ ```
1196
+
1197
+ Underneath it, the **render-blocking resources**: the requests that finished before first paint and
1198
+ held it up, slowest first, with their own cause and fix line. An `async` script is deliberately left
1199
+ out — it is already doing the right thing, and listing it would be advice to break working code.
1200
+
1201
+ The spans always **sum to the reported LCP**. That is what makes the biggest one safe to act on, and
1202
+ it is the property the whole feature rests on.
1203
+
1204
+ **The honest limits, which are most of the value here.** Chrome never says "the LCP resource was X";
1205
+ the breakdown reaches it by joining an element id across two different events, and there are three
1206
+ cases where it cannot:
1207
+
1208
+ - **A text LCP** has no resource to download, so you get **two** spans, not four zeroes, and a line
1209
+ saying so.
1210
+ - **An image whose network request cannot be matched** in the trace folds the download into render
1211
+ delay and says that in as many words, rather than reporting a download of zero.
1212
+ - **A trace with no document-request timing**, or one whose spans come out negative because the
1213
+ document belongs to a different navigation, reports **no breakdown at all** and names what was
1214
+ missing.
1215
+
1216
+ Read those lines as "not measured", never as "fine". None of this needs a new tool, a new parameter,
1217
+ or a bigger trace — it is the same recording, read more carefully. The parsing is pure and covered by
1218
+ `tests/trace-insights.test.ts`, so it needs no browser to test.
1219
+
1220
+ ### Log durability
1221
+
1222
+ The network log is mirrored into the browser's session storage, so it survives Chrome idling the
1223
+ extension's service worker — the case where `browser_network_requests` used to come back empty and
1224
+ `browser_issues` reported nothing wrong. It does **not** survive a browser restart.
1225
+
1226
+ Both list tools take `includePreserved`, which reaches back over the last 3 navigations instead of
1227
+ just the current page — for debugging a login redirect, where the log you want belongs to the page
1228
+ you were just bounced off. The console half is best-effort by construction: the page hands its buffer
1229
+ over as it is being unloaded, so a crashed renderer, a killed tab or a `chrome://` navigation leaves
1230
+ nothing to preserve, and the tool says so rather than pretending.
1231
+
1232
+ ### Errors name the next step, not just the problem
1233
+
1234
+ Failures carry a **typed code** and the tool that recovers from it, so an agent has somewhere to go
1235
+ instead of guessing or retrying the same call. `STALE_REF` names `browser_snapshot`. `NOT_ACTIONABLE`
1236
+ says which check refused and what was covering the element. `ORIGIN_BLOCKED`, `READ_ONLY` and
1237
+ `EVAL_BLOCKED` name the setting that refused and point at `browser_status`. `ADVANCED_MODE_REQUIRED` names the tool that turns
1238
+ it on.
1239
+
1240
+ The same idea runs through the messages that are not errors at all. A timeout on a page-acting tool
1241
+ mentions that an open dialog pauses the renderer and names two ways to clear one. A trace with no
1242
+ measurements says whether the cause is provable (nothing was recorded) or a guess (the window may not
1243
+ have been drawn). A screenshot on an unfocused window refuses and names both escapes rather than
1244
+ handing back a stale frame.
1245
+
1246
+ The set is closed — seventeen codes, and no others. The first twelve describe something that went
1247
+ wrong with your call; the last five label an entry in the issues feed, so a silent failure and a
1248
+ raised error name the same problem the same way.
1249
+
1250
+ | Code | Means | Next step |
1251
+ |---|---|---|
1252
+ | `STALE_REF` | the element was replaced, not just re-rendered | `browser_snapshot` |
1253
+ | `NOT_ACTIONABLE` | it failed the visible / enabled / stable / hit-testable check | the message names which, and what was covering it |
1254
+ | `TAB_CLAIMED` | another agent is driving that tab | `browser_force_claim` |
1255
+ | `TAB_GONE` | the tab was closed underneath you, or could not be recovered after the browser reconnected | `browser_list_tabs` |
1256
+ | `LEASE_LOST` | your claim expired or was taken — usually arrives as a notice on your next successful call, not as a failure | `browser_select_tab` |
1257
+ | `NO_BROWSER` | nothing is connected, the link dropped mid-call, or the browser the call was aimed at did not come back | retried once automatically when the action demonstrably never happened, and only against that same browser. When the link dropped *after* the request went out and the tool is one that cannot be repeated safely, it is **not** flagged retryable and the message says the action may already have landed |
1258
+ | `RESTRICTED_PAGE` | a `chrome://` page, the store, or the PDF viewer | drive an ordinary http(s) page |
1259
+ | `ADVANCED_MODE_REQUIRED` | the option needs the debugger | `browser_advanced_mode` |
1260
+ | `CAPTURE_STALLED` | Chrome stopped drawing the tab, twice | flagged retryable — a screenshot has no side effect |
1261
+ | `ORIGIN_BLOCKED` | your own allow / deny / sensitive list refused it, or the tab moved between the check and the action | `browser_status` prints the policy |
1262
+ | `READ_ONLY` | read-only mode is on and this tool changes the page | `browser_status` |
1263
+ | `EVAL_BLOCKED` | running JavaScript you wrote is switched off — even to read | `browser_status`; use a snapshot or `browser_find` instead |
1264
+ | `CSP_BLOCKED` `MIXED_CONTENT` `CORS_BLOCKED` `DEPRECATED_API` `THIRD_PARTY_COOKIE_BLOCKED` | why something on the page silently did nothing | `browser_issues` |
1265
+
1266
+ Each also arrives as `structuredContent` — the code, the message, whether it is retryable, and the
1267
+ tool to call next — so a client can branch on it without reading the prose at all.
1268
+
1269
+ **The honest limit:** the nine tools that declare an output schema (`browser_get_cookies`,
1270
+ `browser_storage`, `browser_downloads`, `browser_network_requests`, `browser_status`,
1271
+ `browser_list_clients`, `browser_list_tabs`, `browser_new_tab`, `browser_select_tab`) get the code in
1272
+ the **text only**. A client validates any `structuredContent` against the declared schema — failures
1273
+ included — so attaching the error head there replaced the real reason with a schema-validation
1274
+ complaint. The `CODE: message` line and the `Recover:` line are identical either way.
1275
+
1276
+ **Every one of these exists because the bare version cost someone a debugging session.**
1277
+
1278
+ ### Automatic retry for transient failures
1279
+
1280
+ If a call fails because the connection to the relay dropped or the extension's worker was being
1281
+ replaced — failures where the action demonstrably never happened — the server retries it **once** and
1282
+ notes that it did. This applies only to tools that declare themselves idempotent: a click, a form fill
1283
+ or a force-claim is never retried, because "it probably didn't happen" is not good enough for a side
1284
+ effect. Timeouts are never retried either, for the same reason.
1285
+
1286
+ ### Five outcomes, not two
1287
+
1288
+ `isError` is a boolean, and five different things were being squeezed through it. Three of them came
1289
+ out wrong, and every one of them was wrong in the direction that costs you something:
1290
+
1291
+ | What happened | What you get now |
1292
+ |---|---|
1293
+ | **Refused** — a safety policy, a bad argument, a section name that does not exist | the failure, with its code. Nothing was sent, so nothing can have happened |
1294
+ | **Done** | the result. Optional extras that failed on the way out are footnotes, never a demotion |
1295
+ | **Partly done** | `outcome: "partial"`, plus the per-field verdicts. `browser_fill_form` also returns `{ filled, total, errors }` as `structuredContent` |
1296
+ | **Failed** | `isError: true`. A tool that *returns* a failure is now recorded as one in the audit trail too — it used to be logged `ok` |
1297
+ | **Nobody knows** | the link dropped **after** the request went out. The message says the action **may have taken effect**, points you at `browser_snapshot`, and is **not** flagged retryable |
1298
+
1299
+ The last row is the one that mattered most. A `browser_click` whose reply was lost used to come back
1300
+ as "usually transient — retry in a moment", flagged retryable. The request had already gone out.
1301
+ "Nobody knows whether it happened" is not "it did not happen", and the difference is a second click —
1302
+ a second payment, a second message sent. Tools that declare themselves idempotent, and every read, are
1303
+ unaffected: repeating them is free, so they keep the retry and the old wording.
1304
+
1305
+ `outcome` rides **beside** `isError`, not inside `structuredContent`, so it is visible on a tool that
1306
+ declares an output schema without breaking that schema. It is present only when `isError` alone would
1307
+ mislead — in practice on a partial fill.
1308
+
1309
+ **The honest limit:** "may have taken effect" is the best answer available, not a lookup. Nothing
1310
+ tells the server whether a request it never got an answer to was executed, and a **timeout** is
1311
+ outside this rule entirely — it keeps the existing advice about a possible open dialog, which is the
1312
+ far likelier cause.
1313
+
1314
+ ### The link to the relay repairs itself
1315
+
1316
+ The retry above is about one call. This is about the connection underneath it. When the server loses
1317
+ the relay — it crashed, it was restarted, the editor that was hosting it quit — the server dials back
1318
+ on its own, waiting **0.5 seconds** before the first attempt and doubling up to **8 seconds** between
1319
+ later ones. It keeps going for as long as the relay is unreachable, and the delay only returns to
1320
+ 0.5 seconds once a relay has actually completed the handshake, so a relay that accepts the connection
1321
+ and then rejects it is backed off from rather than hammered.
1322
+
1323
+ The same is true at startup: a server that comes up while the relay is briefly unavailable is not
1324
+ stranded. It reports the failure to whatever asked, and then keeps trying in the background.
1325
+
1326
+ **The honest limit:** until it is back, calls fail with `NO_BROWSER`, and a call is what tells you it
1327
+ recovered — nothing announces the reconnection. `browser_status` is the cheapest way to ask, and it
1328
+ does not itself force a connection. Shutting the server down cancels the retries for good; a server
1329
+ you deliberately stopped never comes back on its own.
1330
+
1331
+ ### `browser_status` says whether it is coming back
1332
+
1333
+ That question used to have no answer. `browser_status` printed one line —
1334
+ *"relay: not connected yet (starting / retrying)"* — for four different situations, including the one
1335
+ where nothing is being retried at all, so "wait a moment" and "this is over" read identically.
1336
+
1337
+ It now prints a `link:` line whose first word is the state, in capitals, so it can be matched without
1338
+ reading the sentence after it:
1339
+
1340
+ | Line | What to do |
1341
+ |---|---|
1342
+ | `link: CONNECTED` | nothing |
1343
+ | `link: WAITING — connected to the relay, but no browser has joined it yet` | open a browser with the extension; the server is fine |
1344
+ | `link: RETRYING — 2 failed attempts, next attempt in 1.5s. It will keep trying on its own. Last failure: …` | wait; it recovers without you |
1345
+ | `link: CONNECTING — 1 failed attempt so far. Last failure: …` | wait |
1346
+ | `link: STOPPED — this server is shutting down and will NOT reconnect.` | **stop waiting** — restart the server |
1347
+
1348
+ **Being on the relay and having a browser are separate states**, which is why `WAITING` exists: a
1349
+ server can be perfectly connected and still unable to drive anything, and reporting that as
1350
+ "connected" is what makes it look like the tools are broken.
1351
+
1352
+ The failure reason is the error's message only — never a stack — capped at 200 characters, with the
1353
+ shared token spliced out by value if it appears. The count is of *consecutive* failures and resets on
1354
+ a handshake the relay actually completed, not on a socket that merely opened.
1355
+
1356
+ **The honest limit:** it is one reason and one count, not a log. An earlier failure is overwritten by
1357
+ a later one, and nothing here is persisted — a history buffer would grow fastest in exactly the
1358
+ situation where nobody is reading it. It is also deliberately **not** in `outputSchema`: that is paid
1359
+ for on every request by every agent, and the state is already the first word of the line.
1360
+
1361
+ ### A retry never changes browser or tab
1362
+
1363
+ A retry goes back to the **same browser and the same tab** the call was aimed at, and nowhere else.
1364
+ The browser is matched by the identity the extension stores for itself, so reloading it — or its
1365
+ worker being evicted and revived — brings the same browser back under a new connection id and the
1366
+ retry follows it, carrying the original tab.
1367
+
1368
+ **The honest limits, all of which refuse rather than guess:**
1369
+
1370
+ - If that browser does not come back within `AUTOMATE_BROWSER_CONNECT_WAIT_MS`, you get `NO_BROWSER`.
1371
+ Another browser being connected changes nothing — a different profile has different tabs and a
1372
+ different logged-in session, so delivering there is not a recovery.
1373
+ - If the extension is old enough not to report a stable identity, a reconnect cannot be recognised at
1374
+ all, so there is no retry: `NO_BROWSER` on the first failure.
1375
+ - If the tab cannot be carried across, you get `TAB_GONE` and the action is **not** attempted. Adopt a
1376
+ tab again with `browser_select_tab` and repeat it.
1377
+
1378
+ This matters more than it sounds. Before this, a retry re-resolved the browser for itself, and the
1379
+ browser it landed on owned no tab of yours — so the extension fell back to the tab **the user was
1380
+ looking at**, and a navigation meant for a test page could replace their work.
1381
+
1382
+ ### Reload, and the two options that need the debugger
1383
+
1384
+ `browser_navigate { reload: true, ignoreCache: true }` forces a cache-bypassing reload — the answer to
1385
+ "but I already fixed that". It needs nothing special.
1386
+
1387
+ Two navigation options **do** require `browser_advanced_mode`, and say so in their own descriptions,
1388
+ because no debugger-free browser API can provide them:
1389
+
1390
+ - `initScript` — run your JS **before any page script** on this navigation (stub `Date.now`, mock
1391
+ `fetch`, pre-seed storage, kill a consent banner before it renders).
1392
+ - `handleBeforeUnload: "accept" | "dismiss"` — answer a native "Leave site?" prompt so the navigation
1393
+ cannot hang on it. The dialog policy that covers `alert`/`confirm`/`prompt` cannot: that prompt is
1394
+ drawn by the browser, and page scripts can neither see nor answer it.
1395
+
1396
+ Both are armed before the navigation is issued, and torn down again once it has finished or failed, so
1397
+ nothing is left running on later pages. **Torn down once it has finished** is the load-bearing half: a
1398
+ teardown that happened when the call returned would pull the init script before the new document
1399
+ existed, and the call would report `initScript: "installed"` for a script that never ran. So when
1400
+ either option is in play, `waitUntil: "none"` still waits for the page to commit before cleaning up —
1401
+ that one combination is not instant, and it is capped by the same 15-second budget. With advanced mode
1402
+ off they refuse and tell you to enable it — they never silently do nothing.
1403
+
1404
+ Separately, and with no debugger involved: when an open `alert`/`confirm`/`prompt` freezes the page,
1405
+ a page-acting tool used to time out with an unexplained "Socket message timeout". It now names the
1406
+ dialog as the likely cause and points at `browser_handle_dialog`.
1407
+
1408
+ ### Loading a site with a bad certificate
1409
+
1410
+ Internal staging environments routinely serve a certificate the browser refuses — self-signed, expired,
1411
+ or issued for a different host. **AutomateBrowser cannot get you past that, and no longer pretends to.**
1412
+
1413
+ **What to do instead:** start the browser with `--ignore-certificate-errors`, or click through the
1414
+ warning page by hand once — the click is remembered for that host for the session, and everything else
1415
+ in AutomateBrowser then works against it normally.
1416
+
1417
+ **Why there is no switch for it.** There was one, `browser_advanced_mode {acceptInsecureCerts: true}`,
1418
+ from 2026-09-10 until it was deleted on 2026-09-16. It never worked on any build: the call comes back
1419
+ `'Security.setIgnoreCertificateErrors' wasn't found`, because `chrome.debugger` exposes a **fixed
1420
+ allow-list** of DevTools Protocol domains to extensions and `Security` is not on it — the same
1421
+ allow-list that makes a heap snapshot impossible here. Measured against headless Chrome 153, headed
1422
+ Chrome 153 and Edge; identical in all three. A permanent error is not a feature, and it was charged to
1423
+ every request in every IDE, so it was removed rather than kept as a signpost.
1424
+
1425
+ **If you pass it anyway, you get told.** The argument is refused by name — it is not quietly dropped,
1426
+ which would leave an agent working from older guidance believing certificate checking was off. The
1427
+ refusal says it never worked, why, and names both workarounds above.
1428
+
1429
+ **The honest limit this leaves.** Nothing now probes that allow-list, so if a future Chrome adds
1430
+ `Security` to it, no check will announce it — the tripwire went with the feature. That was the
1431
+ deliberate trade: a per-request cost, forever, is too much to pay for an early warning about a command
1432
+ nobody can call. Note also that even a working bypass could not help where Chrome offers none, such as
1433
+ an HSTS-pinned host or an enterprise-policy interstitial, and that a tab already parked on the
1434
+ certificate interstitial will not accept a debugger attachment **at all** (`Cannot attach to this
1435
+ target`).
1436
+
1437
+ **How this was missed for six days, since that is the more useful lesson.** The command was verified
1438
+ present in Chromium's own source, which is true and was the wrong thing to check: what decides this is
1439
+ `chrome.debugger`'s allow-list, not whether the browser implements the command. Ten smoke checks
1440
+ covered it against a fake browser and all ten passed, because they assert the flag *leaves the server*
1441
+ correctly — which it did. Nothing exercised a real browser until `npm run test:live` was written.
1442
+
1443
+ ### Advanced mode does not change what interactions return
1444
+
1445
+ With `browser_advanced_mode` on, `browser_click` and `browser_press_key` are dispatched as real
1446
+ OS-level input rather than synthetic events. That is the only difference an agent sees. **Both still
1447
+ wait for the page to settle and still return the same fields** — `navigated`, `urlBefore`, `urlAfter`,
1448
+ `settled`, `elapsedMs`, plus the `hit` naming what was actually under a coordinate click.
1449
+
1450
+ Until 2026-09-01 they returned a bare `{ok: true}` instead. Enabling the debugger for something
1451
+ unrelated — reading one response body, recording one trace — therefore changed both the shape of every
1452
+ click reply and its timing, because the settle was skipped entirely. Pressing Enter to submit a form
1453
+ returned before the navigation had started, handing the next call a page still in motion. Nothing
1454
+ announced this, and no error was ever raised.
1455
+
1456
+ Two honest limits, and the first one matters more than the fix:
1457
+
1458
+ - **Trusted input cannot reach a background tab at all.** Chrome discards real input aimed at a tab it
1459
+ is not drawing. Since the agent works in a background tab by default, `browser_click` and
1460
+ `browser_press_key` **refuse** while advanced mode is on, naming the reason and telling you to bring
1461
+ the tab forward or turn the mode off. They never report a success that did not happen — but it does
1462
+ mean the practical pattern is: enable advanced mode for the thing that needs it, then turn it off
1463
+ before interacting. The debugger-free path works fine in a hidden tab.
1464
+ - The trusted path still does not report `recovered` or `domSettled`. Those describe the injected
1465
+ operation's ref-recovery, which real input genuinely does not perform, so they are omitted rather
1466
+ than faked.
1467
+
1468
+ Related, and found by the same test on 2026-09-01: trusted **Enter** dispatched only a raw key-down,
1469
+ which fires `keydown` listeners but runs no default action — so it moved focus, reported success, and
1470
+ **did not submit the form**. It now carries the character payload that makes the renderer run the
1471
+ default action, as does Space. Modifier combos (`Control+A`) deliberately still send no character: a
1472
+ shortcut is not a keystroke.
1473
+
1474
+ ### The agent's own tab
1475
+
1476
+ The agent drives **only a tab it explicitly owns**. On its first action against a browser it opens a
1477
+ new tab in the background and works there; that tab is its target until it selects another. It never
1478
+ adopts whichever tab you happen to have in front of you, so a request to "test this page" cannot
1479
+ navigate away a tab holding unsaved work.
1480
+
1481
+ To point it at a tab **you** already have open, use `browser_select_tab` — by `tabId`, `index`, or a
1482
+ `url`/`title` substring (`{ url: "localhost:3000" }`). That is a deliberate takeover, and the tab is
1483
+ driven where it sits, without being brought to the front.
1484
+
1485
+ **On `browser_release_client`, a tab the agent OPENED is closed; a tab it ADOPTED from you is left
1486
+ exactly where it is.** The distinction is deliberate — cleaning up after itself must never mean
1487
+ closing something of yours.
1488
+
1489
+ The honest limits:
1490
+
1491
+ - **If a background tab cannot be given to the agent, the call fails** rather than falling back to
1492
+ your tab. The error names `browser_select_tab` as the way out.
1493
+ - **The agent's tab is not closed when your editor simply exits** — only on an explicit release. An
1494
+ abandoned session leaves one background tab behind.
1495
+ - **A tab you handed over with `browser_select_tab` is never cleaned up**, by design. Closing it is
1496
+ your call.
1497
+ - **Cleanup is deferred, never redirected, when the browser that owns the tabs is gone.** Releasing
1498
+ sends closes only to that browser; if it has disconnected — or quits part-way through the sweep —
1499
+ the remaining tabs stay on its books and the next release closes them once it is back. So a release
1500
+ can legitimately close nothing and still report success. The alternative was worse: tab ids are
1501
+ small and allocated per browser, so Chrome's tab 101 and Edge's tab 101 both exist, and a close that
1502
+ re-resolved "the active browser" closed one of the user's tabs in the other one.
1503
+ - **Reloading the extension mid-session used to break cleanup entirely. Fixed 2026-09-02.** A relay
1504
+ id belongs to a *connection*, so reloading the extension — or the background worker being evicted
1505
+ and revived — brought the same browser back under a new one, and the record of which tabs the agent
1506
+ had opened went with the old id. Release then closed nothing. Ownership is now keyed by the
1507
+ extension's own stored id, which survives both. Found by hand in Edge: open a tab, reload the
1508
+ extension, release, and the tab was still there. A 90-second idle spell did **not** trigger it, so
1509
+ the trigger is the reconnect, never the waiting.
1510
+
1511
+ ### A clean, logged-out session on demand
1512
+
1513
+ Every tab the agent drives is your real, signed-in profile, so "what does a first-time visitor see?"
1514
+ has no answer without logging yourself out for real. `browser_new_tab { incognito: true }` opens a
1515
+ **private window** instead — a clean session with none of your cookies or logins — and drives a tab
1516
+ inside it. That is how a signup flow, a paywall, a cookie banner or any logged-out state gets checked
1517
+ without touching your account.
1518
+
1519
+ **It needs one setting, and only a person can turn it on.** On the extension's details page:
1520
+ "Allow in Incognito" in Chrome, "Allow in InPrivate" in Edge. Turning it on restarts the extension,
1521
+ so the connection blinks. Until then the call fails with `INCOGNITO_BLOCKED` and a message naming that
1522
+ exact setting in both browsers' words, rather than a bare failure.
1523
+
1524
+ Measured in Chrome 152 and Edge on 2026-09-05, and re-runnable with
1525
+ `node scripts/incognito-check.mjs [--edge]`:
1526
+
1527
+ - **The refusal had to be built, not caught.** Asking for a private window without permission does not
1528
+ fail — `chrome.windows.create({incognito:true})` **resolves with nothing at all**, in both browsers.
1529
+ Anything trusting that promise gets a "success" and no window. Permission is therefore checked
1530
+ first, which is what turns silence into a sentence.
1531
+ - **A private tab is owned, claimed and released like any other**, and appears in `browser_list_tabs`
1532
+ marked `(private)` so a clean session is never mistaken for your real one.
1533
+ - **Cookies are a separate jar, and the tools now address the right one.** A private window has its own
1534
+ cookie store; asking Chrome for cookies without naming a store reads *your* real one. So
1535
+ `browser_get_cookies` and `browser_set_cookie` resolve the driven tab's own store and use it. Before
1536
+ this, driving a private tab would have shown the agent your live session while it believed it was
1537
+ looking at a stranger's.
1538
+ - **Web storage was already correct** — `browser_storage` runs inside the tab, so it sees the private
1539
+ origin's storage and not yours.
1540
+ - Closing the tab ends the session. Nothing survives it, and there is nothing to clean up.
1541
+
1542
+ ### What can take your focus
1543
+
1544
+ Exactly one tool takes focus: **`browser_switch_tab`**, whose description says so, and which exists
1545
+ for "show me what you did". Everything else works in the background:
1546
+
1547
+ - `browser_new_tab` opens in the **background** by default. `active: true` is the explicit opt-in and
1548
+ is the only argument anywhere that brings a window forward.
1549
+ - `browser_select_tab` takes over a tab **without** activating it.
1550
+ - Navigation, clicks, typing, reads and snapshots all run on a background tab.
1551
+
1552
+ Screenshots included: **`browser_screenshot` works on a background tab and does not bring it
1553
+ forward.** How it gets there is worth knowing, because it has a visible cost.
1554
+
1555
+ The cheap capture Chrome offers extensions photographs *whatever is on screen* — the foreground tab
1556
+ of a window. Pointed at a background tab it returns the wrong page, and pointed at a window the OS
1557
+ is not drawing it returns a **stale or blank frame with no error at all** (measured 2026-08-27). So
1558
+ when the target tab is not the foreground tab of a drawn window, this server does not use that path.
1559
+ It renders the exact tab through the debugger instead, which needs nothing on screen.
1560
+
1561
+ - **The cost:** Chrome shows its "being debugged" banner for the duration. The debugger is detached
1562
+ again straight after, unless `keepEnabled: true`. This is the same mechanism `fullPage: true` has
1563
+ always used.
1564
+ - **The result says so.** A capture taken this way comes back with a line naming the reason — the tab
1565
+ was not the foreground one, or its window was minimised, or not focused — so an agent can tell you
1566
+ why the banner appeared instead of leaving you to wonder. It rides alongside the image, and in
1567
+ `structuredContent` for a client that prefers fields. A capture on the cheap path carries neither,
1568
+ so nothing changed for the ordinary case. **Fixed 2026-09-04:** the extension had always reported
1569
+ both, and the server dropped them before the reply was assembled, which made this very line untrue
1570
+ in both this file and the shipped skill for as long as either has said it.
1571
+ - **When the foreground tab IS the target**, nothing changes — the cheap path is used and no banner
1572
+ appears.
1573
+ - **The honest failure:** if the debugger cannot attach — a restricted page, or policy — the call
1574
+ **fails with that reason named**, and suggests `browser_switch_tab`. It never silently falls back
1575
+ to the capture that would return the wrong tab's pixels. Verified 2026-09-02 by pointing it at
1576
+ `edge://extensions/` from another tab: it refused, quoted Chrome's own reason, and returned no
1577
+ image.
1578
+ - **Having DevTools open does not block it, despite what this file used to say.** Until 2026-09-02
1579
+ the line above named DevTools as the first cause and offered closing it as the first remedy. Both
1580
+ were wrong: measured in Edge 152, a background tab with the DevTools panel open attached and
1581
+ captured correctly, because Chromium permits several debugger clients on one tab. The picture you
1582
+ get is whatever that tab is really rendering — including a phone-sized page if their DevTools is in
1583
+ device-emulation mode.
1584
+
1585
+ ### Sharing a browser with another agent
1586
+
1587
+ Ownership is **per tab**, so two editors can drive two different tabs of the same browser at the same
1588
+ time. Only same-tab access is serialized, through a soft claim that expires on idle, on
1589
+ `browser_release_client`, or when a client disconnects.
1590
+
1591
+ When another agent does take a tab you were driving, you are **told on the result of your next call**,
1592
+ whatever that call was — not left to discover it when an action lands somewhere unexpected. It arrives
1593
+ as a notice on a successful result, not as an error, because your call did succeed.
1594
+
1595
+ `browser_list_clients` shows every connected browser with who is driving it; `browser_select_client`
1596
+ picks one (`force: true` takes over and notifies the other agent); `browser_status` is the diagnostic
1597
+ when something is missing. The roster lists agents that are merely *connected*, not just those
1598
+ currently holding a claim, so an idle editor never silently disappears.
1599
+
1600
+ ### The server leaves when your editor does
1601
+
1602
+ A stdio MCP server normally exits when its client closes the pipe. That is watched here three ways
1603
+ (`close`, end-of-file, and a pipe reset, because `close` alone is unreliable on Windows) and so are
1604
+ `SIGINT` and `SIGTERM`. Shutdown is also bounded: if closing cleanly takes more than five seconds,
1605
+ the process exits anyway.
1606
+
1607
+ **An editor can also vanish without closing anything** — the window is killed, the machine sleeps and
1608
+ the MCP session is rebuilt, the process tree is reparented. The open relay socket keeps the event
1609
+ loop alive and the crash guards swallow the errors, so nothing would ever bring that process down;
1610
+ it would sit in every other editor's roster as an agent that is connected and never does anything.
1611
+ So the server watches the process that launched it and exits within five seconds of it disappearing.
1612
+
1613
+ Two things this deliberately does not do. It does not take the relay with it — the relay is shared,
1614
+ and idles out on its own about five minutes after the last agent *and* browser have gone. And it does
1615
+ not close the tab the agent was working in; only an explicit `browser_release_client` does that.
1616
+ → [The agent's own tab](#the-agents-own-tab)
1617
+
1618
+ Separately, an unexpected asynchronous error never kills the session. It is logged to stderr and the
1619
+ process carries on, because the alternative — the transport closing and your editor showing
1620
+ "Transport closed" — is a worse outcome than one failed tool call. A relay that is slow to start is
1621
+ handled the same way: the connection is made in the background and tool calls wait for it, so a
1622
+ failing relay can never make the initial handshake fail.
1623
+
1624
+ ### Machine-readable results
1625
+
1626
+ Tools that return data — not just a confirmation — also return `structuredContent` alongside the human
1627
+ text, so a client can consume the fields directly instead of parsing prose. The terminal CLI exposes
1628
+ the same thing with `--json`, which is what makes it scriptable.
1629
+
1630
+ ### Finding out your copy is out of date
1631
+
1632
+ A stale install is not a harmless thing here — the multi-IDE partition bug that shaped this whole
1633
+ roadmap was caused by one, and nothing in the product told anyone they were behind. The section
1634
+ below it compares two *local* processes to each other, so it can never notice that both are old.
1635
+
1636
+ So, on startup, the server checks whether a newer release exists and prints one line on **stderr**
1637
+ if there is:
1638
+
1639
+ ```
1640
+ A newer @automatebrowser/mcp is available: 0.3.0 -> 0.4.0. Update with `npm i -g @automatebrowser/mcp`.
1641
+ Set AUTOMATE_BROWSER_NO_UPDATE_CHECK=1 to silence this.
1642
+ ```
1643
+
1644
+ **Exactly what happens, since this is the one thing that leaves your machine.** A single unauthenticated
1645
+ `GET https://registry.npmjs.org/@automatebrowser/mcp/latest`, with no body, no identifier and no
1646
+ telemetry of any kind. The registry learns that some machine asked for this package's version number,
1647
+ which is the same thing it learns when you install it. Nothing about your browsing, your tabs, your
1648
+ profile or your machine is sent, because none of it is in the request.
1649
+
1650
+ **The honest limits.**
1651
+
1652
+ - **At most one request per 24 hours.** The answer is cached in `~/.automate-browser/update-check.json`
1653
+ next to the audit log, and a failed attempt is stamped too — so an offline machine retries tomorrow
1654
+ rather than on every start.
1655
+ - **It never blocks and never fails loudly.** Startup does not wait for it, the request gives up after
1656
+ 3 seconds, and being offline, blocked by a proxy or served nonsense is a silent no-op. If the very
1657
+ first run cannot reach the registry, the notice simply arrives on a later one.
1658
+ - **stderr only, never stdout.** stdout carries the MCP protocol frames and one stray byte there would
1659
+ corrupt the session.
1660
+ - **It respects `AUTOMATE_BROWSER_LOG_LEVEL`.** Set it to `error` and the notice is suppressed with
1661
+ everything else at warning level.
1662
+ - **It cannot update anything.** It tells you; you run the command.
1663
+ - **`AUTOMATE_BROWSER_NO_UPDATE_CHECK=1` prevents the request itself**, not just the printed line, and
1664
+ no cache file is written.
1665
+ - **The CLI does not do this** — only the MCP server does, once per server start.
1666
+
1667
+ ### Relay version mismatch
1668
+
1669
+ The relay is shared and single-instance, so whichever build starts it first owns the port until it
1670
+ shuts down. If one editor points at an older copy of AutomateBrowser, every other editor ends up
1671
+ driving *its* relay. `browser_status` now says so explicitly, naming both versions. The fix is to
1672
+ point every editor at the same build, close them all so the relay exits, then reopen.
1673
+
1674
+ ### File-path sandbox
1675
+
1676
+ Every tool that takes a local path only accepts one inside the **roots your MCP client advertises**,
1677
+ plus the system temp directory. That is one read path — `browser_upload_file`, which without this
1678
+ could make your logged-in browser upload `~/.ssh/id_rsa` to any site — and four write paths:
1679
+ `browser_perf_trace`, `browser_snapshot`, `browser_screenshot` and `browser_eval`, each of which can
1680
+ send a large result to a file instead of into the reply.
1681
+
1682
+ A client that sends no roots gets the server's working directory + temp instead; there is no
1683
+ allow-everything mode short of the env var above. Paths are resolved through symlinks before the
1684
+ check, so neither `../` nor a symlink out of a root gets past it, and a refusal names the roots it
1685
+ would have accepted so the agent can retry correctly.
1686
+
1687
+ **Naming your own folders: `AUTOMATE_BROWSER_WORKSPACE`.** Roots are a client feature, and plenty of
1688
+ clients do not implement them — at the time of writing Cline, Zed, Windsurf, Gemini CLI, OpenAI Codex
1689
+ and Amazon Q Developer CLI all connect without one, while Claude Code, Cursor and VS Code (GitHub
1690
+ Copilot) do send roots. Rather than reaching for the allow-everything switch, set this variable to the
1691
+ folders you want writable:
1692
+
1693
+ ```jsonc
1694
+ "env": { "AUTOMATE_BROWSER_WORKSPACE": "D:\\work\\my-app;D:\\work\\traces" } // ":" separated on macOS/Linux
1695
+ ```
1696
+
1697
+ The honest limits. It **adds** to whatever your client sent and never replaces it, so scoped roots
1698
+ stay scoped. Blank entries are dropped, which means an empty or whitespace-only value adds nothing
1699
+ rather than quietly meaning "everywhere" — that is the one behaviour worth knowing, because a variable
1700
+ set to `""` by a wrapper script is how a sandbox usually falls open. It grants the named folder and
1701
+ its children only, not the parent. And it is not a bypass: `..` and symlinks are still resolved and
1702
+ still checked. To see what actually took effect, run with `AUTOMATE_BROWSER_LOG_LEVEL=debug` — at
1703
+ startup the server prints every root in force and where each one came from:
1704
+
1705
+ ```text
1706
+ path sandbox: 3 root(s) in force — D:\work\my-app [AUTOMATE_BROWSER_WORKSPACE]; \
1707
+ D:\D Drive\Projects\thing [working directory (no client roots)]; C:\...\Temp [temp directory]
1708
+ ```
1709
+
1710
+ **A write target is also checked before the work starts, not after it.** Once the path is known to be
1711
+ inside a root, the parent directory is created if it does not exist, and the target is confirmed
1712
+ writable and not an existing directory. A trace can record for a minute before it writes; discovering
1713
+ a bad path then would throw all of it away. It also means `{filePath: "traces/run1.json"}` simply
1714
+ works rather than failing because `traces/` was not there yet.
1715
+
1716
+ ### Cookie, storage and header values are hidden by default
1717
+
1718
+ `browser_get_cookies`, `browser_storage {action:"get"}` and `browser_get_network_request` return the
1719
+ **names** and every other field, with each secret value replaced by `<redacted>`. Pass
1720
+ `revealValues: true` to get the real ones.
1721
+
1722
+ The reason is that this server drives a browser you are signed into everywhere. "List the cookies on
1723
+ this page" is a reasonable thing for an agent to do while debugging a login, and the default answer
1724
+ to it should not be your session token — copied into a transcript, a log, and whatever the model
1725
+ provider retains. The redaction is applied on the server, so the values do not reach the reply at
1726
+ all. The result also carries a `redacted` flag, so a client can tell a hidden value from an empty one.
1727
+
1728
+ Reveal them when you actually need them, which is rarer than it sounds — checking that a cookie
1729
+ **exists**, or when it expires, or whether it is `httpOnly`, needs no value at all. Note that
1730
+ `browser_eval` can read `document.cookie` directly and is not covered by this; the redaction is a
1731
+ sensible default on the tools built for the job, not a containment boundary. The audit log applies
1732
+ the same rule from the other side: a value that could be a secret is recorded as a length, never as
1733
+ a value.
1734
+
1735
+ **Request and response headers are the same secret by another name.**
1736
+ `browser_get_network_request` returns both header sets alongside the body, and hides the value of
1737
+ any header named `authorization`, `proxy-authorization`, `cookie` or `set-cookie`, or whose name
1738
+ contains `token`, `api-key`, `apikey`, `secret`, `password` or `credential`. Matching is
1739
+ case-insensitive, because HTTP header names are. The substring rule is deliberately wide: it will
1740
+ also hide a pagination `x-continuation-token`, and that costs one re-call with `revealValues: true`,
1741
+ where missing a real bearer token costs a live credential in a transcript that is already written.
1742
+
1743
+ Header **names** are always kept, and the result says how many values were withheld — because "this
1744
+ request sent no auth header" and "this request sent an auth header you cannot see" are different
1745
+ answers, and an agent debugging a `401` needs the difference. A realistic 20-header request/response
1746
+ pair costs **277 output tokens**, paid only when you call the tool — the schema cost of the new
1747
+ `revealValues` argument, which every request pays, is **27 tokens**.
1748
+
1749
+ ### Where a navigation may go
1750
+
1751
+ `browser_navigate` accepts `http:`, `https:` and `about:` URLs, and refuses everything else by name.
1752
+ That rules out `javascript:` (which would execute in whatever page is open), `file:` (which would
1753
+ read the disk through the browser, around the file sandbox above), and the browser's own
1754
+ `chrome://` / `edge://` pages, which cannot be automated anyway.
1755
+
1756
+ This is a scheme check, not a site policy — it is always on and there is nothing to configure. To
1757
+ restrict *which sites*, use the allow and deny lists below.
1758
+
1759
+ ### What can reach the local socket
1760
+
1761
+ The relay listens on loopback, which any program on this machine can reach — including a web page,
1762
+ since a page's JavaScript is free to open a WebSocket to `127.0.0.1`. Three rules sit in front of it:
1763
+
1764
+ - **A peer that presents a browser-extension origin must be the AutomateBrowser extension.** Any
1765
+ other extension origin is rejected with a 403 before the connection opens. Set
1766
+ `AUTOMATE_BROWSER_EXTENSION_ORIGINS` if you build the extension yourself under a different id.
1767
+ - **A peer that floods the socket is disconnected** — more than 120 frames in a second and the
1768
+ connection is closed. Genuine automation bursts sit far below that; the ceiling and its window are
1769
+ both configurable.
1770
+ - **A malformed frame ends the connection**, rather than being skipped. Anything that is not JSON, or
1771
+ that is JSON without the expected envelope, closes the socket immediately.
1772
+
1773
+ With `AUTOMATE_BROWSER_TOKEN` set, every peer must additionally prove it holds the secret — and the
1774
+ secret itself is never transmitted. The relay sends a random challenge, each side signs it with
1775
+ HMAC-SHA256, and only the signature crosses the wire. Both browsers and agents are checked the same
1776
+ way, and the comparison is constant-time.
1777
+
1778
+ ### Which sites the agent may touch
1779
+
1780
+ Off by default. Set none of the four variables above and nothing changes — no extra round-trip, no
1781
+ refusals, exactly today's behaviour.
1782
+
1783
+ Set one and every page-acting tool is judged before it runs. A pattern is an origin with `*`
1784
+ wildcards (`http://localhost:*`, `https://*.example.com`, or a bare host meaning any scheme):
1785
+
1786
+ ```jsonc
1787
+ "env": {
1788
+ "AUTOMATE_BROWSER_ALLOW_ORIGINS": "http://localhost:*, https://*.staging.example.com",
1789
+ "AUTOMATE_BROWSER_SENSITIVE_ORIGINS": "https://mail.example.com",
1790
+ "AUTOMATE_BROWSER_READ_ONLY": "1"
1791
+ }
1792
+ ```
1793
+
1794
+ - A navigation is judged on **where it is going**; every other tool on **the tab it is on**, read from
1795
+ the browser at the moment of the call. Only a tool that genuinely takes a destination can be judged
1796
+ on an argument — `browser_navigate`, and `browser_perf_field_data`, which asks a public API about an
1797
+ address and drives no page at all. A `url` on anything else is a filter or a field the tool never
1798
+ declared, and it grants nothing: attaching `url` to a click does not move the click.
1799
+ - A refusal names the origin and the variable that refused it, so the agent can say why it stopped.
1800
+ - The tab that was checked is the tab that is driven. If the target changes between the check and the
1801
+ action — another call on the same server selecting a different browser — the call is refused with
1802
+ `ORIGIN_BLOCKED` and nothing is sent, rather than acting on a verdict about somewhere else. Retrying
1803
+ it is safe.
1804
+ - Denied origins are also blocked at the **network** layer via `declarativeNetRequest`, because
1805
+ otherwise `browser_eval` on an allowed page could just `fetch()` them. Network matching is by
1806
+ **domain** — subdomains included, scheme and port ignored — which is deliberately broader than the
1807
+ tool gate.
1808
+ - **Every browser you drive gets those rules, not just the first one.** They are installed before the
1809
+ first protected call on each browser, and again after that browser reconnects — a reconnect is the
1810
+ moment the server stops being able to prove the rules are still there, and one 2-second round-trip
1811
+ is cheaper than assuming. A browser that already has the current deny-list is not asked twice, so
1812
+ the cost is once per browser per connection, not once per call.
1813
+ - **Several agents on one browser keep each other's blocks.** Each server's deny-list is stored
1814
+ against that server, and the browser blocks the **union** of all of them. Two IDEs with different
1815
+ lists therefore both get what they asked for, rather than the second one silently deleting the
1816
+ first one's rules. The honest cost: a browser shared with another agent blocks more than your own
1817
+ list names. If the combined lists exceed **500 domains** the install fails rather than landing
1818
+ half of them, and the tool gate carries on alone.
1819
+ - **Read-only mode and the no-JavaScript switch are settled before any of that.** Neither depends on
1820
+ where the tab is, so neither waits for the page to be read — and neither can be skipped by the page
1821
+ failing to be read. Read-only holds when the lookup times out, when it answers with nonsense, and
1822
+ before any browser has connected at all.
1823
+ - **An allow-list refuses an unknown location; a deny-list alone does not.** If the page's address
1824
+ cannot be read, "I could not tell where this was going" resolves to a refusal only when an
1825
+ allow-list says where it must be. With just a deny-list configured there is nothing to be outside
1826
+ of, so the call proceeds. That is deliberate, and it is why read-only is decided first.
1827
+ - `browser_status` prints the active policy; the browser/tab-selection tools are never gated, or an
1828
+ agent being refused would have no way to ask why.
1829
+
1830
+ **What this does not do.** It decides which origins the agent may touch — never what it may do once
1831
+ it is on one. An agent allowed onto a site can still do anything a logged-in user could do there.
1832
+ Running its own JavaScript is the one exception, and only if you take it away with the switch below.
1833
+ Read-only mode is keyed off each tool's own read-only annotation, so opening and closing tabs is
1834
+ still permitted. And the network-layer half needs the rebuilt extension; without it the tool gate
1835
+ still applies but `fetch()` from page JS does not.
1836
+
1837
+ ### Forbidding JavaScript the agent wrote
1838
+
1839
+ Off by default. `AUTOMATE_BROWSER_NO_EVAL=1` refuses every call that would run source the agent
1840
+ composed, and leaves everything else alone:
1841
+
1842
+ | Refused | Still works |
1843
+ |---|---|
1844
+ | `browser_eval` — both the `expression` and the `function` form | click, type, hover, drag, fill a form, upload |
1845
+ | `browser_navigate {initScript}` — the rest of the same call is fine | snapshot, find, read text/HTML, screenshot, console, network |
1846
+
1847
+ That is the middle setting between full trust and read-only, and it is a different setting from
1848
+ either. Read-only stops the agent *changing* anything; this stops it *running* anything, including a
1849
+ read as harmless-looking as `browser_eval {expression:"document.title"}`. On a browser signed into a
1850
+ bank, "you may click the buttons on this page, but you may not execute code in it" is the distinction
1851
+ that matters, and until now there was no way to say it.
1852
+
1853
+ The refusal carries its own code, `EVAL_BLOCKED`, and names the variable and the fact that a person
1854
+ set it — so an agent reports a policy rather than a bug, and reaches for a snapshot instead of
1855
+ retrying. `browser_status` prints `no-eval` in its safety line.
1856
+
1857
+ **The honest limit — read this before relying on it.** This bars the two doors the *server* owns. It
1858
+ is not a sandbox around the page:
1859
+
1860
+ - **Advanced mode is untouched.** With the debugger attached, tools that drive CDP still do what they
1861
+ do; this switch does not audit their internals, it refuses the two arguments that carry your
1862
+ agent's source.
1863
+ - **It cannot stop the page's own JavaScript**, which runs on every page regardless. Nor
1864
+ `browser_page_tools`, which calls an action the page declared about itself — that is the page's
1865
+ script either way.
1866
+ - **It is a server-side setting, not a browser one.** Another agent on the same relay with the
1867
+ variable unset is unaffected; the switch belongs to the MCP server you set it on. Set it in the
1868
+ same `env` block as the origin lists.
1869
+ - **It costs zero tokens.** No schema changed, so a refused tool still advertises itself normally —
1870
+ an agent finds out by being refused, not by reading a shorter list.
1871
+
1872
+ ### What the agent did: the action audit log
1873
+
1874
+ Every tool call appends one line to `~/.automate-browser/automate-browser-audit.log` (or wherever
1875
+ `AUTOMATE_BROWSER_AUDIT_FILE` points): the tool, the browser and tab it targeted, the URL when there
1876
+ was one, whether it succeeded, and how long it took. `browser_status` names the file and prints the
1877
+ last few lines.
1878
+
1879
+ It is **on by default** because this server drives your real, logged-in browser: "what did it touch?"
1880
+ has to be answerable *after* something looks wrong, not only if you predicted you would want to ask.
1881
+ It is a local file and nothing is sent anywhere. `AUTOMATE_BROWSER_AUDIT=off` stops it.
1882
+
1883
+ Argument values that could be secrets — anything named like text, value, token, password, cookie,
1884
+ headers — are recorded as a **length**, never a value. Everything else is truncated. The trail rotates
1885
+ at 1 MB, keeping one predecessor.
1886
+
1887
+ It lives in `~/.automate-browser/`, **not** the OS temp directory. That is the whole point of the
1888
+ rotation: a trail you can still read tomorrow. Temp directories get swept — on the machine this was
1889
+ measured on, **2026-09-02, not one file in the temp folder was older than 24 hours** — so a log kept
1890
+ there answers "what did it touch?" only for questions you ask the same day. The relay's own connection
1891
+ log moved to the same folder for the same reason: `~/.automate-browser/automate-browser-relay.log`
1892
+ records every `browser connected` and `browser removed` with a timestamp, which is what tells you
1893
+ whether a browser that went quiet ever came back on its own.
1894
+
1895
+ `AUTOMATE_BROWSER_AUDIT_FILE` still points the audit trail anywhere you like. If the home directory
1896
+ cannot be created, both logs fall back to the temp directory rather than writing nothing.
1897
+
1898
+ ### Performance: this machine, and what real visitors get
1899
+
1900
+ `browser_perf_trace` records a trace and now **reports LCP / FCP / INP / CLS with a good /
1901
+ needs-improvement / poor rating** instead of handing over a file; `{action:'analyze', filePath}`
1902
+ re-reads a trace saved earlier without recording another. A metric the trace did not contain is
1903
+ reported as missing, never as zero.
1904
+
1905
+ `browser_perf_field_data {url}` answers the other half — what real Chrome users experienced at p75 —
1906
+ and touches no browser at all. It needs a free Chrome UX Report API key in `AUTOMATE_BROWSER_CRUX_KEY`
1907
+ and is inert without one. **It sends the URL you ask about to Google's public CrUX API**, and
1908
+ nothing else about you or the page goes with it. That is one of the only two outbound calls this
1909
+ server ever makes; the other is the daily version check, which sends nothing at all.
1910
+ → [Finding out your copy is out of date](#finding-out-your-copy-is-out-of-date)
1911
+
1912
+ ### Memory: is the page leaking?
1913
+
1914
+ `browser_perf_trace {action:'memory'}` samples `performance.memory.usedJSHeapSize` **every 500 ms**
1915
+ across a window and reports the start, the end, every reading, and a **least-squares trend in MB/s**.
1916
+ The trend is a fit rather than end-minus-start because the heap saws: one garbage collection
1917
+ mid-window drops it a long way, and a genuinely leaking page can finish the window *lower* than it
1918
+ started.
1919
+
1920
+ **The reply states the cadence it achieved, which is often not 500 ms.** Chrome throttles timers in a
1921
+ tab it is not drawing to roughly once a second, and your tab is a background tab by default — so the
1922
+ same window returns about half the samples. The trend is computed from the **real elapsed time** the
1923
+ page measured, so the MB/s figure stays right either way; only the sample count changes.
1924
+ `browser_switch_tab` is the one thing that changes it.
1925
+
1926
+ It is the one performance action that **needs no advanced mode** — no debugger attaches, no banner
1927
+ appears — so it is safe to run against a tab a person is looking at. `durationMs` accepts
1928
+ **1000-30000** (default **5000**) and an out-of-range value is **refused, not clamped**; for a longer
1929
+ watch, call it again between your own interactions. The 500 ms interval is deliberately not an
1930
+ argument: Chrome quantises the reading into coarse buckets, so sampling faster buys more points off
1931
+ the same staircase, not more resolution.
1932
+
1933
+ **The honest limit, and the reply says it every time: a rising heap is not proof of a leak.** It may
1934
+ be memory a collection has not reclaimed yet. And `performance.memory` is a **Chrome-family API** —
1935
+ in Firefox or Safari the call fails and says so. It is verified working on **Chrome and Edge**.
1936
+
1937
+ **There is no heap-snapshot action, and there cannot be one.** It was built, and Chrome refused it:
1938
+ `chrome.debugger` exposes a fixed allow-list of DevTools Protocol domains, and `HeapProfiler` is not
1939
+ on it — the CPU `Profiler` is, the heap one is not. `Target.attachToTarget` inherits the same filter,
1940
+ and no `chrome.*` API dumps a heap, so no extension can capture one by any route. To find out *what*
1941
+ grew, open **DevTools → Memory** yourself, take two snapshots and use the comparison view. This server
1942
+ tells you the heap is rising; a person still has to say why.
1943
+
1944
+ ### Accessibility: a floor, not a pass
1945
+
1946
+ `browser_issues {audit:'a11y'}` runs **axe-core 4.13.0** against the page you are on and returns the
1947
+ violations grouped by impact, **critical first**. Each rule reports the number of elements it matched,
1948
+ up to **five examples** each, and a link to the rule's own fix guidance. The reply names the axe
1949
+ version, so a report can be reproduced against the same rules later.
1950
+
1951
+ **Findings map back to snapshot refs.** Where an element already carries a ref from a
1952
+ `browser_snapshot`, the finding shows it — so you can click, read or fix that exact element with any
1953
+ other tool instead of parsing a CSS selector. Take the snapshot *first*: an audit run before any
1954
+ snapshot has nothing to map to, and shows selectors alone. A missing ref never hides a finding.
1955
+
1956
+ **The honest limit, and the reply says it every time: automated rules catch roughly a third of real
1957
+ accessibility barriers.** They check that attributes exist, not that they are *right* — nothing here
1958
+ can tell you whether alt text describes the image, whether the focus order makes sense, or whether a
1959
+ custom widget is actually operable by keyboard. A clean result means the automatable third found
1960
+ nothing. It is a floor, not a pass, and "0 violations" is not an accessibility claim.
1961
+
1962
+ Rules axe cannot decide alone are **counted, not listed** — contrast over a background image, say. The
1963
+ count appears at the end so you know how much was left undecided without the report filling up with
1964
+ things that are probably fine.
1965
+
1966
+ Findings are **paged, 20 rules per page, worst first** (`limit` changes the page size), and the footer
1967
+ names the exact next call. It
1968
+ runs in the extension's isolated world, so it leaves nothing behind on the page and works on sites
1969
+ whose content-security policy blocks injected script. No debugger, no banner. It adds about **151 KB**
1970
+ to the packed extension, which is the whole cost.
1971
+
1972
+ ### Driving a browser on another machine
1973
+
1974
+ The relay listens on `127.0.0.1` only. Set `AUTOMATE_BROWSER_RELAY_HOST` (with
1975
+ `AUTOMATE_BROWSER_TOKEN`) and it binds that address instead, so a browser on a *different* computer
1976
+ can connect — type that computer's address into the extension popup's "Editor on another computer"
1977
+ field and set the same token there.
1978
+
1979
+ **A non-loopback bind without a token is refused, on both sides** — the relay will not listen and the
1980
+ extension will not dial. `browser_status` warns whenever the relay is reachable beyond this machine.
1981
+
1982
+ ### Proxy control (whole browser)
1983
+
1984
+ `browser_proxy` routes traffic through a proxy — `{mode:'fixed_servers', server:'host:port'}`,
1985
+ `{mode:'pac_script', pacUrl:'…'}`, `direct`, `system`. Call it with no arguments to see what is in
1986
+ force, and `{clear:true}` to hand the setting back.
1987
+
1988
+ > ⚠ **This affects the whole browser, not just the automated tab.** Chrome has no per-tab proxy, so
1989
+ > the tabs you are working in are routed the same way until it is cleared. Every call that changes the
1990
+ > setting says so in its reply, and a change another extension outranks is reported as *not applied*
1991
+ > rather than as success.
1992
+
1993
+ ## Skills that ship with it
1994
+
1995
+ The tools say what *can* be done; the skill says what to do with them. One skill —
1996
+ **`automate-browser`** — ships inside the package. Its `SKILL.md` is the short part an agent always
1997
+ reads: the tab-ownership rules, the standard loop, and an index telling it which reference to open
1998
+ **at which lines**, so it never reads a whole file to answer one question.
1999
+
2000
+ **Installing it in Claude Code** (this repo is its own marketplace):
2001
+
2002
+ ```bash
2003
+ /plugin marketplace add /path/to/AutomateBrowser
2004
+ /plugin install automate-browser@automatebrowser
2005
+ ```
2006
+
2007
+ Or from a shell: `claude plugin marketplace add /path/to/AutomateBrowser` then
2008
+ `claude plugin install automate-browser@automatebrowser`. Verify a manifest change with
2009
+ `claude plugin validate .`.
2010
+
2011
+ **Working inside this repository, the skill does not load** — and that is correct, not a fault. Claude
2012
+ Code loads skills from `.claude/skills/` (the maintainer notes) and from *installed* plugins. A
2013
+ checkout is the plugin's **source**, not an installed copy of it.
2014
+
2015
+ | Reference | For |
2016
+ |---|---|
2017
+ | `tabs-and-multi-agent` | Which tab an agent may drive, focus, cleanup on release, and sharing one browser with other agents |
2018
+ | `page-interaction` | Refs rather than selectors, finding elements cheaply, filling a form in one call, waiting, stale refs |
2019
+ | `reading-and-extraction` | Getting structured data out — which read tool is cheapest, extracting with a function, pagination, big results to a file |
2020
+ | `sessions-and-state` | Working inside the user's existing session, cookies and storage, dialogs, uploads, and stopping cleanly for 2FA |
2021
+ | `capture-and-diagnostics` | Screenshots (including background tabs), console logs, the issues feed, network, performance, advanced mode |
2022
+ | `troubleshooting` | Nothing connected, calls that hang, actions that silently do nothing, and what each error code means |
2023
+ | `tool-reference` | All 46 tools with their arguments and the gotcha for each |
2024
+
2025
+ **The tool tables inside `tool-reference` are generated from the server's own schemas**, and
2026
+ `npm run docs:generate` fails if a tool exists with no entry — so the shipped guidance cannot quietly
2027
+ fall behind the code. `npm run verify:release` additionally asserts the `SKILL.md` and all seven
2028
+ references are actually inside the published tarball.
2029
+
2030
+ They live in [skills/](skills/), separate from the maintainer notes in `.claude/skills/` — those are
2031
+ about *this repository's internals* and are deliberately not shipped.
2032
+
2033
+ ## Development
2034
+
2035
+ ```bash
2036
+ npm run check # the gate CI runs — every starred command below, plus `npm audit --omit=dev`
2037
+ npm run contracts:check # * assert the extension's generated command types still match the schemas
2038
+ npm run typecheck # * tsc --noEmit
2039
+ npm run typecheck:tests # * the same over tests/
2040
+ npm run lint # * eslint .
2041
+ npm run format:check # * prettier --check .
2042
+ npm run test # * the unit suite — 410 tests over 34 files (node:test via tsx)
2043
+ npm run build # * build dist/index.js + dist/relay.js + dist/cli.js (tsup)
2044
+ npm run watch # rebuild on change
2045
+ npm run smoke # * connection smoke test (isolated ports, fake browsers + controllers)
2046
+ npm run inspector # run the server under the MCP Inspector
2047
+ npm run docs:generate # * rewrite the generated tables and counts in README + the shipped skill
2048
+ npm run verify:release # * assert the version matches everywhere and the tarball carries every file
2049
+ npm run tokens # * measure what the advertised schemas cost per request (all three profiles; name one for its table)
2050
+ npm run test:integration # drive a real Chrome (Puppeteer) through the live behaviours
2051
+ npm run test:live # the checks that used to say "needs a person" — real certs, real traces
2052
+ npm run memory:relay # profile the relay's heap across connection churn (its own CI job)
2053
+ npm run eval # score an agent against the scenarios in scripts/eval-scenarios.json
2054
+
2055
+ node scripts/incognito-check.mjs [--edge] # private-browsing behaviour, both settings states
2056
+ ```
2057
+
2058
+ `check` is the one to run before committing. It fails on a type error, a build error, a version that
2059
+ has drifted, a generated table that no longer matches the schemas, a broken connection path, or a
2060
+ dependency advisory — the same set CI enforces, so a green `check` locally is a green CI run.
2061
+
2062
+ The extension is built separately from [Chrome-extension/](Chrome-extension/) (`npm run build`,
2063
+ `npm run dev`, `npm run compile`). `src/` and `Chrome-extension/` are the sources of truth.
2064
+
2065
+ Before opening a pull request, read [CONTRIBUTING.md](CONTRIBUTING.md) — it covers the gates above,
2066
+ the token budget every tool schema is charged against, and the two separate sets of skills. What
2067
+ changed in each version is in [CHANGELOG.md](CHANGELOG.md). To report a vulnerability, do **not** open
2068
+ a public issue; see [SECURITY.md](SECURITY.md).
2069
+
2070
+ ### Watching the relay for leaks
2071
+
2072
+ Every process here dies with its client except one. The relay is a singleton that outlives every
2073
+ editor and is meant to run for days, so it is the only place a slow leak is both plausible and
2074
+ expensive. `npm run memory:relay` connects and disconnects waves of browsers and controllers, then
2075
+ measures the relay's heap at the point where it is back to zero peers — a correct relay returns to
2076
+ roughly where it started.
2077
+
2078
+ Measured on the 0.2.0 build: **0.25 MB of growth across 600 connections**, flat over the last five
2079
+ waves. CI fails the run at 2 MB, eight times the worst observed figure. The honest limit is that this
2080
+ watches the JS heap only, so a leak in native socket buffers would show up in RSS and could hide from
2081
+ it.
2082
+
2083
+ ### Troubleshooting
2084
+
2085
+ If tools return **"No connection to browser extension"**, check, in order: the extension popup shows
2086
+ a green dot; only one relay is live on `9009–9013`; and the client points at `node dist/index.js`
2087
+ (not a published npx package). Full triage:
2088
+ [.claude/skills/connection-debugging/SKILL.md](.claude/skills/connection-debugging/SKILL.md).
2089
+
2090
+ ## License
2091
+
2092
+ [Apache License 2.0](LICENSE). Contains portions © Microsoft Corporation and © Google Inc.