Let AI agents use your real, logged-in browser — in their own background tabs, while you keep browsing.
Website
---
## About
**AutomateBrowser** is an [MCP](https://modelcontextprotocol.io) server paired with a Chrome/Edge
browser extension. Instead of launching a fresh, sterile browser instance like most automation
tools, it drives **your actual browser tab** — with your logins, cookies, sessions, and fingerprint
intact — from AI clients such as Claude Code, Claude Desktop, Cursor, VS Code, and Windsurf.
- **MCP server** (`@automatebrowser/mcp`) — a stdio server that exposes 46 browser tools to your AI client.
- **Browser extension** (`Chrome-extension/`, v1.0.0) — a Manifest V3 extension that connects to the
server over a local WebSocket and runs the automation inside your tab.
- **Terminal CLI** (`automate-browser`) — the same tools from a shell, sharing the browser with
whatever IDEs are connected. See [Terminal CLI](#terminal-cli).
The two halves talk over a loopback WebSocket on `127.0.0.1:9009`. Nothing about your browsing
leaves your machine. The server makes exactly two outbound calls, both optional: a once-a-day
check for a newer release, which sends nothing about you and can be switched off
(→ [here](#finding-out-your-copy-is-out-of-date)), and the field-data tool, which is inert
unless you supply an API key.
## Why AutomateBrowser
- 🪟 **Works beside you, not instead of you** — the agent opens its **own tab in the background** and
works there. The tab you are reading never moves and your focus is never taken.
→ [The agent's own tab](#the-agents-own-tab)
- 🎯 **Or takes over any tab you point at** — name a tab you already have open by its title or
address, and the agent drives it where it sits, without bringing it to the front.
- 👥 **Several agents, one browser** — each editor works in its own tab of the same browser at the
same time. → [Sharing a browser with another agent](#sharing-a-browser-with-another-agent)
- 🕶️ **A stranger's view on demand** — a private window with none of your logins, without signing
out. → [A clean, logged-out session on demand](#a-clean-logged-out-session-on-demand)
- 🧹 **Cleans up after itself** — closes the tabs it opened, never one of yours.
- ⚡ **Fast** — automation runs locally; no remote round-trips.
- 🔒 **Private** — your browsing stays on your device.
- 👤 **Logged in** — uses your real profile, so you stay signed into every service.
- 🥷 **Stealthy** — your genuine browser fingerprint sidesteps most bot-detection and CAPTCHAs.
- 🧩 **Debugger-free by default** — interactions go through `chrome.scripting`, so there is **no
"started debugging this browser" banner**. CDP is per-tab and used only where nothing else can do
the job: when you ask for it, for a full-page screenshot, and for a screenshot of a background tab,
where it is the only way to get the right pixels without stealing your focus. It detaches again
straight after. → [What can take your focus](#what-can-take-your-focus)
- 🪶 **Token-efficient** — interactions return short confirmations; full accessibility snapshots are
fetched only when an agent asks for fresh element refs.
- 🤝 **Multi-IDE sharing & per-tab control** — a single shared relay lets every IDE see and drive the
same browsers. Ownership is **per tab**, so two agents (e.g. two IDEs) can drive two *different* tabs
of the same browser at the same time; only same-tab access is serialized, via a soft-claim with a
named handoff.
## What it can do
A tour of the whole surface. Each line links to the detail further down; the full per-tool tables are
under [Tools](#tools).
**Drive the page**
- Navigate, reload (including a **hard reload that bypasses the cache**), back, forward, and wait for
a load or for a condition to become true. A navigation reports itself finished only when **the
navigation you asked for** finished — never because the page you were leaving had already loaded.
→ [What `settled` means on a navigation](#what-settled-means-on-a-navigation)
- A navigation that **did not happen says so**, in the snapshot reply too, instead of handing you the
previous page under a success message.
→ [A navigation that did not happen says so](#a-navigation-that-did-not-happen-says-so)
- A page that **failed to load** fails the call with Chrome's own error, instead of an "ok" on Chrome's
error page. → [A page that failed to load says so](#a-page-that-failed-to-load-says-so)
- Click, type, hover, drag, press keys, select options, scroll, and **fill a whole form in one call**.
Checkboxes and radios take a **boolean and nothing else** — a value that is neither is refused rather
than guessed at. → [Checkboxes and radios take a boolean](#checkboxes-and-radios-take-a-boolean-and-nothing-else)
- A form that **spans an embedded widget and the page around it** fills in one call, in the order you
wrote it. → [Filling a form that spans frames](#filling-a-form-that-spans-frames)
- Click by **element** or by **coordinate**, for canvases, maps and PDF viewers that a snapshot cannot
name. → [Clicking something a snapshot cannot name](#clicking-something-a-snapshot-cannot-name)
- A click **the page ignored says so** — "Clicked" alone used to be all you got from a page that acts
only on a real click. → [A click the page ignored is reported](#a-click-the-page-ignored-is-reported)
- A hover **says what it did**: by default page scripts see the mouse but CSS `:hover` styles do not
apply; with advanced mode on it is a real mouse move and they do.
→ [Hover: what the page sees, and what CSS sees](#hover-what-the-page-sees-and-what-css-sees)
- Interactions report the same result **whether or not the debugger is attached** — same fields, same
waiting — and only advanced mode itself makes them real input; reading a response body does not.
→ [Advanced mode does not change what interactions return](#advanced-mode-does-not-change-what-interactions-return)
- Upload files, and answer `alert`/`confirm`/`prompt` dialogs — including the native "Leave site?"
prompt that page JavaScript cannot even see.
- Every interaction **waits for the element to be genuinely ready** — visible, enabled, still, and not
covered by an overlay — then waits for the page to settle before returning.
→ [Actionability](#actionability-interactions-wait-for-the-element-to-be-ready)
- Choose per call what **"finished" means** — return at once, wait for load, or wait for the network
to go quiet — and cap how long that wait may take. By default a navigation also waits for the page
to **finish drawing**, so a script-built page like YouTube is not read as an empty shell.
→ [Deciding when an action is finished](#deciding-when-an-action-is-finished)
- **Wait for a condition, not a duration** — an element, the URL, or text, where text can be a
pattern (`/Result: \d+/` skips the "Result: n/a" placeholder) or something to wait to **disappear**.
→ [Waiting for a condition, not a duration](#waiting-for-a-condition-not-a-duration)
- Run your own JavaScript against elements you already found, with a **time limit you choose** for
slow page work. → [Running your own JavaScript](#running-your-own-javascript-against-elements-you-already-found)
**Read the page**
- Accessibility snapshots that stay **lean by default**, with a full mode and a write-to-file mode for
pages that would otherwise fill the context window. An icon-only button still gets a name — its
tooltip, its image's alt text or its `#id` — so two of them can be told apart.
→ [Snapshots](#snapshots-lean-full-or-to-a-file)
- Clean text or Markdown extraction, raw HTML, and a `find` that returns just the elements you asked
for instead of a whole tree — the link that says "Sign in", not the page wrapped around it, with
the link's address, a date's `datetime` or a `` tag's value on the same line.
→ [Reading a page without a snapshot](#reading-a-page-without-a-snapshot)
- **Element refs survive a re-render**, and recover themselves once if the page swapped the element
out underneath you. → [Element refs survive a re-render](#element-refs-survive-a-re-render)
- Reads reach **inside iframes, including cross-origin ones**, with empty frames dropped and a cap so
an ad-heavy page stays readable. → [Frames](#frames-including-cross-origin-ones)
- Ask a page for the **actions it declares about itself**, and call one directly instead of hunting
for the controls. Almost no site declares any yet, so the honest answer is usually "none".
→ [Tools the page declares about itself](#tools-the-page-declares-about-itself)
**See what went wrong**
- Console logs, **uncaught errors with their stacks**, and service-worker lifecycle in one place.
- A **browser issues feed** for the problems that produce no console error at all: content blocked by
a security policy, deprecated APIs, browser interventions, and failed or 4xx/5xx requests. This is
usually the answer when something silently did nothing.
- Network request list, and the **response body** for any single request, addressable when the same
URL was fetched more than once.
- Both logs **survive the browser shutting the extension down** to save memory, and carry over the
previous pages so a redirect can still be debugged. → [Log durability](#log-durability)
- Both lists are **paged, newest first**, so a chatty page cannot spend your whole context in one
call, and a footer names the exact call for the page before it.
→ [Paged logs](#the-console-and-network-lists-are-paged-newest-first)
- A short **footer** on each action telling you how many new console errors and browser issues that
action caused. → [Console-error and issue footers](#console-error-and-issue-footers)
- An **accessibility audit** of the page you are on, grouped worst-first and mapped back to the refs
you can act on. It catches about a third of real barriers, and says so every time.
→ [Accessibility: a floor, not a pass](#accessibility-a-floor-not-a-pass)
- Failures carry a **typed code and the tool that fixes them**.
→ [Errors name the next step](#errors-name-the-next-step-not-just-the-problem)
- A result says **which of five things happened** — refused, done, partly done, failed, or *nobody
knows* — instead of one yes/no flag covering all of them. A form that filled two of three fields
says so, and an action whose reply was lost says it **may already have happened** rather than
inviting you to do it twice.
→ [Five outcomes, not two](#five-outcomes-not-two)
- When the connection is down, `browser_status` says **whether it is coming back** — retrying, waiting
for a browser, or stopped for good — instead of one line covering all three.
→ [`browser_status` says whether it is coming back](#browser_status-says-whether-it-is-coming-back)
**Measure speed**
- Record a performance trace and get **Core Web Vitals** — LCP, FCP, CLS, INP — rated against Google's
thresholds, plus the long tasks that blocked the main thread. One call profiles a whole page load.
A page Chrome is not drawing is refused at once rather than recorded for nothing.
→ [A hidden page is refused, not measured](#a-hidden-page-is-refused-not-measured)
- **Find out where the LCP time went**, not just how long it was: the server, discovering the image,
downloading it, or painting it — with the cause and the fix named in a line each, and the
render-blocking resources listed. → [Where the LCP time actually went](#where-the-lcp-time-actually-went)
- Re-analyse a saved trace later without recording again.
- **Watch the JS heap** over a window and get the trend, with no debugger and no banner.
→ [Memory: is the page leaking?](#memory-is-the-page-leaking)
- Ask what **real visitors** experience, from Google's public field data, rather than just this
machine. → [Performance](#performance-this-machine-and-what-real-visitors-get)
**Capture**
- Screenshots of the viewport, the **full scrollable page**, or **one element**, as PNG/JPEG/WebP, and
straight to a file so the image never costs you a context window.
→ [Screenshots](#screenshots-that-dont-cost-a-context-window)
- A **strip of stills** on a timer, to show a person what happened instead of describing it. Stills,
not video. → [A strip of stills](#a-strip-of-stills-for-showing-a-person-what-happened)
- An inline screenshot is **held under 1536 px wide**, so a 2K or 4K display does not quietly cost
three times the context for the same picture. → [The size ceiling](#the-size-ceiling-on-inline-screenshots)
**Pretend to be somewhere or something else**
- Fake a **location**, a user agent, extra headers, or a colour scheme.
- Emulate a **phone viewport with touch**, a throttled network, or a slow CPU.
→ [Emulation](#emulation-location-headers-and-the-responsive-testing-set)
- Run your own script **before the page's own code**, to stub a clock or mock a fetch.
→ [Reload, and the two options that need the debugger](#reload-and-the-two-options-that-need-the-debugger)
**Manage state**
- Cookies, localStorage and sessionStorage, downloads (with their **path on disk**, and an option to
wait for a transfer to finish), and tabs.
- **Stored values come back hidden by default** — an agent reading cookies, storage or a captured
request's headers gets the names, not your session tokens, until it explicitly asks for them.
→ [Cookie, storage and header values are hidden by default](#cookie-storage-and-header-values-are-hidden-by-default)
**Work alongside you, and alongside other agents**
- **The agent works in its own tab, never yours.** It opens one in the background on first use and
leaves the tab you are reading completely alone. → [The agent's own tab](#the-agents-own-tab)
- **That holds through a disconnect too.** If the browser drops mid-action, the automatic retry goes
back to the same browser and the same tab, or refuses — it never falls through to the tab you are
looking at. → [A retry never changes browser or tab](#a-retry-never-changes-browser-or-tab)
- **The connection repairs itself.** Lose the relay, or start up before it is ready, and the server
keeps dialling back with a growing delay instead of sitting disconnected until you act.
→ [The link to the relay repairs itself](#the-link-to-the-relay-repairs-itself)
- **Nothing steals your focus** except the one tool whose job is to show you something.
→ [What can take your focus](#what-can-take-your-focus)
- **"Show me" works on a minimised browser.** Switching to a tab brings the window back and says
whether the page is really on screen. → [Switching restores a minimised window](#switching-restores-a-minimised-window)
- **A clean, logged-out session on demand**, in a private window, for checking what a first-time
visitor sees without logging out of anything.
→ [A clean, logged-out session on demand](#a-clean-logged-out-session-on-demand)
- **Per-tab ownership**, so two editors can drive two tabs of the same browser at once, with a named
handoff when they want the same one.
→ [Sharing a browser with another agent](#sharing-a-browser-with-another-agent)
- One shared relay across every IDE, and a **terminal CLI** with `--json` for scripting.
→ [Terminal CLI](#terminal-cli)
- **Tells you when your copy is out of date** — one line on startup, at most one registry
lookup a day, and an env var that stops it entirely.
→ [Finding out your copy is out of date](#finding-out-your-copy-is-out-of-date)
**Keep it safe**
- A **file-path sandbox** on every write, and a way to name your own folders when your editor does
not. → [File-path sandbox](#file-path-sandbox)
- **Certificate checking is never switched off.** The one switch that claimed to do it never worked
on any build and was removed; the section names the two routes that do.
→ [Loading a site with a bad certificate](#loading-a-site-with-a-bad-certificate)
- Only **http(s) pages** can be navigated to or opened in a new tab — a `javascript:`, `file:` or
`chrome://` URL is refused by name. → [Where a navigation may go](#where-a-navigation-may-go)
- The local socket **only accepts the extension**, and hangs up on a peer that floods it or sends
nonsense. → [What can reach the local socket](#what-can-reach-the-local-socket)
- The optional shared secret is **never sent over the socket**; both ends sign a challenge with it
instead. → [Driving a browser on another machine](#driving-a-browser-on-another-machine)
- **The server leaves when your editor does**, so a closed window never strands a phantom agent in
the roster. → [The server leaves when your editor does](#the-server-leaves-when-your-editor-does)
- **Allow / deny lists of sites** the agent may touch, enforced at the network layer as well as the
tool layer, plus a global read-only mode.
→ [Which sites the agent may touch](#which-sites-the-agent-may-touch)
- A switch that lets the agent **click and read a logged-in session but never run code you did not
write**. → [Forbidding JavaScript the agent wrote](#forbidding-javascript-the-agent-wrote)
- An **audit log** of every call with its target, with secrets recorded as a length and never a value.
→ [What the agent did](#what-the-agent-did-the-action-audit-log)
- **Fewer tools on demand** — serve a `core` or `slim` profile so the schemas cost less of every
request. → [Configuration](#configuration)
**Reach further**
- Drive a browser **on another machine**, with a mandatory token off loopback.
→ [Driving a browser on another machine](#driving-a-browser-on-another-machine)
- Set a **proxy** for the whole browser. → [Proxy control](#proxy-control-whole-browser)
## Architecture
AutomateBrowser uses a **singleton relay** model. One lightweight relay process owns the WebSocket
port; each IDE's MCP server connects to it as a *controller*, and each browser extension connects as
a *browser*. The relay routes tool calls between them and enforces one-driver-**per-tab** leases — so two agents can
drive two different tabs of one browser at once.
```
IDE 1 (Claude Code) IDE 2 (Cursor) IDE 3 (VS Code)
node dist/index.js node dist/index.js node dist/index.js
(controller) (controller) (controller)
\ | /
\ | /
+-----------------> 127.0.0.1:9009 <----------------+
Relay (dist/relay.js)
|
+----------------------+----------------------+
| | |
Chrome ext Edge ext Brave ext
(your tab) (your tab) (your tab)
```
- **Relay** (`dist/relay.js`) — a single long-lived WebSocket host bound to the lowest free port in
`9009–9013`. Spawned on demand, idle-exits ~5 min after the last peer leaves. It speaks the browser
wire protocol byte-for-byte (the extension can't tell it apart from a direct host) and holds the
live browser registry.
- **Controller** (`dist/index.js`) — the MCP server an IDE runs. It does **not** bind a port; it dials
the relay, registers with a human name, and forwards resolved tool calls. Any number of controllers
can run at once and share the same browsers.
- **Browser** (the extension) — connects on startup (no "Connect" button), races ports `9009–9013`,
caches the last-good port, and executes commands in your tab.
> **Soft claims (per tab):** the first action on a tab auto-claims *that tab* for 60 s (renewed on every
> action), so two agents can drive two different tabs of one browser concurrently. With no explicit
> `browser_select_tab`, a drive defaults to the browser's **focused tab** (not the whole browser), so two
> agents working on two different tabs coexist automatically; an agent keeps its own tab instead of
> following your focus onto a tab another agent drives. (A whole-browser lease is taken only as a last
> resort when no tab id is known, e.g. a `chrome://` active tab.) An agent targeting a tab (or browser)
> another agent holds is politely refused **by name** and can take over with `browser_force_claim`. Claims
> free on idle, on `browser_release_client`, or on disconnect.
## Repository layout
```
AutomateBrowser/
├── src/ # MCP server — source of truth
│ ├── index.ts # stdio MCP entry; composes the 46 tools
│ ├── cli.ts # `automate-browser` terminal entry (same tools, one shot)
│ ├── server.ts # MCP Server + ListTools/CallTool handlers
│ ├── context.ts # Relay-backed facade tools call into
│ ├── relay-link.ts # Controller <-> relay socket
│ ├── relay-ensure.ts # Discover/spawn the relay
│ ├── relay/ # The relay host (index, relay, control, browsers, ...)
│ ├── tools/ # The 46 MCP tools (snapshot, content, forms, tabs, ...)
│ └── vendor/ # Vendored former-monorepo deps (config, messaging, types)
├── Chrome-extension/ # MV3 browser extension (WXT) — source of truth
│ ├── wxt.config.ts # Manifest config + permissions
│ ├── entrypoints/ # background service worker, content script, popup
│ ├── lib/ # connection, tab targeting, identity, automation engine
│ └── .output/chrome-mv3/ # BUILD OUTPUT — load THIS folder unpacked
├── dist/ # Built bundle (index.js + relay.js + cli.js)
├── scripts/connection-smoke.cjs # Connection smoke test
├── server.json # Distribution manifests — MCP registry,
├── gemini-extension.json # Gemini CLI, and the Agent Plugins pair.
├── plugin.json # All version-gated against package.json
├── mcp.json # by verify:release and by npm test
└── skills/automate-browser/ # The agent skill shipped with the package
```
## Quick start
**Two halves, both one step.** The *server* installs from npm or as a plugin, and the *extension*
installs from the [Chrome Web Store](https://chromewebstore.google.com/detail/automatebrowser/lcpeddejaghbleiejgdjekobffadfkba).
### 1. Install the server
**Claude Code** — one install, and it brings the `automate-browser` skill with it:
```bash
/plugin marketplace add farazpawle/automatebrowser
/plugin install automate-browser@automatebrowser
```
> Already have a hand-written `automatebrowser` entry? **Remove it first** — the plugin registers
> the server itself, and two entries in one client means two controllers and duplicate tools.
**Claude Desktop** — download
[`automatebrowser.mcpb`](https://github.com/farazpawle/automatebrowser/releases/latest/download/automatebrowser.mcpb)
(always the latest release; also on the [website](https://farazpawle.github.io/automatebrowser/#download))
and open it, or drag it onto **Settings → Extensions**. The bundle carries its own dependencies, so
nothing else installs first. It is the server only: the skill is the separate
`automate-browser-skill.zip` on the same release (see [Skills that ship with it](#skills-that-ship-with-it)).
**Any other editor** — register the published package as a stdio server. The command it runs is:
```bash
npx -y --package @automatebrowser/mcp mcp-server-automatebrowser
```
`--package` is not optional: the package ships **two** bins and neither is named `mcp`, so a bare
`npx @automatebrowser/mcp` cannot choose one and fails in a non-TTY MCP client. Claude Code will
write the entry for you — `claude mcp add automatebrowser -- npx -y --package @automatebrowser/mcp
mcp-server-automatebrowser` — while other clients want it as JSON: step 3 has the shape.
Neither route gives you a browser yet. That is step 2.
### 2. Add the browser extension
**[Add AutomateBrowser from the Chrome Web Store](https://chromewebstore.google.com/detail/automatebrowser/lcpeddejaghbleiejgdjekobffadfkba)**
— Chrome or Edge, v137+. In Edge, allow extensions from other stores when it asks. It connects on
its own; there is no button to press.
**Or build it yourself** — only if you are changing the extension:
```bash
cd Chrome-extension
npm install
npm run build # WXT build -> Chrome-extension/.output/chrome-mv3/
```
Then in Chrome or Edge (v137+):
1. Open `chrome://extensions` (or `edge://extensions`).
2. Turn on **Developer mode** (top-right).
3. Click **Load unpacked**.
4. Select the build output folder:
`Chrome-extension/.output/chrome-mv3/`
A card titled **"AutomateBrowser"** appears and connects automatically — no button to press. Chrome
gives an unpacked extension a **random id per profile**, so yours will not match anyone else's and
will change if you remove and re-add it; read it off the card when you need it. Click the
toolbar icon (or **Alt+J**) to open the popup, where you can pin a specific tab, set an optional
label, see the live connection status, and see **every agent connected to the server** — each listed
by name and tagged with the tab it's driving (or *idle* when it holds no active tab). The list is the
full roster, so a connected agent stays visible even between actions (it doesn't vanish when its lease
lapses), and you can tell at a glance that Claude Code, Codex, and Copilot are all connected — and who
is working in which tab.
> Point **Load unpacked** at `.output/chrome-mv3/` — **not** at the `Chrome-extension/` source root.
> Chrome can only load the compiled output.
> **Updating from an earlier build re-prompts you.** This release adds two permissions —
> `downloads` (so `browser_downloads` can report a file's final path) and `declarativeNetRequest`
> (so a denied origin is blocked at the network layer, not only at the tool). Chrome disables an
> extension until new permissions are approved, so re-enable it once after updating. Both ship
> together on purpose: one re-approval, not two.
### 3. Build from source — only if you are changing the server
Step 1 already gave you a working server. This is the path for developing against your own
checkout, and for running several editors off one build rather than off the registry.
```bash
# from the repo root
npm install
npm run build # tsup -> dist/index.js (server) + dist/relay.js (relay) + dist/cli.js (terminal client)
```
The server vendors its former monorepo dependencies under [src/vendor/](src/vendor/), so it builds
standalone — no external workspace required.
**Then point your client at that build** (`node dist/index.js`, absolute path) instead of at npx. If
several editors drive one browser they must all launch the *same* build: a published npx entry in one
editor and a local build in another puts two different builds on one relay. Example (Claude Code / Claude
Desktop / Cursor `mcpServers`, VS Code uses `servers`); on Windows, escape the backslashes:
```json
{
"mcpServers": {
"automatebrowser": {
"command": "node",
"args": ["C:\\path\\to\\AutomateBrowser\\dist\\index.js"],
"type": "stdio",
"env": {
"AUTOMATE_BROWSER_CLIENT_NAME": "Claude"
}
}
}
}
```
Or via the Claude Code CLI:
```bash
claude mcp add automatebrowser -- node "/absolute/path/to/AutomateBrowser/dist/index.js"
```
Restart / reconnect the client after editing config. You don't configure a port — controllers
auto-discover the relay.
#### Installing it from a manifest instead
Five manifests in this repository describe the same server to five listing formats, so a client that
speaks one of them can install it without you writing any JSON:
| Manifest | Platform | How it is installed |
|---|---|---|
| `server.json` | The MCP registry | Published with the package; clients that browse the registry find it there |
| `gemini-extension.json` | Gemini CLI | `gemini extensions install https://github.com/farazpawle/automatebrowser` |
| `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 |
| `.claude-plugin/plugin.json` + `marketplace.json` | Claude Code | `/plugin marketplace add` — see [Skills that ship with it](#skills-that-ship-with-it) |
| `mcpb/manifest.json` | Claude Desktop | Not installed from the repository: it is packed with `dist/` and production `node_modules` into the `.mcpb` attached to each [release](https://github.com/farazpawle/automatebrowser/releases) |
**The first four work as of `1.0.0`** (published 2026-09-19). Three of them launch
`npx @automatebrowser/mcp@`, which resolved to nothing until that day and now resolves to this
build — a **controller** that joins the relay exactly like a local `node dist/index.js` entry. The old
warning that npx would partition the relay described a self-hosting build that this package has never
served, and it is gone.
**Only one route gives an agent both halves.** The MCP registry entry and a hand-written config
install the *tools* only — `skills/` is inside the published package, but an MCP client does not read
skills out of a dependency; only an installed **plugin** is searched. The Claude plugin pair and any
Agent Plugins 1.0.0 client are the exception: each registers the server *and* carries the skill, so one
install is the whole thing. See [Skills that ship with it](#skills-that-ship-with-it).
All six version claims across those files are gated: `npm run verify:release` and `npm test` both fail
if any one of them disagrees with `package.json`, so a listing cannot quietly advertise a version that
was never released.
## Tools
All 46 tools are prefixed `browser_`. Interactions are **lean by default** — they return a short
confirmation, not a snapshot. Call `browser_snapshot` to get element `ref`s, then pass `{ element, ref }`
to the interaction tools (a raw CSS selector is not accepted).
The navigating and interacting tools also take an optional **`timeout`** (milliseconds) so an agent can
give up early instead of waiting out the server default; `0` or omitting it uses the default. Calls from
one client run **one at a time, in the order they were issued**, so a click can never overtake the
navigation that was meant to precede it. Different clients are not serialized against each other — that
is what the per-tab claim is for.
**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.
### Navigation & history
| Tool | Description |
|------|-------------|
| `browser_navigate` | Navigate to a URL in YOUR OWN tab, opened in the background if you have none |
| `browser_go_back` | Go back to the previous page |
| `browser_go_forward` | Go forward to the next page |
### Snapshot & interaction
| Tool | Description |
|------|-------------|
| `browser_snapshot` | Capture accessibility snapshot of the current page |
| `browser_click` | Click an element by ref, or a viewport point by x/y |
| `browser_hover` | Hover over element on page |
| `browser_type` | Type text into editable element |
| `browser_select_option` | Select an option in a dropdown |
| `browser_drag` | Perform drag and drop between two elements |
### Input & timing
| Tool | Description |
|------|-------------|
| `browser_press_key` | Press a key or modifier combo (e.g. Enter, Tab, "Control+A", "Shift+Tab") on the focused element |
| `browser_wait` | Wait for a specified time in seconds |
| `browser_wait_for` | Wait for a page condition to become true (element appears/disappears, text appears, URL changes) |
### Reading content
| Tool | Description |
|------|-------------|
| `browser_read_page` | Read the page's main content as clean text or Markdown (strips nav/scripts/styles) |
| `browser_get_html` | Get the raw outerHTML of the page (or of a specific element by `ref`) |
| `browser_find` | Find elements by text, role, and/or CSS selector and return refs WITHOUT a full snapshot |
### Page-declared tools
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.
| Tool | Description |
|------|-------------|
| `browser_page_tools` | List and call actions a page declares about itself (WebMCP) |
### Forms & scrolling
| Tool | Description |
|------|-------------|
| `browser_fill_form` | Fill multiple form fields (inputs, textareas, selects, checkboxes, radios, contenteditable) in ONE call |
| `browser_clear` | Clear the value of an input, textarea, or contenteditable element by `ref` |
| `browser_scroll` | Scroll the page or an element |
### State: cookies, storage, network, downloads, dialogs
| Tool | Description |
|------|-------------|
| `browser_get_cookies` | List cookies for the URL of the tab you are driving (optionally filter by `name`) |
| `browser_set_cookie` | Set (create/overwrite) a cookie on the URL of the tab you are driving |
| `browser_storage` | Read or write the page's localStorage/sessionStorage |
| `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 |
| `browser_handle_dialog` | Control JS dialogs (alert/confirm/prompt) |
| `browser_downloads` | Recent downloads: final path on disk, URL, mime, size, state |
| `browser_proxy` | Route the browser through a proxy |
### Performance
`browser_perf_trace` measures THIS machine on THIS run. Recording attaches the debugger itself (banner) and detaches it on stop unless advanced mode was already on; `action: "memory"` 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.
| Tool | Description |
|------|-------------|
| `browser_perf_field_data` | Real-user Core Web Vitals (p75 LCP/INP/CLS/FCP/TTFB) for a URL, from Google's Chrome UX Report |
### Capture & evaluation
| Tool | Description |
|------|-------------|
| `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 |
| `browser_get_console_logs` | Console logs, uncaught errors with stacks, and service-worker lifecycle (register/state/messages) |
| `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 |
| `browser_eval` | Evaluate JavaScript in the tab you are driving and return the result |
### Tabs
| Tool | Description |
|------|-------------|
| `browser_list_tabs` | List the connected browser's open tabs |
| `browser_new_tab` | Open a new tab IN THE BACKGROUND and drive it — no focus stealing |
| `browser_switch_tab` | STEALS THE USER'S FOCUS: brings a tab to the front and drives it, by `tabId` (preferred) or `index` |
| `browser_select_tab` | TAKE OVER a tab the user already has open, WITHOUT focusing it — the tool for "pick up the testing I started" |
| `browser_close_tab` | Close a tab by `tabId` or `index` |
### Multi-IDE / clients
| Tool | Description |
|------|-------------|
| `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 |
| `browser_select_client` | Choose which connected browser your subsequent tools act on |
| `browser_force_claim` | Forcibly take over a browser that another agent is currently driving, and make it active for your tools |
| `browser_release_client` | Release your claim on the browser you are currently driving so another agent can take it |
| `browser_status` | Diagnostics for the AutomateBrowser relay |
### Advanced (opt-in CDP)
Attach the Chrome debugger only when you need full-fidelity input or network bodies. Enable with
`browser_advanced_mode` first (a perf trace attaches by itself); a debugging banner shows only
while it's attached.
| Tool | Description |
|------|-------------|
| `browser_advanced_mode` | Enable/disable opt-in debugger (CDP) mode for the tab you are driving |
| `browser_upload_file` | Set files on a file input (real upload) |
| `browser_get_network_request` | Get a network request's response BODY, status and headers by URL substring |
| `browser_perf_trace` | Record a performance trace (attaches the debugger while recording) |
| `browser_emulate` | Emulate location, headers, colour scheme, viewport, user agent, network or CPU |
## Terminal CLI
`automate-browser` runs any one of the tools above from a shell. It joins the relay exactly like an
IDE does, so it drives the **same** browser your editors are driving and appears in their
`browser_status` peer list as `cli`.
```bash
npx -p @automatebrowser/mcp automate-browser # or `npm link`, from a checkout
# (-p is required: the package ships two bins, so a
# bare `npx @automatebrowser/mcp` cannot pick one)
automate-browser status # who is connected, which relay, which browsers
automate-browser navigate https://example.com
automate-browser read-page --args '{"format":"markdown"}'
automate-browser eval 'document.title'
automate-browser screenshot # writes a PNG to the temp dir and prints the path
automate-browser tabs --json # machine-readable, for scripting
```
Aliases cover the common cases (`navigate`, `read-page`, `snapshot`, `screenshot`, `status`, `eval`,
`tabs`); anything else is called by tool name, with arguments as JSON:
```bash
automate-browser browser_click --args '{"element":"Sign in button","ref":"e7k2f"}'
```
The tool name may be given with or without the `browser_` prefix. `--json` prints the whole result
(including `structuredContent`) instead of the human-readable text. Exit status is `0` on success and
`1` on any failure, so it composes in scripts. A failure prints the **same text an agent gets** — the
`CODE: message` line and the `Recover: call ` line — on stderr; it used to print the bare
sentence, so the terminal was the one caller that never saw the code. Unlike the MCP server the CLI always offers every tool,
regardless of `AUTOMATE_BROWSER_TOOLS` — profiles exist to save an agent's tokens, and a terminal
pays none.
## Configuration
All settings are environment variables on the **server** side (set them in the MCP client's `env`):
| Variable | Purpose | Default |
|----------|---------|---------|
| `AUTOMATE_BROWSER_CLIENT_NAME` | Human name for this IDE in the relay roster | `mcp-` (CLI: `cli`) |
| `AUTOMATE_BROWSER_TOKEN` | Optional auth token (set the same value in the extension popup) | unset |
| `AUTOMATE_BROWSER_TOOLS` | Tool profile (`full`, `core`, `slim`) or a comma-separated category list | `full` |
| `AUTOMATE_BROWSER_CONNECT_WAIT_MS` | How long a tool waits for a browser to appear | `30000` |
| `AUTOMATE_BROWSER_LEASE_TTL_MS` | Soft-claim lease duration | `60000` |
| `AUTOMATE_BROWSER_INSTANCE_ID` | Identity used to replace this agent's own previous roster entry when it restarts | derived from parent process + folder |
| `AUTOMATE_BROWSER_CONTROLLER_STALE_MS` | Drop an agent that has sent nothing for this long | `45000` |
| `AUTOMATE_BROWSER_WS_PORT_RANGE` | Port scan range (e.g. `9109-9116` to isolate) | `9009-9013` |
| `AUTOMATE_BROWSER_RELAY_IDLE_MS` | Idle time before the relay exits | `300000` |
| `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` |
| `AUTOMATE_BROWSER_RELAY_FOREGROUND` | `1` also tees the relay's log to stderr instead of the file only | unset |
| `AUTOMATE_BROWSER_SNAPSHOT_EACH_ACTION` | Bundle a snapshot after every interaction | unset |
| `AUTOMATE_BROWSER_STRUCTURED` | `1` also sends each result's `structuredContent` and lists output schemas, for scripts. Leave off for agents: Claude Code then shows only that data and hides the written reply | unset (written reply only) |
| `AUTOMATE_BROWSER_DELTA_FOOTER` | `off` disables the console-error footer below | on |
| `AUTOMATE_BROWSER_DELTA_FOOTER_MS` | Hard ceiling on the footer's console probe | `2000` |
| `AUTOMATE_BROWSER_NAV_CONFIRM_MS` | How long a navigation that reported no movement is re-checked before it is called a failure | `2000` |
| `AUTOMATE_BROWSER_SCREENSHOT_MAX_WIDTH` | Widest an **inline** screenshot may come back. A `filePath` capture is never downscaled; `0` = off | `1536` |
| `AUTOMATE_BROWSER_SCREENSHOT_MAX_HEIGHT` | Tallest an **inline** screenshot may come back, aspect ratio preserved; `0` = off | `4096` |
| `AUTOMATE_BROWSER_ACTIONABILITY` | `off` disables the pre-action checks and post-action settle below | on |
| `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) |
| `AUTOMATE_BROWSER_WS_RATE_MAX` | Frames one socket may send per window before it is closed | `120` |
| `AUTOMATE_BROWSER_WS_RATE_WINDOW_MS` | The window that ceiling is counted over | `1000` |
| `AUTOMATE_BROWSER_EXTENSION_ORIGINS` | Comma-separated browser-extension origins allowed to open a socket | any `chrome-extension://` origin |
| `AUTOMATE_BROWSER_LOG_LEVEL` | `debug`, `info`, `warn` or `error`. Diagnostics go to stderr, never stdout | `info` |
| `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) |
| `AUTOMATE_BROWSER_ALLOW_UNRESTRICTED_PATHS` | `1` disables the file-path sandbox below | unset |
| `AUTOMATE_BROWSER_ALLOW_ORIGINS` | Comma-separated origin patterns; when set, **only** these may be driven | unset (unrestricted) |
| `AUTOMATE_BROWSER_DENY_ORIGINS` | Origins that may never be driven, also blocked at the network layer | unset |
| `AUTOMATE_BROWSER_SENSITIVE_ORIGINS` | Origins that stay readable but can never be acted on | unset |
| `AUTOMATE_BROWSER_READ_ONLY` | `1` refuses every page-changing tool, everywhere | unset |
| `AUTOMATE_BROWSER_NO_EVAL` | `1` refuses every tool that runs JavaScript you wrote — `browser_eval` and `browser_navigate`'s `initScript` | unset |
| `AUTOMATE_BROWSER_AUDIT` | `off` stops the action audit log below | on |
| `AUTOMATE_BROWSER_AUDIT_FILE` | Where the audit trail is written | `~/.automate-browser/automate-browser-audit.log` |
| `AUTOMATE_BROWSER_NO_UPDATE_CHECK` | Any value stops the daily check for a newer release below | unset (check runs) |
| `AUTOMATE_BROWSER_CRUX_KEY` | Google Chrome UX Report API key; `browser_perf_field_data` is inert without it | unset |
### Console-error and issue footers
After a page-changing tool (click, type, navigate, …) the server counts what went wrong since the
last check and, when there is anything, appends a line for each kind:
```
⚠ 2 new console errors since this action — call browser_get_console_logs for details.
⚠ 1 new browser issue since this action (blocked content or failed requests) — call browser_issues for details.
```
The two are deliberately **separate lines**: a blocked script or a failed request produces no console
error at all, so counting them as one would point you at the tool that cannot show them.
Read-only tools never get it, and the count starts from the moment the server launched, so errors
the page logged before the session are not blamed on the first action. The probe costs one extra
round trip (measured at ~2 ms against a real browser) and is capped by
`AUTOMATE_BROWSER_DELTA_FOOTER_MS`; if it fails for any reason it is silently dropped rather than
failing the action. A navigation replaces the page's log buffer, so errors from the previous document
are not counted late — read them with `browser_get_console_logs { includePreserved: true }` (see **Log
durability** below). The issue count covers the current page only.
### Asking for the fresh state in the same reply
The footer above says *that* something broke. `include` is the half that shows *what*: pass
`include: "snapshot"`, `"console"`, `"network"` (or several, comma-separated) to `browser_click`,
`browser_type` or `browser_navigate` and each arrives as its own labelled block in the same reply —
no second and third round-trip that an agent usually doesn't make. A section that fails to load is a
footnote, never a failure of the click that worked — each one is fetched and labelled independently,
so a console read that times out does not cancel the network block beside it. Omit it and nothing
changes.
**A section name it does not recognise is refused before the action runs.** `include: "consoel"`
returns an error and the click never happens, so retrying the corrected call clicks once, not twice.
That ordering is the whole point: an option that only shapes the reply is checked while checking it
is still free.
It is on those three tools only. On hover, drag, scroll or a key press the answer is almost always
"nothing worth a payload", and the param would be paid for on every request forever.
**`includeSnapshot` is the older, blunter version of the same idea, and its default is not the same
everywhere.** A navigation returns a **full snapshot by default**, because arriving on a new page
invalidates every element reference you were holding. Clicks, typing, hovers, drags and option
selects return a **one-line confirmation** instead. Pass `includeSnapshot: true` to add one to an
interaction, or `includeSnapshot: false` to strip it from a navigation you only wanted to perform —
that is the single cheapest saving available on a page-heavy task. Setting
`AUTOMATE_BROWSER_SNAPSHOT_EACH_ACTION` turns it on everywhere at once, which is the pre-lean
behaviour and costs accordingly.
### Uncaught errors keep their stacks
An error the page throws is reported with the trail back to the throw site, in its own section below
the ordinary console lines, rather than as a bare "something went wrong" string. The plain log stays
chronological and unchanged; the stacks are kept out of it so it stays readable.
### Service workers show up in the console
A page's service worker is invisible in ordinary logs, so a broken one looks like the site simply not
working. `browser_get_console_logs` folds worker **lifecycle** into the same log, tagged `[sw]`: the
registration attempt and whether it succeeded, state changes (`installing` → `activated` →
`redundant`), `controllerchange`, and messages the worker posts to the page. A FAILED registration is
reported as an error; a routine update is not, so a healthy worker never triggers the console-error
footer below.
It attaches to a worker that was **already registered before the page loaded**, which is the common
case — otherwise every second visit would look worker-free.
**The honest limit:** a `console.log` written *inside* the worker is not included. No debugger-free
API exposes it, and the tool's own description says so rather than letting you assume otherwise.
### The console and network lists are paged, newest first
A busy single-page app produces hundreds of console lines and hundreds of requests, and both lists
used to arrive whole. `browser_get_console_logs` and `browser_network_requests` now return **50
entries per call**. On `browser_network_requests`, `limit` sets that page size; on
`browser_get_console_logs` it is fixed.
**Page 1 is the newest, and `page: 2` goes further back in time**, not forward. That direction is
deliberate: both tools already returned "the most recent N", so a call that does not mention `page`
returns what it always returned.
When more remains, a footer says which page you are on, how many entries exist in total, and the
exact next call:
```
— page 1/4 of 173 requests, newest page first. Older: browser_network_requests {"page":2}
```
Ask for a page past the end and you get **page 1 with a line saying the page you asked for does not
exist** — never an error, and never an empty result you have to interpret.
**The honest limits.** Everything that fits on one page prints no footer at all, so a quiet page looks
exactly as it did before. Thrown-error stacks are printed for **the page you asked for**, so an error
further back needs its page fetched. And the network log is the extension's ring buffer of the **1000
most recent** requests for the tab; paging cannot reach past what the browser still holds.
The default network page was **100** before this change and is now **50**.
### Snapshots: lean, full, or to a file
`browser_snapshot` returns the lean tree by default. Pass `verbose: true` for the full tree — all text
and structure, not just interactive elements — or `filePath` to write it out and get a summary back,
for pages whose snapshot would otherwise fill the context window. **Element refs are identical in
both modes**, so you can switch mid-task and keep using refs you already hold. `filePath` goes through
the same sandbox as every other write path.
**A control with no text still gets a name.** `` used to print as a bare `- button [ref=…]`, indistinguishable from the icon button beside
it. A control with no name of its own now falls back to its `title`, then the `alt` of an image inside
it, then its `#id`: `- button "#buttonGenerate" [ref=…]`. Refs do not change — they never read the
name. `browser_find` names such a control from its image's `alt` too, so `{ text: "Settings" }`
reaches a cog button whose image says "Settings"; its `id` and `title` already print as attributes.
The limits: an `#id` names the element, not what it does — `#btn-3` tells you nothing, so look before
you click; a control with no title, image alt or id still prints unnamed; and an icon drawn by CSS
alone (a font glyph or background image) carries no text anywhere to borrow.
### Frames, including cross-origin ones
Ad slots, embedded checkouts and payment fields usually live in an `