MCP server ยท self-hosted ยท MIT

Your real browser, driven by your coding agent.

Drive any browser tab from your AI coding agent โ€” through the browser you already have open. Your logins, your cookies, your extensions. No fresh Playwright instance. No CDP debug port. No re-auth dance. Just the browser already on your desk.

install
# 1. clone + build
$ git clone https://github.com/arikw/rx-browser-bridge-mcp
$ cd rx-browser-bridge-mcp && npm install && npm -w mcp run build

# 2. boot the relay (npm, docker compose, or podman compose)
$ npm -w relay run dev

# 3. load extension/ unpacked into Chrome / Edge / Brave

# 4. register the MCP user-scope (so it loads in every CC session)
$ claude mcp add browser-bridge -s user -- \
    node --env-file-if-exists=$PWD/.env $PWD/mcp/dist/server.js

Snippet shows the shape โ€” see the full install guide for secrets, extension config, and registering with other MCP clients.

MIT licensed Self-hosted relay Chrome ยท Edge ยท Brave (Chromium MV3)
agent ยท my-project
> check if my PR triggered the deploy on the office browser
โ—
Calling screenshot on browser "office"โ€ฆ
โ†ณ active tab: github.com/acme/api/actions
Calling query for ".workflow-run-status"โ€ฆ
โ†ณ "queued ยท #4821 ยท 0:12 ago"
Deploy is queued behind one other run. ETA ~3min.
> navigate to the logs view
โ—
Calling navigateโ€ฆ
$ โ–Œ

How it works

Three pieces. Self-hosted relay, MCP server, MV3 extension.

1. Relay โ€” the meeting point

A small Node HTTP+WS server. The MCP POSTs commands to it; the browser extension pulls them off via a persistent WebSocket. Two static bearer tokens โ€” POSTER (writes) and PULLER (reads) โ€” so a leak of one doesn't grant the other. Sqlite audit log on every action.

2. MCP server โ€” your agent's tools

A standard stdio MCP server. Loaded by any MCP-aware coding agent โ€” Claude Code via .mcp.json, Cursor / Cline / Zed / Continue / Goose / Windsurf via their own config. Exposes list_browsers, list_tabs, screenshot, navigate, new_tab, click, fill, query, evaluate.

3. Extension โ€” the executor

Manifest v3 extension that maintains a persistent WS to the relay. Each command runs against the active tab via chrome.scripting.executeScript + chrome.tabs.captureVisibleTab. Destructive actions trigger a Chrome notification with Allow / Deny โ€” default-deny after 10s.

data flow (per tool call)
Your terminal / IDE
Any MCP-aware coding agent
Claude Code, Cursor, Cline, Zed, Continue, Goose, Windsurf โ€” calls one of the MCP tools (screenshot, click, fill, โ€ฆ) with optional target or tag
stdio  ยท  MCP tool call (in)  ยท  tool result (out)
Loaded by your MCP client
mcp/dist/server.js
POSTs /enqueue with action+args, long-polls /poll/:cmd_id for up to 30s
HTTP  ยท  Bearer POSTER_TOKEN  ยท  loopback by default
Self-hosted (Node or docker)
relay/ โ€” HTTP + WS + sqlite audit log
matches command to a registered browser (by target id or tag), pushes via WS, stores result for the poller
WebSocket  ยท  ?token=PULLER_TOKEN  ยท  register ยท heartbeat ยท cmd ยท result
Manifest v3 โ€” any Chromium browser
extension/ โ€” service worker + popup + options
runs the action against the active tab; prompts via chrome.notifications first if the heuristic flags it as destructive

The extension pulls โ€” it doesn't open a port. Nothing in your browser is reachable from outside. The relay can stay bound to 127.0.0.1 and still drive a browser that's behind any NAT.

Why you'd want this

When a fresh headless browser isn't what you want.

Your sessions, your cookies

It's the browser you're already logged into. GitHub, AWS console, internal dashboards, Gmail, anything behind SSO. Your agent works with the same sessions you do โ€” no re-auth, no cookie copy, no service-account workaround.

No CDP, no debug port

Nothing in your browser needs to listen on a port. The extension pulls work from the relay over an outbound WS. Works behind NAT, works on machines you'd rather not poke holes in, works without restarting Chrome with debug flags.

Multiple browsers, tag-routed

Office Chrome, home Brave, a VM's Edge โ€” all register against the same relay. Address them by id (target: "office") or by tag (tag: "reddit", first match wins). One agent session, many surfaces.

What's in the box

Pragmatic surface. The full pipe wired end-to-end, nothing you don't need.

Nine MCP tools

list_browsers, list_tabs, screenshot, navigate, new_tab, click, fill, query, evaluate. Enough to inspect, interact, and report โ€” the agent takes it from there. screenshot takes fullPage:true to scroll-and-stitch the whole page; evaluate runs arbitrary JS (via the userScripts world, so it works even on CSP-locked sites like GitHub); pass tabId from list_tabs to drive a specific tab across multiple windows.

Two-token split auth

POSTER for the MCP (writes), PULLER for the extension (reads). A leak of one doesn't grant the other direction. Static bearer tokens โ€” simple, auditable, rotatable from a single env file.

Destructive-action prompt

Click selectors that look like submit / post / send, or fill on password fields, trigger a Chrome notification with Allow / Deny buttons. Default-deny after 10s. No double-checking your auth-token field after the fact.

Live activity flash + status badge

