RX Browser Bridge
Drive your own already-open browser from your AI coding agent (any MCP client) via a self-hosted relay + MV3 extension. Tools: screenshot (+full-page), navigate, click, fill, query, new_tab, evaluate (arbitrary JS, even on CSP-locked sites via userScripts). Toolbar activity flash + sqlite audit log for visibility.
About
Sourced from arikw/rx-browser-bridge-mcp README
POC for driving any browser tab from a Claude Code session via a self-hosted relay + MV3 extension.
| Status | v0.1.0 — walking-skeleton POC |
| Stack | Node.js ≥ 20, TypeScript, MV3 browser extension |
| License | MIT |
Pieces
┌──────────────────┐ HTTP POST /enqueue ┌──────────────────┐
│ Claude Code │ ─────────────────────────▶ │ │
│ ↳ mcp/server.ts │ GET /poll/:cmd_id (long) │ relay/ │
│ (per-tool │ ◀───────────────────────── │ (Node, sqlite) │
│ call) │ │ │
└──────────────────┘ └────────┬─────────┘
│ WebSocket
│ ws://relay/ws
▼
┌──────────────────┐
│ extension/ │
│ (MV3, Chromium │
│ or Firefox) │
└──────────────────┘
- relay/ — Node HTTP+WS server. Accepts CC's POST
/enqueue, routes to extension over WS, returns result via long-poll/poll. Bearer-token auth (POSTER for CC, PULLER for extension). sqlite for audit log. - mcp/ — MCP stdio server loaded by Claude Code. Tools: see Command set.
- extension/ — Manifest v3 extension. Persistent WS to relay,
dispatches commands to active tab via
chrome.scripting.executeScriptchrome.tabs.captureVisibleTab. Notification-based confirmation prompt for destructive actions (submit/post/send buttons, password fields).
Install
Five steps. Each one ends with a check, so you know it worked before you move on. Once done, the browser tools are available in every Claude Code session, whichever directory you launch from.
You need: Node.js 20 or newer, Docker or Podman, Claude Code (or another MCP client), and Chrome / Chromium / Edge / Brave or Firefox 128+. The commands are for Linux and macOS.
How the pieces fit: the relay is a small server you run. The
browser extension connects to it, and so does the MCP server that
Claude Code starts. Each connection has its own password (token), and
both tokens live in one file, .env:
Token in .env |
Who uses it | Where you put it |
|---|---|---|
PULLER_TOKEN |
the browser extension | paste it into the extension's Options page (step 4) |
POSTER_TOKEN |
the MCP server | nowhere: it reads .env itself (step 5) |
The examples clone into ~/tools/rx-browser-bridge-mcp. Any place works;
just use the same path in step 5.
1. Get the code and build the MCP server
git clone https://github.com/arikw/rx-browser-bridge-mcp ~/tools/rx-browser-bridge-mcp
cd ~/tools/rx-browser-bridge-mcp
PUPPETEER_SKIP_DOWNLOAD=true npm install
npm -w mcp run build
Check: ls mcp/dist/server.js shows the file. (It isn't in git; you
must build it.)
PUPPETEER_SKIP_DOWNLOAD=true only skips a browser download used by the
tests.
2. Create .env with two random tokens
cp .env.example .env
sed -i.bak \
-e "s/^POSTER_TOKEN=.*/POSTER_TOKEN=$(openssl rand -hex 32)/" \
-e "s/^PULLER_TOKEN=.*/PULLER_TOKEN=$(openssl rand -hex 32)/" .env && rm .env.bak
Check: grep TOKEN .env shows two long random values, not replace-me.
Keep PORT=3000 unless that port is taken. If you change it, use the new
port everywhere below where you see 3000.
3. Start the relay
Run it in a container (recommended; it keeps running in the background):
cd relay
docker compose up -d --build # or: podman compose up -d --build
cd ..
Without compose:
cd relay
docker build -t rx-browser-bridge-relay .
docker run -d --name rx-browser-bridge-relay --restart unless-stopped \
-p 127.0.0.1:3000:3000 --env-file ../.env \
-e DATA_DIR=/data -v "$PWD/data:/data" rx-browser-bridge-relay
cd ..
Or straight on the machine, in the foreground: npm -w relay run dev.
Check: curl http://localhost:3000/health answers with JSON.
The relay listens only on this machine (127.0.0.1). Its log is in
relay/data/relay.sqlite.
4. Load the extension into your browser
Chrome / Chromium / Edge / Brave
- Open
chrome://extensionsand turn on Developer mode (top right). - Load unpacked → choose the
extension/folder. - Optional but recommended: open the extension's Details and turn on
Allow user scripts. Without it, the
evaluatetool doesn't work on sites that block running code, such as GitHub.
Firefox (128 or newer)
- Open
about:debugging#/runtime/this-firefox. - Load Temporary Add-on… → choose
extension/manifest.json. - Open
about:addons→ RX MCP Browser Bridge → Permissions, and allow access to all websites, if it isn't on already.
A temporary add-on is removed when Firefox closes, so you repeat these
steps after each restart. To keep it installed, it has to be signed by
Mozilla (see Known gaps). Two differences from Chrome: the confirmation
for risky clicks is a notification you click to allow (ignore it to
deny), and evaluate runs only on sites that allow running code.
Then, in either browser: open the extension's Options page and fill in:
| Field | Value |
|---|---|
| Relay URL | ws://localhost:3000/ws (note ws:// and /ws) |
| Puller token | the PULLER_TOKEN value from .env |
| Browser id | a short name for this browser, e.g. office |
| Tags | optional, comma-separated, e.g. work |
Check: click the extension's toolbar icon. The dot is green and it says connected. If it isn't, the error under it says why. Usually the relay isn't running, or the token doesn't match.
The same window lists what the extension is doing: what's running now, recent commands with ✓/✗, and a kill switch.
5. Register the MCP with Claude Code (user-scope)
So the browser tools load in every Claude Code session — not just
when you launch from this project directory — register at user scope.
The recommended setup points Node at the project's .env via an
absolute path, so you can rotate tokens by editing one file without
re-registering anything:
claude mcp add browser-bridge -s user -- \
node --env-file-if-exists=$HOME/tools/rx-browser-bridge-mcp/.env \
$HOME/tools/rx-browser-bridge-mcp/mcp/dist/server.js
What this does:
-s userwrites the registration into your user config (~/.claude.json), so it loads regardless of CWD.--env-file-if-exists=…/.envtells Node to load env vars from that absolute path on every launch —POSTER_TOKEN,RELAY_URL, and the optionalDEFAULT_TARGETcome from there.- The trailing
--separates Claude's flags from the command to spawn.
Alternative — env vars baked into the registration
If you'd rather not depend on .env (e.g. you want the token in a
secret manager and pulled in at register-time), pass the values
directly with -e:
claude mcp add browser-bridge -s user \
-e POSTER_TOKEN=your-token-here \
-e RELAY_URL=http://localhost:3000 \
-- node $HOME/tools/rx-browser-bridge-mcp/mcp/dist/server.js
The downside: you re-run claude mcp add every time you rotate.
Verify
claude mcp list # should show 'browser-bridge'
claude # launch from any directory
> list_browsers # should return the browser you registered
> navigate https://reddit.com on browser "office"
> screenshot
Other MCP clients
The MCP server is a vanilla @modelcontextprotocol/sdk stdio server.
Any MCP-aware client works — point it at
node /abs/path/to/mcp/dist/server.js with POSTER_TOKEN and
RELAY_URL in the env. Common locations:
- Cursor —
~/.cursor/mcp.json - Cline — settings panel → MCP servers
- Zed, Continue, Goose, Windsurf — see each client's MCP-server docs.
Quick start (project-local, for development)
If you're hacking on the MCP itself and don't want a user-scope
registration, the repo ships a project-local .mcp.json that wires
things up when Claude is launched from this directory:
cd rx-browser-bridge-mcp
claude --mcp-config ./.mcp.json
Same relay / extension setup as steps 3–4 above. Plugin / marketplace packaging is TBD — see Roadmap.
Tests
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium npm test
Spawns a relay on port 3399, launches headless Chromium with the unpacked extension, registers the browser, exercises the full pipe.
Command set (current)
| Tool | Args | Destructive? |
|---|---|---|
list_browsers |
— | no |
list_tabs |
target? / tag? |
no |
focus_tab |
tabId?, target? / tag? |
no (changes what's on screen) |
screenshot |
fullPage?, tabId?, target? / tag? |
no |
navigate |
url, tabId?, target? / tag? |
no |
new_tab |
url, active?, target? / tag? |
no |
click |
selector, tabId?, target? / tag? |
yes if selector matches submit/post/send |
fill |
selector, value, tabId?, target? / tag? |
yes if selector mentions password |
query |
selector, tabId?, target? / tag? |
no |
evaluate |
code, tabId?, target? / tag? |
runs arbitrary JS — see note |
If a page doesn't answer within 25s, the call fails with
tab-unresponsive and the tab's state, instead of hanging. Usual cause:
Chrome paused a background tab, or the page waits on an alert/confirm.
focus_tab brings the tab forward, which wakes it.
evaluate runs arbitrary JavaScript in the active tab and returns the
final expression value (JSON-encoded; return a Promise to await async
work). It's the escape hatch for anything click/fill/query can't
express. It is not behind the confirmation prompt — it relies on the
toolbar activity flash + the audit log for visibility, so the agent can
chain complex page work without a click per step.
It runs in the extension's userScripts world, whose CSP is set to
allow eval and which is exempt from the page's own CSP — so it works
even on CSP-locked sites (GitHub, etc.). This requires the "Allow user
scripts" toggle on the extension's chrome://extensions Details page
(Chrome's mandatory gate for the userScripts API). Without it,
evaluate falls back to the page's MAIN world, where a CSP forbidding
unsafe-eval will block it. Note: the userScripts world shares the DOM
but not the page's JS globals.
screenshot captures the visible viewport by default. Pass
fullPage: true to capture the entire scrollable page: the extension
scrolls a viewport at a time, captures each step with
chrome.tabs.captureVisibleTab, and stitches the slices on an
OffscreenCanvas — no chrome.debugger / CDP. Caveats: position:fixed
/ sticky elements repeat on each slice, lazy content must paint quickly
after each scroll, and very long pages are capped (12 slices) to stay
within the relay's poll budget.
Destructive actions trigger a Chrome notification with Allow / Deny buttons (10s timeout = deny). Per-domain / per-action trust toggles NOT yet implemented.
Targeting
Which browser (every tool):
target: "office"— exact browser id, errors if not online.tag: "reddit"— first online browser carrying the tag.- No multicast/broadcast (intentional POC scope).
Which tab (screenshot/navigate/click/fill/query/evaluate):
- Omit
tabId→ the active tab of the last-focused window, skipping the extension's own pages andchrome://(so it never lands on the popup/options). tabId: N→ that exact tab. Get ids fromlist_tabs(ornew_tab's return). DOM actions hit a background tab without disturbing focus;screenshotbriefly activates the tab first, since only a window's visible tab can be captured.- With multiple windows, "active tab" is just whatever window you focused
last — so for deterministic targeting, look the tab up with
list_tabsand pass itstabId. - Every action echoes the resolved
tab_id(andlist_tabsflags the current tab viacurrent: true/current_tab_id), so once a tab is referenced — even "the current tab" — its id can be captured and reused astabIdto keep follow-up actions pinned to it across turns.
Security model (POC-level)
- Two-token split: POSTER (CC writes), PULLER (extension reads). One leak doesn't grant both directions.
- Tokens via env var. Rotate the defaults before exposing publicly.
- Relay binds to
127.0.0.1by default. Cross-network deploy: front with TLS (Caddy / Cloudflare Tunnel / Tailscale Funnel — out of POC scope). - Extension
host_permissions: ["<all_urls>"]for the walking skeleton — tighten to per-domain allowlist for production. - Audit log captures every register / cmd / result with timestamps.
Inspect:
sqlite3 relay/data/relay.sqlite "select * from audit order by id desc limit 50".
Known POC gaps
- Single-target only; no broadcast / multicast.
- No TLS layer (relay is plain HTTP + WS).
- No per-domain confirmation policy (only the heuristic match on selector text).
- No retry/backpressure on flood-enqueue.
- MCP plugin not published to a marketplace yet.
- Firefox: loads only as a temporary add-on (not signed by Mozilla yet).
Roadmap
- Plugin packaging (
.claude-plugin/) so users canclaude plugin install rx-browser-bridge@arikw. - TLS sidecar (Caddy compose service).
- Per-domain confirmation policy + "trust for 10min" toggle.
- Cross-network deploy guide (Tailscale / Cloudflare Tunnel).
- Tab targeting (URL match, window selector).
License
MIT.
More projects
See all →2026
RX Claude Code Matrix Bridge
Live two-way Matrix ↔ Claude Code TUI bridge via the Channels API research preview. Plugin + always-on daemon + per-session routing + headless fallback.
2
2020
Extension Page Context Runner
Run functions in page context directly from a browser extension content script
27
2020
Outlook Duplicated Items Remover
A VBA script that finds and moves duplicated items in selected outlook folders
21
2023
RX Input Layout Switcher
Alt+Shift keyboard layout switcher GNOME Shell Extension
16 9.8K
2025
Podman OpenRC Restart Policy Manager
This OpenRC init script manages the automatic startup and shutdown of Podman containers with the `restart-policy=always` on system boot or shutdown
5
2022
Express Response Hooks
Extends Express.js with response hooks
4 159.3K 5.3K/mo