Skip to main content
Glama
README.md
<div align="center">

<img src="extension/icons/icon128.png" width="96" height="96" alt="OpenBrowser">

# OpenBrowser

**Browser automation for AI agents โ€” in your real Chrome, with your real logins.**

A Chrome extension plus a zero-dependency MCP server, so any MCP client can drive
an actual browser instead of a headless copy of one.

[![License: MIT](https://img.shields.io/badge/license-MIT-22d3ee?style=flat-square)](LICENSE)
[![Node](https://img.shields.io/badge/node-%E2%89%A518-5b8c5a?style=flat-square)](package.json)
[![Chrome](https://img.shields.io/badge/chrome-%E2%89%A5116-5b8c5a?style=flat-square)](#install)
[![Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen?style=flat-square)](#zero-dependencies)
[![Website](https://img.shields.io/badge/site-openbrowser.pulse--core.com-38bdf8?style=flat-square)](https://openbrowser.pulse-core.com)

### [๐ŸŒ openbrowser.pulse-core.com](https://openbrowser.pulse-core.com)

[Install](#install) ยท [Quickstart](#quickstart) ยท [Tools](#the-tools) ยท [Token cost](#keeping-token-cost-down) ยท [Difficult sites](#difficult-sites) ยท [Security](#security) ยท [Contributing](#contributing)

</div>

---

## What it is

OpenBrowser lets an AI agent drive **your** Chrome โ€” the one already signed in to
your accounts, running your extensions โ€” over the [Model Context
Protocol](https://modelcontextprotocol.io). It has two halves: a Manifest V3
Chrome extension with a side-panel UI, and a small MCP stdio server that any MCP
client (Claude Code, opencode, Cursor, Windsurf, Zed, or your own) can launch.

Most browser automation hands the agent a fresh headless browser instead: signed
out of everything, fingerprinted as a bot, and reading pages as either raw DOM or
screenshots. OpenBrowser takes the opposite position on all three โ€” real session,
trusted input, and a compact accessibility tree that keeps token cost low.

## Highlights

- ๐Ÿ”“ **Real browser, real session.** Runs in your actual Chrome, with your logins,
  cookies, and extensions. Nothing extra to keep signed in.
- โŒจ๏ธ **Trusted input events.** Clicks and keystrokes go through the Chrome
  debugger, so they are indistinguishable from a real user's. Payment forms, login
  pages, and drag-and-drop all work where synthetic clicks are rejected.
- ๐Ÿชถ **Built for token cost.** Pages are read as a compact accessibility tree, not
  screenshots or raw DOM. A full login page costs ~350 characters.
- โšก **Parallel by default.** Every tool takes a `tabId`. Read twenty tabs at once.
- ๐Ÿงฉ **Fourteen composable tools.** Grouped by `action` enums rather than split
  into forty single-purpose ones โ€” models pick an enum value far more reliably.
- ๐Ÿ–ฅ๏ธ **Side-panel UI.** Run any tool by hand and see exactly what an agent would
  get back โ€” the fastest way to debug a flow.
- ๐ŸŒ **Multi-browser and multi-machine.** Several agents share one browser; one
  hub can federate to hubs on other machines and drive their browsers too.
- ๐Ÿ“ฆ **Zero dependencies.** No `npm install`. Node 18+ and Chrome 116+ is the whole
  requirement.
- ๐Ÿ  **Entirely local.** Loopback only by default. No telemetry, no analytics, no
  outbound calls.

<a id="zero-dependencies"></a>

> **Zero dependencies is a feature, not a boast.** `npm install` failing is the
> most common reason a local MCP server doesn't work, and it fails silently from
> the user's point of view. The WebSocket and MCP protocol implementations are
> hand-written for exactly this reason.

---

## Install

### 1. Get the code

```bash
git clone https://github.com/dylansantwani/openbrowser.git
```

There is nothing to build and nothing to install โ€” no `npm install` step.

### 2. Load the extension

Open `chrome://extensions`, turn on **Developer mode**, click **Load unpacked**,
and select the `extension/` folder.

### 3. Point your MCP client at the server

<details open>
<summary><b>Claude Code</b></summary>

```bash
claude mcp add openbrowser -- node /absolute/path/to/openbrowser/mcp-server/src/index.js
```
</details>

<details>
<summary><b>opencode</b> โ€” <code>~/.config/opencode/opencode.json</code></summary>

```json
{
  "mcp": {
    "openbrowser": {
      "type": "local",
      "enabled": true,
      "command": ["node", "/absolute/path/to/openbrowser/mcp-server/src/index.js"]
    }
  }
}
```
</details>

<details>
<summary><b>Anything else</b> (Cursor, Windsurf, Zed, custom clients)</summary>

Standard MCP stdio server:

```json
{
  "mcpServers": {
    "openbrowser": {
      "command": "node",
      "args": ["/absolute/path/to/openbrowser/mcp-server/src/index.js"]
    }
  }
}
```
</details>

---

## Quickstart

First, confirm Chrome is connected. The server prints its status and exits:

```bash
node mcp-server/src/index.js --health
```

The toolbar badge clears when Chrome is connected. If it doesn't, see
[Troubleshooting](#troubleshooting).

Now hand your agent a task. Everything an agent does reduces to two moves โ€” read
the page, then act on it. A read looks like this:

```
browser_navigate url:"example.com"
browser_snapshot
```

and `browser_snapshot` renders the page as a compact accessibility tree with
`[ref=eN]` handles you can act on directly:

```
app.example.com/login ยท "Sign in ยท Example" ยท tab 481 ยท 1280x800
banner
  link "Example" [e1] /
main
  heading "Sign in" h1
  form
    textbox "Email" [e2] required
    password "Password" [e3] required
    checkbox "Remember me" [e4] unchecked
    button "Sign in" [e5]
  link "Forgot your password?" [e6] /reset
```

356 characters โ€” roughly 90 tokens. The same page is ~4,000 tokens as raw
accessibility JSON and ~1,500 as a screenshot.

### A whole login in one call

Once you know a flow, `browser_batch` collapses it into a single round-trip:

```json
{
  "tool": "browser_batch",
  "args": {
    "steps": [
      { "tool": "browser_navigate", "args": { "url": "app.example.com/login" } },
      { "tool": "browser_input", "args": {
          "fields": [
            { "ref": "e2", "value": "ada@example.com" },
            { "ref": "e3", "value": "correct horse battery staple" }
          ]}},
      { "tool": "browser_act", "args": { "action": "click", "ref": "e5" } },
      { "tool": "browser_wait", "args": { "for": "text", "value": "Dashboard" } }
    ]
  }
}
```

One round-trip instead of eight.

---

## The tools

Fourteen tools, grouped by `action` enums rather than split into forty
single-purpose ones โ€” models pick an enum value far more reliably.

| Tool | What it does |
|---|---|
| `browser_tabs` | list / new / close / select / reload / duplicate |
| `browser_navigate` | go to a URL, back, forward, reload |
| `browser_snapshot` | read the page as an accessibility tree with `[ref=eN]` handles, or as an outline of its regions |
| `browser_find` | find elements by description, ranked |
| `browser_act` | click, hover, drag, select, check, answer native dialogs โ€” trusted input events, with `expect` to verify in the same call |
| `browser_input` | type text, fill many fields at once, press keys, `expect` the result |
| `browser_screenshot` | viewport / full page / element / region, or record a GIF |
| `browser_wait` | block on text, selector, URL, network idle, load, or a background job |
| `browser_eval` | run JavaScript in the page |
| `browser_inspect` | console, network, cookies, storage, downloads, frames |
| `browser_batch` | run many calls as one request โ€” with `when`/`repeat` control flow, across many tabs, or in the background |
| `browser_upload` | attach local files to a file input |
| `browser_window` | pick the window/browser, attach a hub on another machine, resize, emulate a device, throttle network |
| `browser_macro` | save and replay step sequences |

Full parameter reference: **[docs/TOOLS.md](docs/TOOLS.md)**.

---

## Keeping token cost down

The design assumes tokens are the scarce resource:

1. **Snapshots, not screenshots.** ~20x cheaper, and refs are directly
   actionable. Screenshot only to verify something visual.
2. **`mode: "diff"` in loops.** After the first snapshot, ask only what changed.
3. **Actions return their own delta.** After a click you usually already know
   what changed, so no follow-up snapshot is needed.
4. **`selector` to scope, `mode:"outline"` to choose.** On a dense page, the
   outline lists every region with its control count and selector for a few
   hundred characters; then read the one region you care about.
5. **`browser_batch` for known flows.** Collapses N round-trips into one, and
   `when` / `unless` / `repeat` handle the "if there is a banner" and "until
   the button is gone" cases that used to force one call per step.
6. **`expect` on actions.** Verify the click in the same call instead of
   spending a turn on wait-then-snapshot.
7. **`browser_macro` for repeated flows.** Derive the flow once, replay for the
   cost of a single call.

And the tool never sleeps: every action waits for the page to react, or to
prove it will not, and returns at that moment.

---

## Difficult sites

The cases that usually break browser automation, and what handles them here:

| Problem | How it is handled |
|---|---|
| Site ignores synthetic clicks | Trusted events via the Chrome debugger |
| Content inside iframes | All frames are read; refs carry their frame (`f2e5`) |
| Cross-origin iframe coordinates | Offsets cascade via `postMessage` so clicks land correctly |
| Element under a cookie banner | Detected before clicking, and reported with what is covering it |
| Shadow DOM / web components | Open shadow roots are pierced when reading and hit-testing |
| React ignores a typed value | Native setter + the event pair frameworks actually listen for |
| Hidden file inputs behind "Browse" | The real `input[type=file]` is located from the visible control |
| Drag-and-drop libraries | Interpolated, eased movement above the drag threshold |
| SPA re-render invalidates a ref | Refs re-resolve through a stored selector before failing |
| Element is off screen | Auto-scrolled into view, then waited until it stops moving |
| CAPTCHA | Detected and reported. **Not bypassed** โ€” that needs a human |
| Click opens a new tab or popup | Reported with the new tab's id, rather than looking like nothing happened |
| Tab is in the background | Foregrounded before input; Chrome silently discards clicks aimed at hidden tabs |
| Native JS dialog (alert/confirm/beforeunload) | Reported with the dialog's text; answered with `browser_act action:"dialog" accept:true/false` |

Every row above is a bug that was found by driving the extension against a real
site, not a hypothetical. The write-ups are in
[docs/SESSION-2026-08-02.md](docs/SESSION-2026-08-02.md) and
[docs/SESSION-2026-08-03.md](docs/SESSION-2026-08-03.md).

---

## Side panel

Click the toolbar icon or press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>U</kbd>
(<kbd>โŒ˜</kbd>+<kbd>Shift</kbd>+<kbd>U</kbd> on macOS).

- **Control** โ€” tabs, quick actions, element search
- **Tools** โ€” run any tool by hand and see exactly what an agent would get
- **Macros** โ€” inspect, run, and delete saved sequences
- **Activity** โ€” every call with timing and errors

The panel calls the same dispatcher the MCP server does, so it is the fastest
way to debug a flow: try it by hand, then hand it to the model.

---

## Configuration

Extension options (`chrome://extensions` โ†’ Details โ†’ Extension options):

| Setting | Default | Notes |
|---|---|---|
| Hub port | `8848` | Must match the server's `--port` |
| Connect automatically | on | Reconnects on browser start |
| Trusted input events | on | Turning this off makes many sites ignore the agent |
| Highlight elements | on | Outlines elements as they are used |
| Capture bodies | off | Request/response bodies; large, often sensitive |
| Snapshot budget | 20,000 chars | Truncation limit |
| Blocklist | identity providers | Never automated |
| Allowlist | empty | If non-empty, *only* these sites are automated |

Server flags (`node mcp-server/src/index.js โ€ฆ`, or the matching env var):

| Flag | Env | Default | What it does |
|---|---|---|---|
| `--port N` | `OPENBROWSER_PORT` | `8848` | Hub port |
| `--host H` | `OPENBROWSER_HOST` | `127.0.0.1` | Bind address; `0.0.0.0` accepts remote hubs |
| `--connect A,B` | `OPENBROWSER_CONNECT` | โ€” | Attach to remote hub(s) at startup |
| `--health` | | | Print hub + browser status and exit |
| `--hub` | | | Run the hub only, no MCP |
| `--verbose` | | | Log to stderr |

---

## How it works

```
  Claude Code โ”€โ”
               โ”œโ”€ stdio โ”€> mcp-server โ”€ ws://127.0.0.1:8848 โ”€> Chrome extension โ”€> your tabs
  opencode โ”€โ”€โ”€โ”€โ”˜
```

The MCP server speaks stdio to your client and WebSocket to the extension. The
first server to start binds the hub port; later ones join it. So several agents
can share one browser โ€” an editor agent and a CLI agent can work side by side
without fighting over it, because each session gets a name of its own (`harbor`),
owns only the tabs in its own tab group, and works in a shared background agent
window that nothing it does can bring in front of you.

Everything is local. Nothing leaves your machine except the pages you ask it to
visit.

### Browsers on other machines

A hub can attach to hubs elsewhere, so one agent with **one** MCP config drives
browsers on any number of boxes:

```
  your agent โ”€โ”€> hub (laptop) โ”€โ”€โ”ฌโ”€โ”€> Chrome, here
                                โ”œโ”€โ”€wsโ”€โ”€> hub (10.0.0.5) โ”€โ”€> Chrome, there
                                โ””โ”€โ”€wsโ”€โ”€> hub (10.0.0.6) โ”€โ”€> Chrome, there
```

On each remote machine, let the hub listen off-loopback:

```bash
node mcp-server/src/index.js --hub --host 0.0.0.0
```

Then, from an agent:

```
browser_window action:"connect" hub:"10.0.0.5"
```

Its browsers appear as `10.0.0.5/<name>` and are used exactly like local ones.
`action:"remotes"` lists what is attached; `action:"disconnect"` detaches.
`--connect 10.0.0.5,10.0.0.6` attaches them at startup instead.

> โš ๏ธ **The hub has no authentication.** Anything that can reach it can run
> JavaScript in a logged-in browser. `--host` defaults to `127.0.0.1` for that
> reason โ€” keep federated hubs on a private network or a VPN mesh, never on a
> public IP.

For why the pieces are split the way they are, see
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).

---

## Security

- Binds **loopback only** (`127.0.0.1`). Nothing is exposed to your network.
- No telemetry, no analytics, no outbound calls of any kind.
- The blocklist ships with identity providers on it, because an automation
  mistake against an SSO flow is expensive and hard to undo.
- CAPTCHAs are reported, never solved or bypassed.
- The `debugger` permission is what makes trusted input possible. It is broad โ€”
  read [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for exactly what it is used
  for, and turn it off in options if you would rather not grant it.

> โš ๏ธ **Treat an agent with browser access as having your logged-in privileges.**
> Use the allowlist when running unattended.

---

## Troubleshooting

<details>
<summary><b>Badge shows <code>โ—‹</code>, health says not connected</b></summary>

```bash
node mcp-server/src/index.js --health
```

The hub only exists while an MCP client has the server running. To test
standalone: `npm run hub`.
</details>

<details>
<summary><b>"Cannot attach to this page"</b></summary>

Chrome blocks extensions on `chrome://` pages, the Web Store, and other
extensions' pages. Navigate somewhere else.
</details>

<details>
<summary><b>"DevTools is open on this tab"</b></summary>

DevTools and the extension cannot both own the debugger. Close DevTools, or use
another tab.
</details>

<details>
<summary><b>Refs keep going stale</b></summary>

The page re-renders aggressively. Use `browser_find` immediately before acting,
or `browser_batch` so the whole sequence runs before the page can change under
you.
</details>

<details>
<summary><b>Service worker went idle</b></summary>

Expected under MV3. It respawns and reconnects on its own; the first call
afterwards may take a moment.
</details>

<details>
<summary><b>A change to <code>extension/</code> did nothing</b></summary>

Chrome caches extension files. Anything under `extension/` needs
`chrome://extensions` โ†’ **reload** before it takes effect. Anything under
`mcp-server/` is picked up when the MCP client next starts the server.
</details>

---

## Development

```bash
npm test          # 232 tests: WebSocket framing, MCP protocol, round trip, formatting
npm run preview   # UI preview + in-browser accessibility-tree tests at :8850
npm run hub       # hub only, verbose
npm run icons     # regenerate icon PNGs
```

`npm run preview` serves two things that need a DOM: the side-panel UI at `/`,
and the accessibility-tree assertions at `/test/a11y-browser.html`.

Layout:

```
extension/
  background/   service worker: bridge, router, CDP, recorder, frames, formatting
  content/      injected: accessibility tree, actions, frame offsets
  sidepanel/    the UI
  options/      settings
mcp-server/src/ ws.js (hand-rolled RFC 6455), hub.js, mcp.js, tools.js
docs/           capabilities, tools reference, architecture, test checklist
site/           the source of openbrowser.pulse-core.com (static, no build step)
```

| Doc | What it is for |
|---|---|
| [docs/CAPABILITIES.md](docs/CAPABILITIES.md) | What the fourteen tools can do in combination โ€” parallel tabs, macros, retroactive network capture, trusted input, iframe reach |
| [docs/TOOLS.md](docs/TOOLS.md) | Full parameter reference |
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Why the pieces are split this way |
| [docs/TESTING.md](docs/TESTING.md) | Manual checklist for the parts that need a real browser |
| [docs/SESSION-2026-08-02.md](docs/SESSION-2026-08-02.md) | First real-site hardening pass: what broke, what was fixed, what is still unproven |
| [docs/SESSION-2026-08-03.md](docs/SESSION-2026-08-03.md) | Second pass โ€” OAuth, popups, checkout forms, and the backgrounded-tab input bug |

`CLAUDE.md` carries the hard rules and the platform behaviours that each cost a
real bug to discover. `AGENTS.md` is the short version for AI coding agents.
`TODO.md` has the open work with reproduction details.

---

## Contributing

Contributions are welcome. The short version: no dependencies, ever; `npm test`
stays green; commits follow [Conventional
Commits](https://www.conventionalcommits.org/). The full guide โ€” dev setup,
testing, commit convention, and PR process โ€” is in
**[CONTRIBUTING.md](CONTRIBUTING.md)**.

## License

MIT โ€” see [LICENSE](LICENSE).

<div align="center">
<br>

**[openbrowser.pulse-core.com](https://openbrowser.pulse-core.com)**

</div>