OpenBrowser
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)
[](package.json)
[](#install)
[](#zero-dependencies)
[](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>
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessNo issues