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, any │
│ Chromium browser) │
└──────────────────┘
- 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:
list_browsers,screenshot,navigate,new_tab,click,fill,query. - 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. Once done, the browser tools are available in every Claude Code session, regardless of which directory you launch from.
The install location is up to you — examples below use
~/tools/rx-browser-bridge-mcp. Adjust the absolute path in step 5 to
wherever you actually cloned.
1. Clone and build
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 # produces mcp/dist/server.js
(PUPPETEER_SKIP_DOWNLOAD=true skips puppeteer's bundled Chromium —
tests use system chromium at /usr/bin/chromium. Drop the flag if you
don't have one.)
2. Configure secrets
cp .env.example .env
# edit .env — set POSTER_TOKEN and PULLER_TOKEN to random values
# (openssl rand -hex 32 each is a good default)
The same .env is read by all three components — relay, MCP server,
and docker / podman compose. Edit once, applies everywhere.
3. Boot the relay
# Option A — tsx (no build step, foreground)
npm -w relay run dev
# Option B — container via compose (auto-discovers compose.yaml)
cd relay && docker compose up --build # or: podman compose up --build
# Option C — without compose (flags mirror compose.yaml)
cd relay && docker build -t rx-browser-bridge-relay:dev .
docker run -d --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:dev
Defaults:
- Listens on
127.0.0.1:3000 - Audit log at
relay/data/relay.sqlite(or/data/relay.sqliteinside the container, bind-mounted torelay/data/on the host)
4. Load the extension into your browser
chrome://extensions(or your Chromium browser's equivalent) → enable "Developer mode" → "Load unpacked" → pick theextension/directory.- Open the extension's Options page and fill in:
- Relay URL:
ws://localhost:3000/ws - Puller token: paste
PULLER_TOKENfrom.env - Browser id: friendly slug, e.g.
office - Tags: comma-separated, e.g.
reddit, hn
- Relay URL:
- For the
evaluatetool to run on CSP-locked sites (GitHub, etc.), open the extension's Details and enable "Allow user scripts" — Chrome's gate for theuserScriptsAPI. Without it,evaluatestill works on pages whose own CSP permitseval.
Click the toolbar icon → popup shows connection status, the last 50 audit entries, 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 |
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 |
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.
- Extension icons are 1×1 placeholders.
- No tab targeting beyond "active tab in active window" — can't drive background tabs.
- MCP plugin not published to a marketplace 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).
- Firefox MV3 manifest variant.
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.6K
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 149.3K 5.5K/mo