Skip to main content
Glama
Atlas-X-AI

Shade Browser Security MCP

by Atlas-X-AI
README.md
# Shade Browser Security MCP

This repository exposes only the explicitly classified SECURITY tools from
Shade Browser through a standalone MCP server. The implementation reuses the
shared `shade_browser_mcp` browser core vendored in this repository; the
public `shade-browser-security` entry point registers a separate FastMCP
instance with the SECURITY allow-list only.

Run locally:

```bash
pip install -e '.[dev]'
shade-browser-security
```

The reduced/general server is maintained separately in the
`shade-browser-mcp` repository's `reduced-safe` branch.

MCP server for Brave CDP browser automation across 20 flock-protected profile slots. Built on FastMCP + Playwright Python.

## What this is

A one-shot-installable Model Context Protocol server that gives Claude Code a Brave browser with real cookies, auto-launched on demand, across 20 isolated slots (ports 61000-61019). Each slot gets its own profile copy by default, so parallel agents never collide on tabs or sessions. Persistent mode reuses a stable per-slot profile for manual logins, extensions, and browser config.

## Install (3 commands)

```bash
uv tool install git+https://github.com/Atlas-X-AI/shade-browser-mcp
shade-browser-mcp postinstall
claude mcp add shade-browser-mcp --scope user -- shade-browser-mcp
```

Then restart Claude Code. Tools appear as `mcp__browser-automation__*`.

### What each step does

| Step | What it does |
|------|--------------|
| `uv tool install git+...` | Installs the Python package globally (isolated venv managed by uv) |
| `shade-browser-mcp postinstall` | Runs `playwright install chromium` — one-time Playwright browser fetch |
| `claude mcp add ... --scope user ...` | Registers the MCP in `~/.claude.json` (user scope = available in all projects) |

### Upgrade an existing user-scope MCP install

Claude Code user-scope MCP registration runs the `uv tool install` copy of
`shade-browser-mcp`. New tools do not appear in already-installed Hayden agent
sessions until that tool install is replaced and Claude Code reconnects to the
server.

For local development on the checked-out branch:

```bash
git switch feat/form-filler-bolt-on
uv tool install --force --editable .
```

Or install this branch directly from Git:

```bash
uv tool install --force git+https://github.com/Atlas-X-AI/shade-browser-mcp@feat/form-filler-bolt-on
```

Then fully exit and relaunch each Claude Code session that should see the new
schema. Existing sessions keep the old MCP process and old tool list until they
restart or reconnect that MCP server.

Smoke-test the installed server's tool registry:

```bash
uvx --from fastmcp fastmcp list --command "shade-browser-mcp" --json \
  | grep -E '"name": "form_(scan|fill|gate_check)"'
```

## Prerequisites

