browser-control 1.1.0

CLI that manages browsers and exposes them over CDP/BiDi for agent-driven development. Includes an optional MCP server.
Documentation

browser-control

browser-control is a Rust CLI that manages browser processes and exposes them over CDP (Chromium) or WebDriver BiDi (Firefox) for agent-driven development. It keeps a small persistent registry of the browsers it has started so multiple agents, shells, or editor sessions can coordinate on the same browser. An optional MCP server is available as a subcommand.

Install

Homebrew (recommended) — tap this repo, then install:

brew tap rickardp/browser-control https://github.com/rickardp/browser-control.git
brew install browser-control

The formula is rendered into Formula/browser-control.rb by CI on every release, so the tap URL above is all you need.

From crates.io:

cargo install browser-control

Prebuilt binaries for macOS (x86_64/aarch64), Linux (x86_64/aarch64) and Windows (x86_64) are attached to every GitHub Release.

On Windows, install via the PowerShell one-liner:

irm https://raw.githubusercontent.com/rickardp/browser-control/main/scripts/install.ps1 | iex

It downloads the latest release zip, extracts browser-control.exe to %USERPROFILE%\.browser-control\bin, and prepends that directory to your user PATH. The script is idempotent — re-running it upgrades to the latest release, and is a no-op when the requested version is already installed. To pin a version, force a reinstall, or skip the PATH update:

$script = irm https://raw.githubusercontent.com/rickardp/browser-control/main/scripts/install.ps1
& ([scriptblock]::Create($script)) -Version 0.3.5
& ([scriptblock]::Create($script)) -Force
& ([scriptblock]::Create($script)) -NoPathUpdate

To uninstall:

irm https://raw.githubusercontent.com/rickardp/browser-control/main/scripts/uninstall.ps1 | iex

By default the user data directory (%APPDATA%\browser-control, containing the browser registry and config) is preserved. Pass -Purge to remove it as well:

$script = irm https://raw.githubusercontent.com/rickardp/browser-control/main/scripts/uninstall.ps1
& ([scriptblock]::Create($script)) -Purge

Requires Rust 1.80 or newer when building from source. A Node runtime (bun preferred, node+npm accepted) is required only if you invoke the Playwright-only MCP tools (browser_click, browser_snapshot, etc.); the sidecar is spawned lazily on first use.

Usage

The CLI groups commands into lifecycle, config, browser-wide session ops, page-context ops, and named tabs. Most commands accept --json for machine-readable output.

Browser selection

Page-context commands (eval, fetch, storage, …) and browser-wide commands (targets, cookies, wait, wait-for-cookie) accept a --browser / -b flag (with $BROWSER_CONTROL env fallback) to select the target browser. The external curl wrapper accepts --browser only because curl itself reserves -b for cookies:

browser-control eval -b firefox 'document.title'
BROWSER_CONTROL=chrome browser-control cookies --format netscape
browser-control fetch --browser brave https://example.com/api/me
browser-control curl --browser brave -L -o report.zip https://example.com/report.zip

Resolution order: --browser flag → $BROWSER_CONTROL env → browser-control set default <value> → error.

Page-context commands also accept a tab suffix: --browser brave/cart routes to a named tab. Browser-wide commands reject tab suffixes with an error.

list-installed

Detect every supported browser installed on this machine.

browser-control list-installed
browser-control list-installed --json

Supported kinds: chrome, edge, chromium, brave (CDP), and firefox (BiDi).

list-running

List the browsers currently registered and alive. Stale entries (dead PIDs or unreachable endpoints) are pruned lazily before printing.

browser-control list-running

Columns: NAME, KIND, PID, ENGINE, ENDPOINT, PROFILE, STARTED.

--json adds engine-specific endpoint details for tooling integration: cdp_port and cdp_ws_url for CDP browsers, bidi_ws_url for Firefox. Stale rows are re-probed before the WS URLs are printed; fields are omitted when the probe fails.

start [BROWSER]

Start a browser and register it. Idempotent by kind: if a browser of the requested kind is already alive, it is reused.

browser-control start                   # first available Chromium-based
browser-control start firefox
browser-control start chrome --headless --json

BROWSER may be a kind (chrome, edge, chromium, brave, firefox) or a friendly instance name printed by a previous start (e.g. firefox-pikachu). When omitted, the first available Chromium-based browser is used.

start blocks until the browser's debugging endpoint is reachable (up to --wait-timeout seconds, default 30) so the next command in a chain can attach immediately. Pass --no-wait to return as soon as the process is spawned.

