browser-control 1.2.1

CLI that manages browsers and exposes them over CDP/BiDi for agent-driven development. Includes an optional MCP server.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
# 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:

```sh
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`](Formula/browser-control.rb) by CI on every release, so the tap URL above is all you need.

From crates.io:

```sh
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](https://github.com/rickardp/browser-control/releases).

On Windows, install via the PowerShell one-liner:

```powershell
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:

```powershell
$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:

```powershell
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:

```powershell
$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-sidecar MCP tools (CSS-selector `browser_click`/`browser_type`,
`browser_press_key`, `browser_wait_for`, `browser_pdf_save`); the sidecar is
spawned lazily on first use. Snapshots, refs, and console/network capture
are native and need no Node.

## 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:

```sh
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.

```sh
browser-control list-installed
browser-control list-installed --json
```

Supported kinds: `chrome`, `edge`, `chromium`, `brave` (CDP), and `firefox`
(BiDi). The two engines share one tool surface; every behavioural difference
is listed in [docs/engine-parity.md](docs/engine-parity.md).

### `list-running`

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

```sh
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.

```sh
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.

```sh
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.

```sh
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 three groups of tools:

- **Engine-agnostic** (every supported browser including Firefox):
  `browser_navigate`, `browser_get_html`, `browser_get_page_text`,
  `browser_eval`, `browser_fetch`, `browser_curl`, `browser_take_screenshot`,
  `browser_storage_get` / `browser_storage_set`, `browser_cookies`,
  `browser_wait_for_cookie`, tab and browser management.
- **Native, no Node** (Chromium and Firefox): `browser_snapshot` (accessibility
  tree with stable `[ref=eN]` handles, `interactive_only`, `ref`, `depth`,
  `max_chars`), `browser_find` (refs for a short description), the `ref` path
  of `browser_click` / `browser_type` / `browser_hover` / `browser_drag` /
  `browser_take_screenshot`, and console/network listing:
  `browser_console_messages`, `browser_network_requests`. On Chromium the
  tree comes from CDP's accessibility domain; on Firefox from an injected DOM
  walker, so names and roles are approximate and closed shadow roots are not
  visible.
- **Chromium-only extras**: `browser_network_body` (Firefox exposes no
  captured bodies; use `browser_fetch` there), screenshot `max_width` (BiDi
  has no scale), and `browser_tab_foreground` (foreground emulation, below).

Where the engines differ in detail (accessible-name approximation on Firefox,
typing semantics, which console entries exist, iframe capture, session
lifecycle), see [docs/engine-parity.md](docs/engine-parity.md).
- **Playwright sidecar** (Chromium family, needs `bun` or `node`): CSS
  `selector` interaction on `browser_click` / `browser_type` /
  `browser_hover` / `browser_drag`, plus `browser_press_key`,
  `browser_wait_for`, `browser_pdf_save`. On the first call the sidecar is
  spawned (prefers `bun`, falls back to `node`+`npm`) against the active
  browser's CDP endpoint. The `--playwright-version` flag overrides the pinned
  `playwright-core` version.

#### Foreground emulation

Chromium treats a tab in a minimized window, or any tab while the display is
locked, as hidden: `document.visibilityState` is `hidden`, `document.hasFocus()`
is false, `requestAnimationFrame` never fires, and timers run once per second.
Games and canvas apps stop, and screenshots show a frozen frame.
`browser_tab_foreground` (MCP) and `browser-control tab foreground` (CLI) flip
that for one tab: the page reports visible and focused, animation frames and
timers run at full rate, and screenshots show live content, all without
revealing the browser. Both surfaces share one mechanism: a small detached
holder process (`browser-control tab foreground-hold`) keeps the CDP session
that carries the emulation open, and records its PID in the registry so either
surface can turn it off and `tab list` shows the flag. It stays on until turned
off, its timeout elapses (default 1 hour, `timeout` / `--timeout`), the tab
closes, or the browser exits. `browser_tab_foreground { enabled: false, all:
true }` or `browser-control tab foreground <browser> off` stops every holder on
a browser, for cleaning up after agents that forgot. Chromium only (Firefox has
no BiDi equivalent). An emulated tab uses CPU as if it were visible, and pages
that gate autoplay or notifications on focus will consider the user present.

```sh
browser-control tab foreground brave/game on --timeout 2h
browser-control tab foreground brave/game off
browser-control tab foreground brave off      # every tab on the browser
```

Console and network capture starts the moment a tool first touches a tab and
keeps the last 1000 console entries and 500 requests per tab, across
navigations, until `clear: true`, the tab closes, or the browser is switched.
It is push-only: on Chromium the server enables the CDP `Runtime`, `Log`,
`Network`, and `Page` domains on a long-lived session per tab; on Firefox it
holds one WebDriver BiDi `session.subscribe` for `log.*`, `network.*`
(Firefox 124+; older builds degrade to console-only) and
`browsingContext.*`. Nothing polls. Set `BROWSER_CONTROL_CAPTURE=0` to
disable it (CDP `Runtime.enable` is detectable by some anti-bot scripts).

`browser_take_screenshot` returns an unscaled PNG by default; pass
`format: "jpeg"`, `quality`, `max_width` (downscale), or `save_to` (write the
file with mode 0600 and return only its path and dimensions) to keep
screenshots out of the conversation context.

`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.):

```sh
brew install browser-control
browser-control set default brave
```

For local Claude Code testing from this checkout:

```sh
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:

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

### `set | get | unset <KEY> [VALUE]`

Manage persistent settings. Keys:

- `default` 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.

```sh
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.

```sh
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.

```sh
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](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`.

```sh
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.

```sh
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`.

```sh
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`.

```sh
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.

```sh
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.

```sh
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:

```sh
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:

```sh
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.

```sh
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.

See the `mcp` subcommand section above for the full tool list. Session ops
(`browser_cookies`, `browser_storage_*`, `browser_wait_for_cookie`,
`browser_fetch`, `list_targets`) are described in
[docs/session-ops.md](docs/session-ops.md); the native accessibility and
capture tools in [docs/adrs/003-native-cdp-observe-and-interact.md](docs/adrs/003-native-cdp-observe-and-interact.md).

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

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

```json
{
  "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](LICENSE).