The toolbar icon flashes red while the bridge is handling a request (โ‰ฅ5s, held while requests keep coming) โ€” an out-of-band cue that something is driving your browser right now. A separate badge tracks the relay link: green connected, amber connecting, orange retrying, red disconnected, grey disabled.

Sqlite audit log

Every register / cmd / result lands in relay/data/relay.sqlite with a timestamp. Grep, join, replay โ€” your call. No external service, no telemetry leaving the box.

Popup with kill switch

Toolbar icon โ†’ status (connected? which relay?), the last 50 audit entries, and a single big disconnect button. The extension stops accepting work the moment you flip it.

Headless e2e test

Puppeteer boots the relay, launches Chromium with the unpacked extension, seeds storage via the SW worker target, exercises the full pipe โ€” screenshot, navigate, query โ€” in under five seconds.

Docker / podman compose

Relay ships with a Dockerfile + compose file that works under both docker compose and podman compose. Single shared .env at repo root drives the container, the host-mode relay, and the MCP โ€” same config, same secrets.

Multi-browser registry

Each browser self-registers with an id + tags on connect, and pings a heartbeat every 20s. list_browsers tells the agent who's online; target / tag picks where a command runs.

Single .env, three components

Relay, MCP, and docker-compose all read the same .env at repo root via --env-file-if-exists. Edit once, applies everywhere โ€” no parallel copies of POSTER_TOKEN to keep in sync.

How it compares

Other MCP browser-automation servers spawn their own browser. That's the right call for clean-room scraping. It's the wrong call when the browser is the workspace โ€” the already-logged-in tabs you've got open right now.

Drives your already-open browser
rx-browser-bridge yes โ€” via MV3 extension
chrome-devtools-mcp only with --remote-debugging-port
playwright-mcp no
puppeteer-mcp no
Reuses your real session cookies / SSO
rx-browser-bridge yes
chrome-devtools-mcp if pointed at profile dir
playwright-mcp requires storageState export
puppeteer-mcp requires cookie injection
Browser exposes no inbound port
rx-browser-bridge yes โ€” outbound WS only
chrome-devtools-mcp no โ€” CDP debug port
playwright-mcp n/a
puppeteer-mcp n/a
Multi-browser, tag-routed
rx-browser-bridge yes
chrome-devtools-mcp single attach
playwright-mcp single instance
puppeteer-mcp single instance
Destructive-action confirmation
rx-browser-bridge heuristic + notification
chrome-devtools-mcp no
playwright-mcp no
puppeteer-mcp no
Full DevTools surface
rx-browser-bridge no โ€” extension APIs only
chrome-devtools-mcp yes
playwright-mcp yes
puppeteer-mcp yes
When to reach for which

Use Playwright / Puppeteer MCP when you want a clean-room browser per task โ€” scraping, screenshot regression, no shared state. Use chrome-devtools-mcp when you need the full DevTools surface (network panel, performance traces, console). Use rx-browser-bridge when the value is in this exact browser, this exact session: dashboards behind SSO, a CI tab you already opened, an admin UI that took a security key to log into.

They compose. Nothing stops you from loading all three MCPs โ€” the right tool surface for the job is the one the agent picks per call.

Questions

Do I need to expose anything on the public internet?

No, not for the single-machine default. The relay binds to 127.0.0.1; the MCP talks to it over loopback; the extension also connects to 127.0.0.1 if Chrome is on the same box. Cross-machine deploys โ€” driving an office browser from a home laptop, say โ€” need the relay reachable by both ends, which you'd front with TLS via Caddy, Cloudflare Tunnel, or Tailscale Funnel.

What stops the agent from clicking "delete account" on me?

Two layers. (1) A heuristic in the extension flags clicks against submit/post/send selectors and fills against password fields as destructive โ€” those trigger a Chrome notification with Allow / Deny buttons that default-deny after 10s. (2) The kill-switch in the popup disconnects the extension entirely. Neither is a substitute for not running this against a session that matters. The current defaults favor speed of iteration over hardened policy; per-domain trust toggles are on the roadmap.

Why an extension, not just CDP?

CDP needs Chrome started with --remote-debugging-port, an open TCP port on your machine, and locks the browser into a mode where any local process can drive it. An extension is just a long-running script the user explicitly loaded. It pulls work over an outbound WS โ€” no port, no flag, no full-takeover surface. The trade-off is the extension API is narrower than CDP โ€” no network interception, no performance traces.

Can I drive a background tab?

Not yet โ€” every command runs against the active tab in the active window. URL-matching and window-selector targeting is on the roadmap; the registration protocol already passes current_url so the routing layer has what it needs.

Firefox?

A Firefox MV3 manifest variant is on the roadmap. The bulk of extension/background.js uses standard chrome.* APIs Firefox supports via its browser.* namespace, so the diff should be small.

How do I wire it into my MCP client?

It's a vanilla @modelcontextprotocol/sdk stdio server, so any MCP-aware client works. Claude Code reads .mcp.json (shipped in the repo). Cursor uses ~/.cursor/mcp.json. Cline uses its settings panel. Zed, Continue, Goose, Windsurf โ€” all support stdio MCPs. Point yours at node mcp/dist/server.js with POSTER_TOKEN + RELAY_URL in the env. A mcp/.claude-plugin/plugin.json is also in the tree for Claude Code marketplace packaging.

What about TLS?

The relay is plain HTTP + WS. For loopback that's fine. For cross-network deploys, front it with a TLS-terminating reverse proxy โ€” Caddy, Cloudflare Tunnel, Tailscale Funnel. A first-party TLS sidecar service in the compose file is on the roadmap.