start always uses a stable per-kind profile directory under the OS app-data dir (macOS: ~/Library/Application Support/browser-control/profiles/<kind>/default/; Linux: ~/.config/browser-control/profiles/<kind>/default/; Windows: %APPDATA%\browser-control\profiles\<kind>\default\), so subsequent starts of the same kind reuse the same browser state across reboots. This is intentional: it avoids re-authenticating in every new browser session.

GUI browser launches are kept in the background by default. Chromium-family browsers are started minimized, automated CDP-created tabs are opened in the background, and macOS launches hide the just-started browser after the debug endpoint is ready. Use show only when a human needs to interact with the browser.

show [--browser BROWSER]

Explicitly reveal the selected browser for login or debugging.

browser-control show -b brave
browser-control show --json

show uses the same browser selector rules as other browser-wide commands. It activates the browser app when possible and brings a live page target to the front, creating an about:blank target if none exists.

mcp [--browser BROWSER] [--playwright-version <X.Y.Z>]

Start an MCP server on stdio that targets a running browser.

browser-control mcp                             # use persisted default browser
browser-control mcp -b firefox                   # target a specific kind
browser-control mcp --playwright-version 1.55    # pin a custom playwright-core

Browser resolution order:

  1. The --browser / -b flag (or BROWSER_CONTROL env, merged by clap; the flag wins when both are present)
  2. The persisted default from browser-control set default <value>
  3. Otherwise, exit with an error

The server exposes engine-agnostic tools (browser_navigate, browser_get_html, browser_eval, browser_fetch, browser_curl, browser_take_screenshot, browser_storage_get, browser_cookies, …) that work on every supported browser including Firefox. Playwright-only interaction tools (browser_click, browser_type, browser_snapshot, browser_press_key, browser_drag, browser_hover, browser_wait_for, browser_pdf_save) route through an internal Node sidecar that wraps playwright-core. On the first call to one of these tools the sidecar is spawned (prefers bun, falls back to node+npm) against the active browser's CDP endpoint; on Firefox they return EngineUnsupported. The --playwright-version flag overrides the pinned playwright-core version for the sidecar.

browser_select switches the MCP server's active browser before preparing engine-specific state such as the Firefox BiDi lock. If that preparation fails (for example, another process holds Firefox's single BiDi session), the server keeps the newly selected browser active and reports the failure. The caller can then retry the same selection after the lock clears, switch to another browser, or switch back explicitly.

Claude Code and Codex plugins

This repository can be installed as a Claude Code or Codex plugin. The plugin bundles the MCP server configuration in .mcp.json and runs the installed browser-control mcp command, so install browser-control first. On startup, the MCP server resolves the configured browser, falls back to the most recent live browser, or starts the default installed browser when none is live. Agents can recover from a terminated browser with browser_start, or switch directly with browser_select using the same selector syntax as the CLI (brave, firefox, brave/cart, etc.):

brew install browser-control
browser-control set default brave

For local Claude Code testing from this checkout:

claude plugin marketplace add .
claude plugin install browser-control@browser-control

For local Codex testing, expose this checkout through the personal marketplace layout (~/.agents/plugins/marketplace.json points at ~/plugins/browser-control), then install it:

mkdir -p ~/plugins
ln -sfn "$PWD" ~/plugins/browser-control
codex plugin add browser-control@personal

set | get | unset <KEY> [VALUE]

Manage persistent settings. The only key today is default, which selects the browser used by mcp when no positional argument and no BROWSER_CONTROL env var is present. Values accept the full BROWSER_CONTROL grammar (URL / kind / friendly name / absolute path) and are validated at set-time.

browser-control set default firefox
browser-control set default ws://127.0.0.1:9222/devtools/browser/abc
browser-control get default
browser-control unset default

The setting is stored as TOML at:

  • macOS: ~/Library/Application Support/browser-control/config.toml
  • Linux: ~/.config/browser-control/config.toml
  • Windows: %APPDATA%\browser-control\config.toml

Override the directory with BROWSER_CONTROL_CONFIG_DIR.

The BROWSER_CONTROL environment variable

A single environment variable selects which browser the current shell session should talk to. It serves as the fallback for the --browser / -b flag on most subcommands. The syntax of the value decides how it is interpreted:

Value form Behavior
http(s)://… or ws(s)://… URL External CDP/BiDi endpoint. Used as-is; not registered and not managed by browser-control.
Friendly name (e.g. firefox-pikachu) Exact match against the registry.
Kind (chrome, firefox, …) First running instance of that kind in the registry.
Absolute path to a browser executable Matched against list-installed to derive the kind, then resolved as a kind.

