Rust-native MCP server for controlling a real local browser through a minimal WebExtension bridge.
bro is built for agents that need a shorter browser interaction loop:
- open many URLs in background tabs and extract useful text/links in parallel
- keep a single tab leased for sequential click/fill/read flows
- use a logged-in Chromium-family browser without sending browsing data through a cloud relay
- keep the MCP server, API policy, concurrency, and tests in Rust
The browser runtime itself is still a WebExtension because Chromium exposes
chrome.tabs, chrome.debugger, content scripts, and service workers only to
the extension environment. bro keeps that layer intentionally thin.
bro is inspired by and initially prototyped against OpenBrowserMCP. OpenBrowserMCP proved the local WebSocket bridge + browser extension shape; bro takes that idea into a Rust-native MCP server with a smaller agent-facing API, tighter regression tests, and a minimal extension adapter. OpenBrowserMCP is Apache-2.0, and this repository keeps the Apache-2.0 license and attribution in NOTICE.
Codex MCP / bro Pi adapter
-> bro Rust server (:3500 /mcp, /ws, /health, /status)
-> bro WebExtension adapter
-> Chromium-family browser tab
The Rust server owns:
- MCP Streamable HTTP
- WebSocket bridge registry
- tool schema and routing
- default-first browser facade tools
- batch scheduling and cleanup policy
- regression tests and release artifacts
The extension owns:
- connecting to
ws://127.0.0.1:3500/ws - authenticating with the local token
- executing browser primitives via WebExtension APIs and CDP
Install with Homebrew:
brew install xiaotianxt/tap/bro
brew services start bro
bro setup codex
bro setup browserFor native bro tools in Pi, install the bro-owned adapter and reload Pi:
pi install git:github.com/xiaotianxt/[email protected]Install the optional Codex/agent skill for bro-specific browser workflows:
npx -y github:xiaotianxt/skills bro-browserAfter the npm package is published, the equivalent command is:
npx -y @xiaotianxt/skills bro-browserThe service runs bro serve locally and exposes MCP at
http://127.0.0.1:3500/mcp. bro setup codex updates
~/.codex/config.toml to connect Codex to that local MCP endpoint with the
bearer token from ~/.bro/settings.json. Restart Codex after running it.
bro setup browser copies the token to your clipboard, opens the browser
extension page, and reveals the unpacked extension directory. In the browser,
enable Developer mode, choose Load unpacked, select the shown directory, then
open bro Options and paste the token.
Use brew services restart bro after upgrading.
For source development:
git clone https://github.com/xiaotianxt/bro.git
cd bro
cargo run -- serveThe first run creates ~/.bro/settings.json:
{
"token": "..."
}Existing settings are read without modification. If the file is malformed or contains an empty token, bro reports the error and leaves the file unchanged.
Build the extension:
pnpm install
pnpm --filter @bro/shared build
pnpm --filter @bro/extension typecheck
pnpm --filter @bro/extension test
pnpm --filter @bro/extension buildFor source checkouts, pass the local extension directory explicitly:
cargo run -- setup browser --extension-dir extension/distRun bro as a local service, then point your MCP client at the HTTP endpoint with
the local bearer token from ~/.bro/settings.json.
For Codex, prefer the setup command:
bro setup codexIt writes a bro MCP server entry like this:
[mcp_servers.bro]
url = "http://127.0.0.1:3500/mcp"
[mcp_servers.bro.http_headers]
Authorization = "Bearer <token from ~/.bro/settings.json>"The setup command does not print the token. If you configure another MCP client
manually, send the same token as an Authorization: Bearer ... header.
Codex also supports bearer_token_env_var, but bro setup codex uses a static
local header so the configuration does not depend on a particular shell startup
file.
The MCP client should connect to this endpoint. It should not spawn the server
per request; keep bro serve running through Homebrew services or another
local process supervisor. Do not commit browser output, cookies, signed URLs, or
tokens.
The Pi package in pi-extension/ uses Pi's extension API and the official MCP
TypeScript SDK. It reads the bearer token directly from ~/.bro/settings.json,
keeps one MCP connection per Pi session, and registers bro's live tool schemas
under a bro_ namespace. Ten outcome-level extraction, flow, network, and
search tools start active. bro_search_tools enables server-owned capability
packs for tabs, frames, accessibility, console, visual input, uploads,
shortcuts, and user scripts. Server-marked internal and compatibility tools are
not registered in Pi.
The adapter maps Pi session identity into bro's tab lifecycle, preserves browser
state across /reload, and finalizes owned tabs and unfinished flows when the
session ends. It remains a protocol adapter: browser policy, tool schemas,
timeouts, and cleanup behavior stay in the Rust server.
For development from this checkout:
pi install /absolute/path/to/broHomebrew installs the unpacked bro extension under share/bro/extension.
bro setup browserThe command:
- finds the installed extension directory
- copies the local bro token to the clipboard when the OS clipboard tool is available
- opens
chrome://extensions/when possible - reveals the extension directory in the file manager when possible
Chromium-family browsers still require a user gesture to load unpacked extensions. In the browser:
- Enable Developer mode.
- Choose Load unpacked.
- Select the extension directory printed by
bro setup browser. - Open bro Options.
- Paste the copied token and save.
The extension accepts only loopback ws:// server URLs. Its token is stored
locally in the extension profile and is not synced between browsers. A token
saved by an older version is moved out of sync storage on first use. Connected
status is shown only after the server authenticates the extension.
Connection settings stay collapsed after setup so the Options page can focus on
persistent inline user scripts. It can list, create, edit, and delete scripts
registered through either the UI or bro's userscripts_* tools. Enable Allow
User Scripts for the extension in chrome://extensions before using this
feature. File-backed scripts can be listed and deleted but cannot be edited in
the UI. Each script can include a short description so its purpose is visible
without opening the source. bro keeps its own local registry and restores
registered scripts if a browser extension update clears Chrome's runtime copy.
Verify the connection:
curl -fsS http://127.0.0.1:3500/status
bro call browsers_contextextensionCount should be at least 1.
browsers_context also includes best-effort native metadata under
nativeInfo when the local server can identify the browser process. On macOS
and Unix-like systems this includes fields such as appName, processId,
executablePath, userDataDir, profilePath, cookieStorePath, and
safeStorageService. Use nativeInfo for cookie/storage export workflows;
browserInfo is only the browser identity visible to the WebExtension.
browser.extract: open one URL, extract visible text, close by defaultbrowser.current.extract: extract the current/default active tab in one callbrowser.batch.extract: extract many URLs in parallelbrowser.batch.run: open many URLs, read plain text, close by defaultbrowser.batch.flow: run the same ordered interaction on many URLs in parallel, close by defaultbrowser.console.capture: open a page, trigger console output, collect logs or exceptions, and clean up in one callbrowser.network.capture: open a page, trigger requests, and collect matching request metadata and bounded response bodies in one callbrowser.flow.start: lease one tab for sequential workbrowser.flow.observe: read leased-tab text or accessibility treebrowser.flow.act: run ordered generic steps such asgoto,click,fill,select, and awaitedevalbrowser.flow.finish: release and optionally close the leased tab
Use browser.batch.flow when each page needs the same interaction before
reading data, for example opening a reviews modal on every product page and
then returning a structured eval result. It keeps the repeated workflow inside
one MCP call with bounded per-URL timeouts and cleanup, instead of requiring the
agent to start, act on, and finish many separate flow sessions.
Raw browser primitives such as tabs_create, tabs_close, read_page,
frames_list, javascript_tool, click_element, fill_element, and
computer remain available for lower-level work. Real computer input brings
the target tab and window to the foreground; screenshots and zoom remain
background-capable. Use frames_list frame IDs with frame-aware flow steps for
iframe interaction.
Extraction defaults are intentionally compact for agent workflows:
- text is capped at 8,000 characters by default
- links are omitted unless
includeLinks:true - accessibility-tree fallback is omitted unless
includeA11y:true - screenshots default to lower JPEG quality; pass
qualityonly when visual detail matters
pnpm install --frozen-lockfile
make checkRun live dynamic-site regression tests when a local bro server and extension are connected:
make live-testThe live tests open Reddit, LinkedIn, X, and Threads search pages in background tabs, verify dynamic extraction, and exercise one-call request/body capture against httpbin.
Run isolated multi-model Pi browser benchmarks from
benchmarks/pi-agent/. Keep raw sessions outside the repository because they
can contain sensitive browser content. The current tool-design findings are in
docs/pi-tool-design.md.
Tagged releases build GitHub-hosted binaries for:
x86_64-unknown-linux-gnuaarch64-unknown-linux-gnux86_64-apple-darwinaarch64-apple-darwinx86_64-pc-windows-msvcaarch64-pc-windows-msvc
The Homebrew formula in xiaotianxt/tap installs the GitHub release binary for
the current macOS or Linux architecture, installs the matching browser
extension asset, and exposes brew services start bro. Releases also include an
npm-compatible tarball for the Pi adapter; the tagged Git repository is directly
installable as a Pi package.
Maintainers release from a clean main checkout with
scripts/release.sh --version <version>. The script runs the checks, creates and
pushes the tag, waits for the GitHub release artifacts, updates the Homebrew tap,
and verifies the installed package.
bro controls a logged-in browser. Keep it local.
- The server binds to
127.0.0.1by default. - The WebSocket bridge requires the token from
~/.bro/settings.json. - Do not expose port
3500to untrusted networks. - Do not commit browser output, cookies, signed URLs, or tokens.