Back to MCP Servers

Unbrowser

Lightweight browser MCP server for LLM agents. Runs JavaScript, follows links, fills forms, manages cookies, and returns low-token BlockMaps from a single native binary without Chrome.

browser-automationjavascriptbrowserllmagent
By protostatis
81Updated 2 weeks agoRustApache-2.0

Installation

npx -y unbrowser

Configuration

{
  "mcpServers": {
    "unbrowser": {
      "command": "npx",
      "args": ["-y", "unbrowser"]
    }
  }
}

How to use

  1. Run the installation command above (if needed)
  2. Open your Claude Code settings file (~/.claude/settings.json)
  3. Add the configuration to the mcpServers section
  4. Restart Claude Code to apply changes

unbrowser

Web access for LLM agents. One static binary. No Chrome.

unbrowser MCP server

unbrowser is the lightweight open-source browser tier from Unchained: cheap, stateful web access for agents when curl/WebFetch is too dumb and full Chrome is too heavy. When a page needs real Chrome, cookies, extensions, or human-in-the-loop auth, escalate to unchainedsky-cli or Unchained.

Try it hosted: Unchained exposes a public Streamable HTTP MCP endpoint at https://unchainedsky.com/unbrowser-mcp for discovery and smoke tests. Glama also runs a hosted MCP release at glama.ai/mcp/servers/protostatis/unbrowser, and the Smithery page is at smithery.ai/servers/protostatis-dev/unbrowser. These hosted endpoints are shared infrastructure: do not send private cookies, secrets, or authenticated browsing tasks through them. For production workflows, install the local binary below so sessions and cookies stay on your machine.

Install

Python (recommended) — wheel ships the native binary. Requires Python 3.10+:

pipx install pyunbrowser   # cleanest on macOS Homebrew / modern Linux (handles PEP 668)
pip  install pyunbrowser   # in a venv on python3.10+

macOS gotcha: the system /usr/bin/python3 is 3.9 and the wheel will reject it with "requires Python >=3.10". Use Homebrew's python3.13 or pipx (which manages its own Python). If pip install fails with PEP 668 ("externally-managed-environment"), that's the same issue — pipx install pyunbrowser is the right call.

from unbrowser import Client       # note: pip name is pyunbrowser, import is unbrowser
with Client() as ub:                # (PyPI's name moderation blocks 'unbrowser';
    r = ub.navigate("https://news.ycombinator.com")   # py- prefix is the standard workaround)

Cargo — binary only, no Python wrapper:

cargo install unbrowser
unbrowser --mcp

MCP — add the binary to Claude Code, Claude Desktop, Cursor, Cline, or any MCP host:

{
  "mcpServers": {
    "unchained": {
      "command": "unbrowser",
      "args": ["--mcp"]
    }
  }
}

The unchained key is only the client-side alias. Use unbrowser if you want exact naming, or keep unchained as the breadcrumb to the full Unchained browser-agent stack.

Hosted MCP smoke/discovery endpoint — for MCP clients that support Streamable HTTP:

{
  "mcpServers": {
    "unbrowser-hosted": {
      "url": "https://unchainedsky.com/unbrowser-mcp"
    }
  }
}

Use this hosted route to inspect tools or run public-page smoke tests. It is intentionally unauthenticated and SSRF-guarded, and it is not a place to replay private cookies or secrets.

Pre-built tarball — for systems without Python or Rust:

# macOS Apple Silicon
curl -L https://github.com/protostatis/unbrowser/releases/latest/download/unbrowser-aarch64-apple-darwin.tar.gz | tar xz
# macOS Intel
curl -L https://github.com/protostatis/unbrowser/releases/latest/download/unbrowser-x86_64-apple-darwin.tar.gz | tar xz
# Linux x86_64 (glibc 2.31+ / Ubuntu 20.04+)
curl -L https://github.com/protostatis/unbrowser/releases/latest/download/unbrowser-x86_64-unknown-linux-gnu.tar.gz | tar xz