Engine (CDP vs BiDi) is auto-detected for URL forms by probing.

HTTP, cookies, and storage

A small set of session subcommands lets agents and shell scripts use a real browser session — with its cookies, headers, TLS stack, ad-blockers, and geo — without scraping cookies.sqlite, re-implementing OAuth flows, or driving a second headless browser. They attach to a browser already registered by start, work over both CDP and BiDi (Firefox), and accept --browser / -b (or $BROWSER_CONTROL) for browser selection. None of them launch a browser; run start first.

targets

List open page targets (and optionally filter by URL regex). Browser-wide — tab suffixes are not supported.

browser-control targets                                        # table: KIND ID URL TITLE
browser-control targets -b firefox --url '^https://example\.com'
browser-control targets --json

cookies

Export cookies from the live browser, normalised across CDP and BiDi. Browser-wide — tab suffixes are not supported.

browser-control cookies --domain '\.example\.com$' --name '^session'    # JSON (default)
browser-control cookies --format header
browser-control cookies -b brave --format netscape -o cookies.txt       # curl/yt-dlp jar (0600)
browser-control cookies --reveal

--domain and --name are unanchored Rust regexes. Without --reveal, values printed to a TTY are redacted; file output via -o always contains full values and is chmod 0600 on Unix. --format netscape produces a file byte-compatible with the Mozilla cookies.txt format (see docs/session-ops.md).

Page-context reads that commonly surface auth state (fetch, eval, storage get, storage list, and wait-for-cookie --validate-url) reload HTTP(S) pages whose document is older than 10 minutes before evaluating, so SSO has a chance to refresh tokens. Override with --max-age 1h, --max-age 30s, etc.

fetch

Run an HTTP request from inside the page's JavaScript context. Cookies, Origin, CORS, and the browser's TLS stack apply — handy for hitting an API that requires the user's session. Page-context — supports tab suffixes via -b browser/tab.

browser-control fetch https://example.com/api/me
browser-control fetch -b brave -X POST -H 'Content-Type: application/json' \
    -d '{"q":1}' https://example.com/api/search
browser-control fetch --target '^https://app\.example\.com' -i \
    -o body.json https://app.example.com/api/data

-i prepends status line + response headers (like curl -i). -o FILE writes the body to FILE (0600 on Unix).

By default fetch runs in a tab on the URL's origin, reusing an existing same-origin tab if one is open and otherwise opening a new tab navigated to the origin root. This guarantees the request carries the cookies and honours the CORS rules of the target site, regardless of which tab the user is currently looking at. The auto-opened tab is left open so subsequent fetches against the same origin reuse it. Pass --target URLREGEX to override and explicitly pick a tab by URL regex.

curl

Run the real system curl outside the page context with a snapshot of the selected browser's cookies and User-Agent. Every argument after the wrapper's --browser option is forwarded unchanged to curl. --browser deliberately has no -b alias because curl uses -b for its own cookie option.

browser-control curl --browser brave -L --fail-with-body \
    https://example.com/api/export
browser-control curl --browser brave/work -L --fail-with-body \
    -o archive.zip https://example.com/archive.zip
browser-control curl --browser brave -- --help

Browser-derived --cookie <temporary-netscape-jar> and --user-agent options, plus Origin and Referer from the selected source tab, are prepended to the curl arguments. With a bare browser selector, a live tab supplies the request context when available. The temporary cookie file is mode 0600 and is deleted after curl exits. Cookies set by curl are not written back to the browser.

Use fetch when browser-faithful behavior matters: it uses the browser's TLS stack and remains subject to page CORS/CSP. Use curl for large or binary responses and direct file downloads. Curl is not subject to browser CORS/CSP, but its TLS fingerprint, SameSite behavior, client hints, and partitioned-cookie handling can differ from the browser.

The corresponding MCP tool is browser_curl. Its args array uses ordinary curl syntax. Stdout responses up to 8 MiB are returned through MCP as text or as a base64 embedded resource for binary data. Larger MCP responses are stopped with an error; pass curl -o <path> or --output <path> to stream an unrestricted download directly to disk.

storage

Read and write localStorage (default) or sessionStorage (--namespace session). Storage is origin-scoped, so most uses want --target. Page-context — supports tab suffixes via -b browser/tab.

browser-control storage get auth_token --target '^https://app\.example\.com'
browser-control storage set theme dark -b brave --target '^https://app\.example\.com'
browser-control storage list --namespace session --key-regex '^feature_' --json

