English | 简体中文
nexus-browser-mcp

A browser-automation MCP server with event-driven, deterministic snapshots.
Built on Playwright. Drives a browser for LLMs through the Accessibility Tree — navigate, click, type, read, fill forms, manage tabs. Key differences from alternatives (e.g. Playwright MCP):
- Deterministic snapshots: no fixed-interval
sleep guessing. A MutationObserver records the last DOM mutation and the browser's own requestAnimationFrame loop decides when the page has been quiet for STABLE_WINDOW_MS (default 800ms) before extracting a snapshot — eliminating "captured mid-animation" races.
- Built-in governance gates: HITL rules (e.g. clicking "pay/confirm" requires human approval),
browser_evaluate disabled by default with unconditional confirmation, JSONL audit log (sensitive-parameter redaction + per-call in/out character metering, so token cost can be reconciled).
- Multi-task isolation: one MCP connection (session) can host multiple independent
task_ids, each with its own BrowserContext (no login-state cross-contamination). Idle tasks are reclaimed by TTL; on next use they're rebuilt and the last page is restored automatically.
- Death observability + self-healing: if a tab or the whole browser is closed externally or crashes, the next call rebuilds it automatically (a persistent profile keeps your login state) and prepends a
[state change] notice telling the agent exactly what was restored and what was lost — no raw Playwright exceptions leak through.
- Developer observability: every page records console messages, uncaught JS exceptions and network request metadata (method/URL/status/failure reason — never bodies) into capped ring buffers;
browser_console / browser_errors / browser_network read them incrementally via a since cursor, so the agent can answer "why did nothing happen" instead of guessing.
Installation
pip install nexus-browser-mcp
# or
uvx nexus-browser-mcp
Two executable entry points are installed: nexus-browser-mcp and nexus-browser. A guaranteed fallback: python -m nexus_browser.server.
Requires playwright and its browser binary:
pip install playwright && playwright install chromium
Integrate (any MCP client)
opencode (~/.config/opencode/opencode.json):
{
"mcp": {
"browser": {
"type": "local",
"command": ["uvx", "nexus-browser-mcp"],
"enabled": true
}
}
}
Claude Code (.mcp.json, project root):
{
"mcpServers": {
"browser": {
"type": "stdio",
"command": "uvx",
"args": ["nexus-browser-mcp"]
}
}
}
Pi Coding Agent: reads standard MCP configuration — project .mcp.json or user-global ~/.config/mcp/mcp.json; stdio is the default transport:
{
"mcpServers": {
"browser": {
"command": "uvx",
"args": ["nexus-browser-mcp"]
}
}
}
See docs/INTEGRATE.md for details (Chinese).
Use your own browser (with login state)
By default, isolated mode launches Playwright's bundled Chromium without your cookies/login state. To use your own browser, pick one:
Option A — load your browser profile directly (recommended, simplest)
Use system Chrome with your everyday user data directory (cookies/login/bookmarks included):
BROWSER_CHANNEL=chrome
BROWSER_USER_DATA_DIR="C:\Users\<you>\AppData\Local\Google\Chrome\User Data"
Note: while running against your real User Data, the process owns the browser — launching your own Chrome concurrently will conflict. Prefer a copied profile or a dedicated --user-data-dir.
Recommended: a tool-dedicated profile (no conflict with your daily browser)
Use BROWSER_CHANNEL=chrome plus a dedicated user data dir (e.g. C:\Users\<you>\.nexus-browser\chrome-profile):
BROWSER_CHANNEL=chrome
BROWSER_USER_DATA_DIR="C:\Users\<you>\.nexus-browser\chrome-profile"
On first use, log in to target sites once in the dedicated Chrome window that pops up when the agent calls a browser tool. Cookies persist in that profile forever after — the agent carries login state while staying fully isolated from your daily browser.
Option B — attach to a running Chrome via CDP
Start chrome --remote-debugging-port=9222 first, then set BROWSER_MODE=cdp.
If the CDP connection fails, the server now fails loudly (no silent fallback to a fresh browser) and tells you to start the debug-port browser first.
In CDP (or persistent-profile) mode the agent can also take over tabs you already have open: browser_list_pages shows them under "external tabs", and browser_adopt_page(ext_index) pulls one into the task (snapshot/click/read all work from then on). Adoption always requires confirmed=true — it hands the agent full read/write access to that page, including its login state.
Configuration (environment variables)
Every option can be overridden via BROWSER_-prefixed env vars:
评测/全功能使用:两个高危能力默认关闭(安全优先)。需要 browser_evaluate 时设 BROWSER_ALLOW_JS_EXECUTION=true,需要 browser_network_body 时设 BROWSER_ALLOW_NETWORK_BODY=true——冷启动跑基准/评测不开它们,对应子任务会被拒(这是设计,不是故障)。
| Variable | Default | Description |
|---|
BROWSER_MODE | isolated | isolated (fresh isolated browser) / cdp (attach to your Chrome) |
BROWSER_CDP_ENDPOINT | http://localhost:9222 | CDP endpoint |
BROWSER_CHANNEL | "" | System browser channel: chrome/msedge etc. (empty = Playwright bundled Chromium) |
BROWSER_USER_DATA_DIR | "" | User data dir (carries cookies/login). When set, one shared persistent context across tasks. Empty = fresh profile |
BROWSER_HEADLESS | false | Headless mode (isolated only) |
BROWSER_DEFAULT_TIMEOUT_MS | 30000 | Playwright per-operation timeout (navigation etc.) |
BROWSER_TOOL_TIMEOUT_MS | 60000 | Outer timeout guard per tool call (returns ERROR instead of hanging) |
BROWSER_STABLE_WINDOW_MS | 800 | Quiet window: how long without DOM mutations counts as "stable" |
BROWSER_STABLE_REQUIRED | 2 | Fallback only: consecutive identical snapshots confirming stability, used when the MutationObserver watcher is unavailable (primary path verifies "zero mutations during capture" via the mutation timeline directly) |
BROWSER_STABLE_TIMEOUT_MS | 3000 | Total stability-wait timeout; degrades gracefully on expiry |
BROWSER_SNAPSHOT_MAX_NODES | 100 | Max nodes per snapshot |
BROWSER_CONTEXT_TTL_SEC | 600 | Idle task auto-reclaim (seconds) |
BROWSER_STREAM_CHAR_CAP | 16000 | Max chars per stream buffer (oldest dropped with a seam marker) |
BROWSER_STREAM_PAGE_CAP | 64000 | Total stream buffer chars per page |
BROWSER_EVENT_MAX_ENTRIES | 500 | Max events (console/exception/request) per page, oldest dropped with a counter |
BROWSER_EVENT_TEXT_CAP | 500 | Per-event text truncation length |
BROWSER_EVENT_HANDLE_MAX | 50 | Recent requests per page keeping a live response handle (for on-demand body reads) |
BROWSER_ALLOW_NETWORK_BODY | false | Allow browser_network_body (response bodies may carry sensitive data) |
BROWSER_NETWORK_BODY_CAP | 4000 | Max chars returned per response body |
BROWSER_TRANSPORT | stdio | stdio / http (streamable-http for remote/multi-client) |
BROWSER_HTTP_HOST | 127.0.0.1 | HTTP bind address; non-localhost requires BROWSER_HTTP_TOKEN (refuses to start otherwise) |
BROWSER_HTTP_PORT | 8817 | HTTP port |
BROWSER_HTTP_TOKEN | "" | Bearer token for HTTP transport |
BROWSER_ALLOW_JS_EXECUTION | false | Allow browser_evaluate (unconditional HITL when enabled) |
BROWSER_HITL_RULES | [] | JSON array of HITL rules, e.g. `[{"action":"click","name_pattern":"pay |
BROWSER_AUDIT_PATH | ~/.nexus-browser/audit.jsonl | Audit log path |
BROWSER_DIALOG_TIMEOUT_MS | 20000 | Parked confirm/prompt auto-dismiss timeout (trail kept in event log) |
BROWSER_DOWNLOAD_DIR | ~/.nexus-browser/downloads | Where accepted downloads are saved (click reports filename + path) |
BROWSER_PROXY | "" | none = launch browser with --no-proxy-server (bypass system proxy) |
Tools