From source:

cargo build --release   # binary at ./target/release/unbrowser

Session CLI

For shell-only agents, use a persistent session instead of heredoc JSON-RPC:

unbrowser session start --id demo
unbrowser exec demo navigate https://news.ycombinator.com
unbrowser exec demo query '.titleline > a'
unbrowser exec --pretty demo blockmap
unbrowser session stop demo

Bare RPC (low-level escape hatch)

echo '{"id":1,"method":"navigate","params":{"url":"https://news.ycombinator.com"}}' | unbrowser

That's the install. Runs anywhere a static binary runs — laptop, Lambda, Cloudflare Workers, edge, embedded.

Open source under Apache 2.0. When the cheap path can't handle a page (heavy SPAs, behavioral bot challenges), escalate to a real browser via unchainedsky-cli (drives your local Chrome via CDP) or the Unchained desktop app.


By the numbers

This binaryHeadless Chrome (Playwright/Puppeteer)
Binary size~10MB250MB+ Chrome download
RAM / session~50MB200–500MB
Cold start~100ms~1s
Tokens / page (LLM)~500 (BlockMap inline)tens of thousands of HTML, parsed by you
Install stepscargo buildinstall Chrome + Node + Playwright + system deps
Lambda / Workers / edge❌ Chrome too big
100K pages/day cost$0 (your infra)$$$ Chrome fleet or hosted API

5–10× lower memory, 25× smaller binary, 10× faster cold start, 70× lower per-page token cost. That's the tradeoff this product makes — defer JS-rendering (Phase 4/5) and pixel rendering (out of scope) in exchange for a footprint that fits in places Chrome doesn't.

Agent-friendly by design

This isn't a Chrome wrapper that an agent uses through a Puppeteer-shaped abstraction. It's a browser whose every output is shaped for LLM consumption:

  • navigate returns a BlockMap — ~500 tokens of structured page summary (landmarks, headings, interactives, density signals) right in the response. No follow-up call needed to know what's on the page.
  • Stable element refs (e:142) — query, click, type, submit using opaque handles. The LLM never has to scrape the DOM itself.
  • challenge field on every blocked navigate — provider, confidence, and the exact clearance cookie name. The agent reacts intelligently instead of guessing.
  • density.likely_js_filled heuristic — distinguishes "real SSR page" from "SSR shell with JS-filled cells" (the CNBC trap). The agent bails before burning round-trips on a page it can't read.
  • MCP-nativeunbrowser --mcp exposes the RPC tool surface to any MCP host (Claude Code, Claude Desktop, Cursor, Cline). 4 lines of config, zero glue code.
  • Real Chrome fingerprint (Chrome 134 JA4 + Akamai H2 hash) so sites don't block you for being a script.

For pages that do need real Chrome (heavy SPAs, JS-challenge bot walls), the binary detects them and accepts cookies via cookies_set — so you solve once in Chrome and replay forever here.

Quick demo — Hacker News top 3

from unbrowser import Client

with Client() as ub:
    ub.navigate("https://news.ycombinator.com")
    for s in ub.query(".titleline > a")[:3]:
        print(s["text"], s["attrs"]["href"])

5 lines, no headless browser install. Output is structured JSON, not 35KB of HTML. The Client wrapper handles subprocess lifecycle (atexit reaper so orphans are impossible), JSON-RPC framing, and surfaces real exceptions instead of silent result lookups.

