🔗 Companion project:
vernikr/browser-agent(PyPIbrowser-agent-server) — the real-Chrome-on-Xvfb daemon this MCP server talks to over its HTTP API. The two projects are developed together; the contract isdocs/api.md.
MCP server that gives LLM agents a real desktop browser. Your agent gets browser_status, browser_fetch, browser_action and browser_agent — backed by headful Chrome on Xvfb with a hardware-level X11 mouse (Bézier curves, isTrusted: true clicks), Cloudflare Turnstile auto-solve and zero-leak CDP. The heavy lifting happens on a Linux host running browser-agent-server; this bridge is a thin, well-behaved MCP citizen in front of it.
# 0. Have a daemon reachable: install browser-agent-server on a Linux host (see the companion repo),
# then open a route to it (default: SSH local forward to 127.0.0.1:8766)
pnpm dlx @vernikr/browser-agent-mcp tunnel start --ssh user@host
# 1. Point your agent at the bridge — writes the MCP config for you
pnpm dlx @vernikr/browser-agent-mcp init --client claude-code # also: claude-desktop | cursor | freebuff | gemini-cli
# 2. Verify end-to-end
pnpm dlx @vernikr/browser-agent-mcp doctorpnpm not available? Every command also works as npx -y @vernikr/browser-agent-mcp …. init --runner npx writes configs as npx -y @vernikr/browser-agent-mcp@latest — the quickest route to fresh releases (see below).
The MCP config written by init (manual form):
{
"mcpServers": {
"browser-agent": {
"command": "pnpm",
"args": ["dlx", "@vernikr/browser-agent-mcp"],
"env": { "BROWSER_AGENT_URL": "http://127.0.0.1:8766" }
}
}
}| Config | Fresh release arrives | Why |
|---|---|---|
pnpm dlx @vernikr/browser-agent-mcp (also @latest) |
pnpm's cache decides | pnpm caches the resolved dlx environment for as long as dlxCacheMaxAge says — even for @latest. Check the setting rather than a number restated here |
npx -y @vernikr/browser-agent-mcp@latest |
whatever the registry cache says | npx always resolves the latest dist-tag over the network; the only thing standing between you and a release is the registry packument's own Cache-Control, which npm owns and can change |
pinned, e.g. …@1.0.2 |
never (by design) | reproducible |
So: want prompt auto-updates (recommended for MCP clients, which restart the bridge on launch) → use the npx runner: pnpm dlx @vernikr/browser-agent-mcp init --client <you> --runner npx, or hand-write the config above with @latest. Sticking to a pnpm-only toolbox? pnpm dlx still updates — on its own schedule; to force it now: pnpm --config.dlx-cache-max-age=0 dlx @vernikr/browser-agent-mcp@latest doctor.
| Tool | What it does | Notes |
|---|---|---|
browser_status |
Daemon/X11/Chrome/mouse/WARP status | read-only; call before a fetch wave |
browser_fetch |
Open URL in real Chrome, Turnstile auto-solve, return text/HTML/cookies | save_text_to / save_html_to keep megabytes out of context; include_screenshot returns MCP image content |
browser_action |
Single-step driving: goto/move/click/type/key/scroll/screenshot/stop |
hardware X11 events (no CDP input); return_screenshot saves a round trip; use_proxy routes one session through WARP |
browser_agent |
Vision-guided computer use: URL + goal → it clicks/types to the answer | slow & costly on small hosts — only for pages plain fetch can't handle |
Errors come back structured: browser-agent-mcp error (<kind>): <message> + a hint: line (network_error, rate_limit, auth_or_bot_wall, timeout, server_error), so agents can branch without parsing prose.
init copies every one of these that is set in your shell into the generated config, so the client behaves the
way your shell does — the alternative is the client silently running on defaults. A variable that is set but
unusable (a non-numeric timeout, an unknown log level) is copied and reported on stderr, because
loadConfig() would otherwise discard it without a word. BROWSER_AGENT_URL is always written, default
included. XDG_STATE_HOME is read but not copied — set BROWSER_AGENT_LOG_FILE if you need the log somewhere
specific.
browser-agent-mcp # MCP server on stdio (what agents run)
browser-agent-mcp --http [--port 8767] # Streamable HTTP at 127.0.0.1:8767/mcp
browser-agent-mcp --selfcheck [--offline]
browser-agent-mcp doctor [--json] [--url …]
browser-agent-mcp init --client <agent> [--user] [--runner pnpm|npx] [--print]
browser-agent-mcp tunnel {start|ensure|check|stop|vnc} --ssh user@host
--selfcheck needs no network with --offline (in-process handshake); without it, also probes the daemon and checks api_version compatibility.
- Install one-liner:
pnpm dlx @vernikr/browser-agent-mcp init --client <your-host>then… doctor. - Daemon side (the thing
BROWSER_AGENT_URLpoints at):browser-agent-serveron PyPI — install & provisioning guide in the companion repo. - Before a fetch wave:
browser_status. If a fetch returnsblocked: true, retry once withuse_proxy: true; a persistent login wall is a human item, not a retry loop. use_proxyonbrowser_fetchandbrowser_actionmeans different things. On a fetch, omitting it lets the daemon auto-retry a blocked page through WARP. On an action there is no auto-retry — you are already driving a live session — so omitting it means "leave the session on the route it is on", andfalseactively forbids WARP. Switching the route restarts Chrome and drops cookies and page state, so set it once per session rather than per click, and checkwarp_proxy_readyinbrowser_statusfirst: a forced route with WARP down degrades to direct silently, andgotoreports success either way.
docs/mcp-clients.md— ready-made configs per agent hostdocs/troubleshooting.md— tunnel, cold start, 429/403 playbook- HTTP contract:
browser-agent/docs/api.md
MIT — see LICENSE.