- **Python 3.10+**
- **uv** — `curl -LsSf https://astral.sh/uv/install.sh | sh`
- **Claude Code CLI** (for `claude mcp add`)
- **Brave browser** at `/opt/brave-bin/brave` or `/usr/bin/brave-browser`
- **Brave profile** at `~/.config/BraveSoftware/Brave-Browser` (that's where your cookies come from — each slot gets a fresh `cp -a` copy)

Run `shade-browser-mcp doctor` after install to verify all six prerequisites.

## Commands

```
shade-browser-mcp              # runs FastMCP stdio server (default — what Claude Code invokes)
shade-browser-mcp serve        # same as above
shade-browser-mcp postinstall  # playwright install chromium
shade-browser-mcp doctor       # health check: Brave, profile, Playwright, Claude registration
shade-browser-mcp --version
```

## Runtime identity and safe rolling reload

Call the machine-readable MCP tool below on every connected server and after
connecting to a candidate build:

```text
browser_runtime_status(
  client_id="launcher-a",
  expected_version="0.1.10",
  expected_revision="<candidate-revision>"
)
```

The response includes the running server's `version`, `revision`, `pid`,
UTC `started_at`, `transport`, effective focus defaults, and top-level
`reload_required`/`should_reload` booleans. A build mismatch is deterministic:
`status` is `reload_required`, `reload.action` is `reconnect`, and
`reload.process_action` is always `none`. The server does not kill or restart
itself and does not raise Brave, move the pointer, type, or call
`bring_to_front` for this protocol. Focus defaults are
`browser_navigate=false` and `browser_switch_tab=false`.

For a rolling reconnect, the launcher/client owns the transport lifecycle:

1. Start the candidate server alongside the current server, injecting an
   immutable build value such as
   `SHADE_BUILD_REVISION=$(git rev-parse --short HEAD)`.
2. Probe the candidate with `browser_runtime_status` without expected values
   and record its returned version/revision as the target identity.
3. Ask one client at a time to report status against that target identity. A
   client on the old server receives `reload_required=true`; close only that
   client's transport and reconnect it to the candidate. Other clients remain
   usable on the old server until their own turn.
4. Verify each reconnected client with the same expected version/revision. A
   matching response has `status=ready`, `reload_required=false`, and the new
   PID/start time. Only the launcher decides when the old server is retired;
   this tool never performs that action.

If `expected_version`/`expected_revision` is omitted, the tool reports the
identity but cannot detect staleness. If the revision is `unknown`, configure
`SHADE_BUILD_REVISION` in the launcher; do not infer freshness from PID or
start time. A reconnect failure leaves the existing client transport alone and
must be handled by the launcher/client's normal retry policy.

## Form-Filler Tools

Three bolt-on tools scan and fill ordinary web forms from a runtime JSON
profile path:

```text
form_scan(slot=-1)
form_fill(slot=-1, profile="docs/examples/form-fill-demo-profile.json", allow_submit=false)
form_gate_check(slot=-1, profile="docs/examples/form-fill-demo-profile.json")
```

`form_scan` returns field IDs, labels, required signals, option lists, upload
controls, submit/advance controls, and profile-key hints such as
`person.email`. `form_fill` reads the profile at call time, fills grounded
fields, verifies read-back, and returns counts plus a gate check. Its response
also includes additive `field_outcomes` entries with `tier_used`, `committed`,
`readback_value`, `escalate`, and `reason` for each resolved field. Outcomes may
also include `attribution`, for example `pre_filled_verified:radio`, when an
already-filled control was verified before any actuation. `form_gate_check`
verifies required fields, identity read-back, required uploads, visible page
errors, and detector health.

Profile shape:

```json
{
  "person": {"email": "casey@example.test", "first_name": "Casey"},
  "company": {"name": "Example Labs"},
  "files": {"Resume": "relative/or/absolute/path.txt"},
  "values": {"Work Type": "full time", "Location": ["Remote United States", "Remote"]},
  "never_submit": true
}
```

`values` entries override inferred `person`/`company` mappings by field ID or
label. Option choices are grounded only by exact text/value equality after
mechanical normalization, or by explicit profile variant lists. Relative upload
paths resolve relative to the profile file. If `never_submit` is true,
`form_fill(..., allow_submit=true)` still refuses to click submit.

Worked examples after navigating a browser slot to a form:

```text
form_scan(slot=4)
```

Returns form inventory such as field IDs, labels, required signals, upload
controls, options, submit controls, and inferred profile-key hints.

```text
form_fill(slot=4, profile="docs/examples/form-fill-demo-profile.json", allow_submit=false)
```

Fills grounded fields from the JSON profile, verifies read-back, uploads files
declared in `files`, and returns a nested gate check without submitting.

```text
form_gate_check(slot=4, profile="docs/examples/form-fill-demo-profile.json")
```

Reports whether required fields, identity fields, required uploads, visible
validation errors, and detector health are all ready for a guarded submit.

See `docs/examples/form-fill-demo-profile.json` and
`docs/examples/form-fill-live-slot-transcript.md` for a real-slot loopback
fixture transcript. `docs/examples/form-fill-local-transcript.md` is a shorter
local fixture summary. The consolidated capabilities, schema, safety, packaging,
and opt-in upgrade guide is `docs/form-fill/README.md`.

## Headed vs headless mode

By default, `shade-browser-mcp` launches Brave **headed** if `DISPLAY` or `WAYLAND_DISPLAY` is set, and **headless** otherwise (CI, SSH sessions without X forwarding, systemd units without a user display).

To force a visible window regardless of env:

```bash
export HEADED=1
shade-browser-mcp
```

To force headless on a desktop session:

```bash
unset DISPLAY WAYLAND_DISPLAY
shade-browser-mcp
```

The `HEADED=1` knob is useful when you want to watch an AI agent drive the browser, or when the MCP daemon was launched from an env that lacks `DISPLAY` but you later want to attach visibly.

## Persistent profiles and Bitwarden

Default slot mode refreshes `~/.cache/brave-cdp/slot-N/` from the real Brave profile on launch. Persistent mode uses `~/.cache/brave-cdp/persistent/slot-N/` and reuses that profile across launches, so manual ChatGPT login, installed extensions, and browser settings survive.

```bash
export SHADE_PROFILE_MODE=persistent
export SHADE_INGEST_BW=1
shade-browser-mcp
```

You can also launch one slot manually:

```bash
launch-brave-cdp 5 --persistent
```

`SHADE_INGEST_BW=1` runs `~/bin/bw-ensure.sh` when `BW_SESSION` is absent and passes the resulting `BW_SESSION` only through the MCP/Brave launcher environment. Tokens are not written to manifests, screenshots, or browser session files.

## Architecture (one-liner)

`BrowserManager._ensure_brave_alive()` on each tool call → httpx-probe port → if dead, `subprocess.run(bash launch-brave-cdp N)` with env including `DISPLAY`/`WAYLAND_DISPLAY`/`XAUTHORITY`/`XDG_RUNTIME_DIR` → poll 20s at 1Hz → `playwright.connect_over_cdp()`. Slots are flock-protected via `cdp_slots.claim_slot()`.

## Per-slot CDP auth proxy (Phase 1, library only)

A protocol-layer slot-ownership proxy ships in `shade_browser_mcp.cdp_proxy`. Each slot now carries an `auth_token` (256-bit, generated by `claim_slot`) and a `private_port` (62000+slot) where Brave will eventually bind. The proxy listens on the public port (61000+slot), validates `Authorization` via a `/t/<token>/...` URL prefix, and rewrites `webSocketDebuggerUrl` so Playwright's WS upgrade also carries the token.

CLI:

```bash
python -m shade_browser_mcp.cdp_proxy <slot>             # passthrough mode
python -m shade_browser_mcp.cdp_proxy <slot> --require-token  # enforce
```

**Phase 1 ships the library + tests + audit fixes only — the proxy is not yet wired into `_ensure_brave_alive`.** That production wiring (and the matching `launch-brave-cdp --private-port` flag) lands in a Phase 2 follow-up. Users who want to opt in early can drive the proxy module directly. See `DONE.md` for the migration ledger and audit-deferred items.

## Shade Fleet Console (local dashboard)

A read-mostly local web console for watching and steering the 20 slots. It is a
single-host operator tool, not a shared service.

```bash
export SHADE_DASHBOARD_TOKEN=$(openssl rand -hex 32)
shade-browser-mcp dashboard --port 8790
```

- **Loopback only.** The console binds `127.0.0.1` exclusively and *refuses* to
  start on any non-local host (no `--allow-nonlocal` escape hatch in v1).
- **Token via env.** The bearer token is read from the `SHADE_DASHBOARD_TOKEN`
  environment variable.
- **Opener URL carries the token in the fragment, never a query string.** On
  start the server prints `http://127.0.0.1:8790/#token=<token>`. The fragment
  (`#token=...`) is never sent to the server in a request line and never lands
  in access logs; a query string (`?token=...`) would, so it is deliberately
  not used.
- **API auth uses a header.** Every `/api/*` request must carry
  `Authorization: Bearer <token>`. The page JS reads the token from the URL
  fragment and replays it as that header; it is never written to
  `localStorage`/`sessionStorage`.

### Phase 2 endpoint model

Phase 2 splits each slot into a private upstream port and a public proxied port:

| Port | Who binds it | Reachable by | Auth |
|------|--------------|--------------|------|
| `127.0.0.1:6200N` (private) | Brave (CDP) | proxy only | n/a (loopback, private) |
| `127.0.0.1:6100N` (public) | per-slot auth proxy | local clients | tokenized path |

- Brave now binds the **private** per-slot port `127.0.0.1:6200N`.
- The per-slot auth proxy listens on the **public** port `127.0.0.1:6100N`.
- Internal Playwright attach uses the tokenized path
  `http://127.0.0.1:6100N/t/<token>` — the token rides in the URL path so it
  survives the WebSocket upgrade.
- **Direct unauthenticated access to `6100N` is rejected with `401`.** A missing
  or wrong token never reaches Brave.

### Synthetic Action View

The console renders each slot's recent activity from structured data, not from a
live pixel feed:

- **Structured browser events** are written to `browser-events.jsonl` (a separate
  stream from the existing `actions.jsonl` audit log), surfaced through
  `GET /api/slots/{n}/events`.
- Each slot shows its current **URL/title** plus **compact ARIA/text snapshots**.
- **Screenshots are captured only on demand, on failure, or as evidence** — there
  are no continuous thumbnails and no rolling viewport capture.

POST controls (all under `Authorization: Bearer <token>`):

```
POST /api/slots/{n}/navigate     # navigate the slot to a URL
POST /api/slots/{n}/screenshot   # capture one frame on demand / as evidence
POST /api/slots/{n}/release      # release the slot
POST /api/slots/{n}/focus        # focus / observe the slot
POST /api/slots/{n}/takeover     # operator takeover
POST /api/slots/{n}/resume       # hand control back to the agent
```

Example read call (sanitized fleet view):

```bash
SHADE_TOKEN="$SHADE_DASHBOARD_TOKEN"
curl -H "Authorization: Bearer $SHADE_TOKEN" http://127.0.0.1:8790/api/fleet
```

The single-frame capture endpoint is `POST /api/slots/{n}/screenshot`; issue it
only when you actually need an on-demand frame.

### v1 non-goals

The console deliberately does **not** include:

- an agent-task launcher from the web view,
- continuous screenshots or live viewport streaming,
- an injected in-page recorder,
- cloud sync, multi-user accounts, TLS, or any public (non-loopback) binding.

## Uninstall

```bash
claude mcp remove shade-browser-mcp --scope user
uv tool uninstall shade-browser-mcp
rm -rf ~/.cache/brave-cdp/           # purge slot profile copies
```

## Development

```bash
git clone https://github.com/Atlas-X-AI/shade-browser-mcp
cd shade-browser-mcp
uv sync --dev
uv run shade-browser-mcp doctor
```

## Design notes

- **Vendored dependencies** — `cdp_slots.py` and `launch-brave-cdp` ship inside the package (loaded via `importlib.resources`). No symlinks, no external `~/bin/` scripts, no assumptions about the host.
- **Wheel loses exec bit** — the shell script is invoked via `bash <path> <slot>` regardless of its executable bit after install.
- **No Brave auto-install** — intentional. The package won't try to install Brave for you; run `shade-browser-mcp doctor` to find out if you're missing it.
- **User-scope MCP registration** — the server runs under `uv tool install`'s isolated venv; `claude mcp add --scope user` points Claude at the installed entry-point, no manual JSON surgery.

## Supersedes

- `/home/anombyte/Hermes/current-projects/atlas-shade/shade-mcp/browser-server.py` (monorepo path)
- `/home/anombyte/bin/browser-mcp-proxy.sh` (wrapper script)
- `/home/anombyte/bin/launch-brave-cdp` (now vendored)

Previous wiring relied on an external `~/bin/launch-brave-cdp` and a symlinked `cdp_slots.py` from `claude-launcher`. That's fragile across reinstalls — this package fixes it.

TDQS

B3/5.0

Scored across 18 tools

Disambiguation3/5

Most tools have clear distinct purposes, but there is overlap among the network monitoring tools (browser_network_requests vs browser_network_intercept vs browser_ws_monitor) and the three browser_use_* variants (tor, tunnel, residential) are similar except for the network path. Descriptions help but an agent may struggle to pick the exact right tool in ambiguous situations.

Naming Consistency3/5

All tools are snake_case and most start with 'browser_', but the pattern after the prefix is inconsistent: some use verb_noun (use_tor, solve_challenge), some are noun phrases (network_requests, passive_recon), and one (form_scan) lacks the browser_ prefix. This mixed convention is still readable but not uniform.

Tool Count4/5

18 tools is slightly above the ideal 3-15 range but still manageable for a comprehensive security toolset. Each tool addresses a distinct aspect of browser security and anti-detection, so the count feels justified rather than bloated.

Completeness2/5

The toolset focuses heavily on network routing, monitoring, and stealth features, but omits basic browser lifecycle operations like navigate, screenshot, or close slot. Without a direct way to navigate to a URL (except via raw CDP), the surface is incomplete for practical security testing workflows.

Maintenance

ActivitySlowing
ResponsivenessNo issues