<details> <summary>Bare-RPC version (if you can't use Python)</summary>

The same demo without the wrapper — useful for languages other than Python or multi-step sessions. The protocol is JSON-RPC over stdin/stdout, one JSON object per line:

import subprocess, json
p = subprocess.Popen(["./target/release/unbrowser"],
    stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1)
i = 0
def call(method, **params):
    global i; i += 1
    p.stdin.write(json.dumps({"id": i, "method": method, "params": params}) + "\n")
    p.stdin.flush()
    return json.loads(p.stdout.readline())["result"]

call("navigate", url="https://news.ycombinator.com")
for s in call("query", selector=".titleline > a")[:3]:
    print(s["text"], s["attrs"]["href"])

That's the entire protocol surface. Same shape from any language with subprocess + JSON.

</details>

One-shot CLI

For shell-friendly calls, use the convenience subcommand:

unbrowser navigate https://news.ycombinator.com --json

That prints one JSON result and exits from any install path (PyPI wheel, Cargo, or release tarball). Use JSON-RPC only when you need a persistent session. Run unbrowser --help for the native CLI surface.

A/B runtime shims

For corpus tests against JS-heavy pages, compare the default stable shims with the opt-in enhanced browser-environment shims:

unbrowser navigate https://example.com --exec-scripts --json
unbrowser navigate https://example.com --exec-scripts --json --shims enhanced
# or for JSON-RPC / MCP sessions:
UNBROWSER_SHIMS=enhanced unbrowser

enhanced adds content-positive layout/media/scroll/IndexedDB guesses on top of the stable runtime. It is intentionally opt-in so A/B runs can measure whether more page state materializes without changing the baseline.

Script evaluation is still bounded by UNBROWSER_SCRIPT_EVAL_BUDGET_MS (default 5000); navigate results report scripts.budget_exhausted and scripts.budget_skipped when the budget stops further script execution. The outer RPC watchdog (UNBROWSER_TIMEOUT_MS, default 30000) still wins if it is lower than the script budget.

For a JSONL corpus sweep:

python3 scripts/shim_ab.py --url https://nextjs.org/docs --url https://www.npmjs.com/package/playwright

SPA tier — what works, what doesn't

Empirical, not aspirational. Latest matrix: 28/30 on tested categories.

Page tierCoverageWhat to expect
Static + SSR (Wikipedia, MDN, news, docs, GitHub repo browsing, search engines, archive.org)✅ excellentsub-second navigate; full BlockMap; all selectors work; ~hundreds of tokens vs ~tens of KB raw
SSR + light hydration (Next.js docs, marketing pages, react.dev's static content)✅ usablereads SSR'd content fine; hydration adds nothing but doesn't break either
Bot-walled with cookie handoff (Zillow, Cloudflare-protected sites)✅ via cookies_setsolve once in Chrome, replay forever; challenge.provider field tells the agent which vendor
Module-loader SPAs (Ember, AMD apps like crates.io)⚠️ partial with exec_scripts: truebundles fetch + execute, modules register, but framework auto-mount needs case-by-case shimming
Heavy React/Vue bundles (react.dev runtime, large dashboard apps)⚠️ bounded — won't hang, won't renderwith exec_scripts: true the navigate completes inside the 30s wall-clock budget (5s for the script-eval phase, the rest for settle); rendered DOM may not materialize. Tune via UNBROWSER_TIMEOUT_MS
Apps requiring Workers / Canvas / IndexedDB / WebGL❌ out of scope by designuse the cookie-handoff path with real Chrome via unchainedsky-cli (CDP) or the Unchained desktop app
Hardest-tier anti-bot (PerimeterX with behavioral, Kasada, Akamai BMP advanced)❌ even cookie handoff is fragilereal Chrome via CDP is the right tier

Vs the alternatives:

ThiscurlPlaywright / headless Chrome
Static / SSR pages✅ but token-heavyoverkill
SPA-shell sites⚠️ partial via exec_scripts
Bot-walled (with cookie handoff)
Run in Lambda / Workers / edge❌ Chrome too big
Per-page cost at 100K/day~free~free$$$
LLM-shaped output✅ BlockMap inlineDIY parseDIY parse

Verified against (working)

Concrete sites tested with measured times. Cold-start to extracted-result.

| Category |

View source on GitHub