eval

Evaluate a JavaScript expression in the active page. Returns the result as plain text by default; --json emits the full evaluation envelope. Page-context — supports tab suffixes via -b browser/tab.

browser-control eval 'document.title'
browser-control eval -b brave/cart --json 'fetch("/api/whoami").then(r => r.json())'
browser-control eval --target '^https://app\.example\.com' 'document.cookie'

--await-promise is on by default, so async expressions just work.

wait

Block until the browser's CDP / BiDi endpoint is up. Useful right after start in scripts. Browser-wide — tab suffixes are not supported.

browser-control start firefox && browser-control wait -b firefox --timeout 30

wait-for-cookie

Block until a cookie matching --domain REGEX --name REGEX exists in the browser. Optional --validate-url URL follows up with a fetch() from the page and requires a 2xx response before exiting — the typical pattern for "wait until the user has finished logging in". Browser-wide — tab suffixes are not supported.

browser-control wait-for-cookie \
    --domain '\.example\.com$' --name '^session_token$' --timeout 120
browser-control wait-for-cookie -b brave --domain example.com --name auth \
    --validate-url https://example.com/api/session

Exit status is 0 on match, non-zero on timeout.

Migrating from hand-rolled helpers

A typical "launch browser, wait for login, call API" shell flow collapses to:

browser-control start brave
browser-control wait-for-cookie -b brave --domain clientzone.gamesglobal.com \
    --name '__Secure-next-auth.session-token' --timeout 120
SESSION_JSON=$(browser-control fetch -b brave \
    https://clientzone.gamesglobal.com/api/auth/session)

And any Python write_netscape_cookie_jar() helper that reads cookies.sqlite directly is replaced by:

browser-control cookies --format netscape -o cookies.txt
# then: curl --cookie cookies.txt https://…   or   yt-dlp --cookies cookies.txt …

Named tabs

Named tabs let agents manage isolated tab contexts. tab open creates (or reuses) a named tab; tab list shows them; tab adopt binds an existing unnamed tab to a name.

browser-control tab open brave/cart https://shop.example.com     # create named tab
browser-control tab list brave                                    # list registered tabs
browser-control tab list brave --all                              # include unnamed live tabs
browser-control tab adopt brave/my-tab ABC123DEF                  # adopt by target ID
browser-control eval -b brave/cart 'document.title'               # use in page-context commands

tab list --all surfaces unnamed tabs with their target IDs. Use tab adopt <browser>/<name> <target-id> to bind them to a name, making them addressable via -b <browser>/<name> in eval, fetch, storage.

MCP integration

browser-control is itself an MCP server when invoked as mcp. Add it to your host's .mcp.json like any other stdio server.

Default tools (exposed by the Rust server) include navigate, get_dom, screenshot, fetch, select_element, plus the session ops introduced in this release: list_targets, cookies, storage_get, storage_set, and wait_for_cookie. See docs/session-ops.md for the underlying model.

{
  "mcpServers": {
    "browser-control": {
      "command": "browser-control",
      "args": ["mcp"]
    }
  }
}

You can scope a single host invocation to a specific browser by setting BROWSER_CONTROL:

{
  "mcpServers": {
    "browser-control": {
      "command": "browser-control",
      "args": ["mcp"],
      "env": { "BROWSER_CONTROL": "firefox" }
    }
  }
}

Architecture

browser-control is a thin CLI in front of a SQLite registry of browser processes. The CLI starts and tracks browsers; agents talk to those browsers directly over CDP or BiDi. The MCP server is just another way to reach the same registry.

                  ┌───────────────────────────────────────┐
                  │ SQLite registry (OS app-data dir)     │
                  └───────────────────────────────────────┘
                                   ▲
                                   │ read / write
                                   │
   user ──► browser-control start ─┴─► spawns ──► Browser (Chrome/Edge/Firefox/…)
                                                         ▲
                                                         │ CDP / BiDi
                                                         │
              MCP host ──► browser-control mcp ┘
                              (resolves browser via registry / BROWSER_CONTROL)
                              (Playwright tools route through internal sidecar)

The CLI does not stop or restart browsers; the user owns lifecycle. Stale registry entries are pruned lazily on read.

Status

Pre-1.0. The CLI surface and the BROWSER_CONTROL environment variable are the intended stable contracts; everything else may shift.

The previous TypeScript MCP server (@anthropic-community/browser-coordinator-mcp) is preserved on the legacy-ts branch and tagged v0-final-ts. Its npm package is deprecated.

License

MIT. See LICENSE.