Public Browser

MCP serverWeb & browsing

Browse your real profile: -30% tokens, -25% cost, -41% tool calls, -34% tool defs, +40% faster

Unavailable. This server has no hosted endpoint yet, so ahel can't serve it.

Connect ahel once, and every AI you use reads what you have installed.

From the project's README

As published by silbercue/public-browser in README.md.

Lets Claude Code and Cursor drive Chrome — with your real, logged-in profile. On the same 30-test benchmark page it used 30% fewer tokens, 25% less money, 41% fewer tool calls and 40% less time than Playwright MCP at the same pass rate — two runs each, 2026-09-03, driver Claude Opus 5, raw data in the repo (Benchmarks, including where it loses). Its own tool definitions are 34% smaller since v2.10.4 (7,607 → 4,990 tokens, reproduce with node scripts/token-count.mjs). Direct CDP, a11y-tree refs, multi-tab ready — 2,360 TypeScript tests, 237 Python tests.

Built for Claude Code, Cursor, and any MCP-compatible client.

Looking for an alternative to Playwright MCP, Browser MCP, or claude-in-chrome? Public Browser talks to Chrome directly via the DevTools Protocol — no Playwright dependency, no Chrome extension bridge, no single-tab limit. One command to install, zero config. See benchmark comparison below.

Why Public Browser?

Every Chrome MCP server has the same problem: bulky responses, too few reliable refs. Screenshots return 10-30x more context payload than text trees. Selector-based refs break the second the DOM rerenders. Extension bridges (Browser MCP) get stuck on the connected tab. Playwright wrappers spin up a new browser instance for every session.

Public Browser fixes this. It talks directly to Chrome via CDP (same protocol Playwright and Puppeteer use internally), returns an accessibility-tree-based reference map, and caches it across calls so click(ref: 'e5') and type(ref: 'e7', ...) survive scrolls and DOM updates.

Benchmark rows below are April 2026, 35-test suite, Opus 4.6 unless a cell also gives a September value. Cells marked Sep come from the blind September 2026 re-run (35-test page, 30 scored, driver claude-opus-5, two runs per required server; one browser-use run) — run files public-browser-run1/2.json, playwright-mcp-run5/6.json, chrome-devtools-mcp-run3/4.json, browser-use-run6.json in test-hardest/results/. Cross-suite comparison is not valid; see Benchmarks.

What you getPlaywright MCPBrowser MCPclaude-in-chromebrowser-usePublic Browser
Benchmark pass rate (Apr 2026: 31 scored / Sep 2026: 30 scored)29/31 (563s)Sep: 30/30 (468s, 493s)6/31, abortedSep: not re-run(24-test suite only)21/31 (1870s)Sep: 24/30, incomplete run (2023s)30/31 (598s)Sep: 30/30 (281s, 296s)
Session tokens, whole run (Sep 2026 only)Sep: 8.8M, 9.6MSep: 56.2M, incomplete runSep: 6.3M, 6.5M (−30%)
Cost per run, Opus 5 list price (Sep 2026 only)Sep: $4.28, $4.78Sep: $25.25, incomplete runSep: $3.41, $3.35 (−25%)
Avg Tool-Response (Chars)1,448Sep: 740, 656807Sep: 1,298, 1,214
P95 Tool-Response (Chars)8,068Sep: 3,617, 1,5872,328Sep: 6,077, 6,479
view_page avg (Chars)6,084 (browser_snapshot)Sep: 1,911, 2,2691,124Sep: 2,841, 3,398
Multi-tab supportYesNo (single tab)YesPartialYes
ConnectionNew browserExtension bridgeExtensionSubprocessDirect CDP (pipe or WebSocket)
Ref systemPlaywright refsPlaywright refsCSS selectorsScreenshotsA11y-tree refs (stable across DOM changes)
Drag & dropYesNoPartialNoYes (native CDP mouse events)
Shadow DOM + iframeYesYesPartialNoYes (with OOPIF session support)
Multi-step plan executionrun_plan — server-side plan executor with variables, conditions, suspend/resume

P95 is not computed the same way in both rows: the April values are the largest per-tool P95 (by_tool[].p95_chars, jq index floor((n-1)·0.95)), the September values are a nearest-rank P95 over all MCP calls of a run. Do not read the April and Sep P95 numbers as one series.

What changed since April. In April 2026 Public Browser's page views were 5.4x smaller than Playwright MCP's; Playwright MCP 0.0.80 has since made its snapshot format much more compact, so in the September runs its individual responses are smaller than ours (Ø 740 and 656 chars against 1,298 and 1,214) and the tool-response payload summed over a whole run is roughly a tie (109,075 / 104,432 chars against 101,478 / 99,147) — "smallest responses" is no longer a claim we can make. What the model actually consumed over the whole session is a different story: 6.3M and 6.5M tokens against 8.8M and 9.6M (~30% fewer), $3.41 and $3.35 against $4.28 and $4.78 at Opus 5 list price (~25% less). That gap comes from the second thing left standing: ~41% fewer tool calls (84 and 86 against 137 and 151) and ~40% less time to finish (281s and 296s against 468s and 493s, page timer; 34% on the full wall clock, 331s and 346s against 501s and 527s) at the same reliability, 30/30 in each of those four runs. Every call re-reads the whole conversation so far, so fewer calls means fewer re-reads — that is where the token and cost gap comes from, not from smaller responses. The advantage moved from "cheaper per look" to "cheaper per task", and the headline was changed accordingly.

September 2026 data, both runs per server, against Playwright MCP 0.0.80. Rows where the bar runs past the line are rows Public Browser loses. Method and the full table: Benchmarks.

Quick Start

Install in Claude Code

One command — installs globally for all projects:

claude mcp add --scope user public-browser npx -y public-browser@latest

Important: after claude mcp add you must fully quit and reopen Claude Code. /mcp reconnect is not enough — Claude Code reads the mcpServers config only at session start and caches it. After the restart, the first tool call auto-launches Chrome visible (no headless, no port setup). Done.

To enable parallel Python Script API access, add --script to the args: claude mcp add --scope user public-browser npx -y public-browser@latest -- --script

Install in Cursor

Add to ~/.cursor/mcp.json:

{
  "mcpServers": {
    "public-browser": {
      "command": "npx",
      "args": ["-y", "public-browser@latest"]
    }
  }
}

For parallel Python Script API access, use "args": ["-y", "public-browser@latest", "--", "--script"]

Install in Cline

Add to your cline_mcp_settings.json:

{
  "mcpServers": {
    "public-browser": {
      "command": "npx",
      "args": ["-y", "public-browser@latest"]
    }
  }
}

Install in other MCP clients

Any client that supports stdio MCP servers: npx -y public-browser@latest with no arguments.

Try it — your first prompt

After installing, paste this into your AI coding assistant:

Open mcp-test.second-truth.com, read the page, and fill the contact form with Name "Test User" and Email "test@example.com".

This exercises three core tools in sequence: navigate loads the page, view_page reads the accessibility tree with stable element refs, and fill_form fills multiple fields in one call. You should see Chrome open, the page load, and the form filled — all without writing a single line of code.

Uninstall

claude mcp remove --scope user public-browser

Chrome Profiles

By default, Public Browser starts Chrome with a fresh temp profile — no cookies, no logins, no extensions. For tasks like research on sites that block anonymous visitors, you can launch Chrome with your real profile instead.

List available profiles

npx public-browser profiles

Launch with a profile

Three ways — pick whichever fits your setup:

# CLI flag
npx public-browser --profile "Julian"

# Environment variable
PUBLIC_BROWSER_PROFILE="Julian" npx public-browser

# MCP tool (call BEFORE any browser interaction)
configure_session({ profile: "Julian" })

When using a real profile, Public Browser preserves extensions, cookies, logins, and sync. It creates a lightweight wrapper directory with a symlink to your real profile data — Chrome gets a "non-default" data dir (required for remote debugging) while using your actual profile.

If Chrome is already open

Public Browser detects this via lock-file inspection. If Chrome is running with remote debugging enabled, it attaches via CDP. If not, it shows a clear error asking you to close Chrome first.

Script API (Python)

A second way to use Public Browser — deterministic browser automation from Python, without an LLM in the loop. Scripts use the same tool implementations as the MCP server (Shared Core) — every improvement to click, navigate, fill_form etc. automatically benefits your scripts too. The MCP server handles AI-driven workflows; the Script API is for repeatable scripts you write yourself.

How fast that is without an LLM: a scripted run of the 24-test version of the benchmark suite finished the whole suite in 21 seconds (type: mcp-scripted, 2026-04-04). That number says what deterministic scripting costs, not how Public Browser compares to other MCP servers — every cross-server comparison in Benchmarks is LLM-driven on both sides.

Installation

pip install publicbrowser

Or, from a source checkout, install the local package:

python -m pip install ./python

Chrome.connect() auto-starts the Public Browser server as a subprocess via a local public-browser binary or the npx fallback — no manual Chrome launch or port setup needed.

Legacy single-file alternative: For quick prototyping you can copy python/publicbrowser_standalone.py into your project. This uses v1 direct CDP and does not benefit from server-side improvements — use the local publicbrowser package for the full Shared Core experience.

How it works

Python Script                        Escape Hatch (Power User)
    |                                    |
    v                                    v
HTTP POST /tool/{name}              WebSocket (CDP)
Port 9223                           Port 9222
    |                                    |
    v                                    |
Public Browser Server                    |
    |                                    |
    v                                    |
registry.executeTool()                   |
    |                                    |
    v                                    |
Tool Handler                             |
(click.ts, navigate.ts, ...)             |
    |                                    |
    v                                    v
Chrome <------------ CDP --------------->

Your script sends HTTP requests to the Public Browser server on port 9223. The server executes the exact same tool handlers that the MCP server uses — one codebase, one test suite (2300+ tests), two access paths.

Auto-Start

Chrome.connect() finds and starts the server automatically:

  1. Running server — checks if port 9223 already responds, connects immediately
  2. PATH binary — finds public-browser in PATH, starts it with --script
  3. npx fallback — runs npx -y public-browser@latest -- --script
  4. Explicit pathChrome.connect(server_path="/path/to/public-browser") for custom setups

Example: Login + Data Extraction

from publicbrowser import Chrome

chrome = Chrome.connect()

with chrome.new_page() as page:
    page.navigate("https://competitor.example.com/login")
    page.fill({"#email": "tomek@shop.de", "#password": "***"})
    page.click("button[type=submit]")
    page.wait_for("text=Dashboard")

    for cat in ["electronics", "furniture", "toys"]:
        page.navigate(f"https://competitor.example.com/prices/{cat}")
        prices = page.evaluate(
            "[...document.querySelectorAll('tr')].map(r => r.textContent)"
        )
        save_csv(cat, prices)

chrome.close()

Methods

MethodDescription
Chrome.connect()Connect to or auto-start the Public Browser server
chrome.new_page()Context manager — opens a new tab, auto-closes on exit
page.navigate(url)Navigate and wait for load
page.click(selector)Click element by CSS selector, text, or ref
page.type(selector, text)Type text into an input
page.fill({"sel": "val"})Fill multiple form fields at once
page.wait_for(condition)Wait for JS condition or "text=..." shorthand
page.evaluate(expression)Run JavaScript, return result
page.download()Enable downloads, return download dir
page.close()Close the tab (auto-called by context manager)
page.cdp.send(method, params)Escape Hatch — direct CDP access via WebSocket (see below)

Escape Hatch: Direct CDP Access

For use cases the high-level API doesn't cover — network interception, console log subscriptions, performance tracing, cookie management — you can drop down to raw CDP commands:

with chrome.new_page() as page:
    page.navigate("https://example.com")

    # Enable network tracking
    page.cdp.send("Network.enable")

    # Get all cookies
    cookies = page.cdp.send("Network.getAllCookies")

    # Performance tracing
    page.cdp.send("Tracing.start", {"categories": "-*,devtools.timeline"})

The Escape Hatch communicates directly with Chrome via WebSocket (port 9222), bypassing the server. It connects lazily on the first send() call and reuses the connection for subsequent calls. Each page gets its own WebSocket routed to the correct tab.

MCP Coexistence

When the MCP server and Python scripts need to run at the same time, add --script to the MCP config. Chrome.connect() handles the rest automatically — each script works in its own tab, MCP tabs are never touched.

Enabling --script in MCP Config

Claude Code:

claude mcp add --scope user public-browser npx -y public-browser@latest -- --script

Cursor / Cline (mcp.json):

{
  "mcpServers": {
    "public-browser": {
      "command": "npx",
      "args": ["-y", "public-browser@latest", "--", "--script"]
    }
  }
}

See python/README.md for the full API reference and advanced examples.

Node Library API (multiple instances in one process)

The MCP server and the Python Script API both drive exactly one Chrome per process. When you need several browsers at once — say a read-only research browser and a separate action browser per agent — spawning one npx public-browser per instance costs 4–6 s of start-up each. createSession() runs the same session inside your own Node process instead:

import { createSession } from "public-browser";

const research = await createSession({
  cdpUrl: "http://127.0.0.1:9333",          // or cdpPort: 9333
  userDataDir: "/var/agents/a1/research",   // created if missing
  headless: true,
  stealth: false,                            // stay identifiable — see below
  downloadDir: "/var/agents/a1/quarantine",  // never deleted by us
  downloadHash: true,                        // adds sha256 to every download
  downloadNaming: "suggested",               // real filenames, not GUIDs
  cortexDir: "/var/agents/a1/cortex",        // per-instance pattern store
  inheritEnv: ["HTTPS_PROXY"],               // opt in — see Environment below
});

const action = await createSession({ cdpPort: 9334, userDataDir: "/var/agents/a1/action" });

await research.callTool("navigate", { url: "https://example.com" });
const page = await research.callTool("view_page", {});

await research.close();
await action.close();

callTool(name, params) takes the same tool names and parameters as the MCP tools (navigate, view_page, click, type, fill_form, run_plan, download, ...) and routes through the identical handlers (Shared Core).

Isolation. Each session runs in its own worker thread by default, so the module-level caches (element refs, selector cache, viewport state, stealth flag, cortex matcher) exist once per session rather than once per process — two sessions can never hand each other stale element refs.

Measured on macOS with isolation: "process", attaching to a Chrome started outside Public Browser (a worker thread saves ~40 ms):

Median
createSession() launches its own headless Chrome~0.9 s
attach to a running Chrome, up to the first tool response~0.7 s
...through to a real page navigated and read~1.8 s

Most of the attach cost is Chrome starting a renderer for the tab Public Browser opens for itself — an attached session never takes over tabs that belong to someone else.

A thread is not a security boundary: same process memory, same file descriptors. isolation: "process" forks one OS process per session instead — separate heap, separate descriptors, separate crash domain — for integrators whose trust model draws the line there. isolation: "inline" skips isolation altogether and is only correct when the thread runs exactly one session.

isolationBoundaryStartupUse when
"worker" (default)thread — private module caches~1 sseveral sessions in one trusted process
"process"OS process — private memory + descriptors~1 sthe sessions must not share a process with the host
"inline"none — the calling threadfastestexactly one session per thread

No listening CDP port (transport: "pipe"). By default Chrome is launched with --remote-debugging-port, which is what makes --attach, the Script API and reconnect-after-crash possible — and which also means every other process on the machine can drive that browser. For a session holding real logins that is a way around any permission check you perform yourself.

const action = await createSession({
  transport: "pipe",                      // no --remote-debugging-port at all
  userDataDir: "/var/agents/a1/action",
  headless: true,
});

CDP then travels over the child's stdio pipe, which only Public Browser holds: lsof shows nothing listening and a second process finds no way in. The price is everything the port paid for — no reconnect after a Chrome crash, no second client, no attach, and no named profile (Chrome rejects the pipe with a real user profile). Both contradictions fail at createSession() rather than at the first tool call. session.transport reports which mode is in use, and session.cdpPort is undefined — there is no port, and reporting the default would name whatever Chrome the user has open on 9222.

Environment. A session does not start from the host environment. It starts from a documented minimum and you widen it deliberately — an orchestrator holding cloud credentials, API keys and tokens should not hand them to a browser session just because the two share a process tree.

What a session always gets is ESSENTIAL_ENV_VARS: PATH, HOME, the temp dir, CHROME_PATH, locale/timezone, the Linux display variables and the Windows process basics. Everything else is opt-in:

// PATH/HOME/CHROME_PATH plus the proxy — and nothing else from the host.
await createSession({ inheritEnv: ["HTTPS_PROXY", "NO_PROXY"] });

// Full inheritance, the pre-2.8 behaviour.
await createSession({ inheritEnv: true });

Proxy variables are deliberately not essential: a proxy URL can carry credentials, so it is allowlisted on purpose rather than inherited by accident.

On top of that, a session never inherits Public Browser's own SILBERCUE_* / PUBLIC_BROWSER_* configuration variables — in any inheritEnv mode. Each of them has an option here, and a host-level variable, usually set for the host's own Chrome, silently redirecting a configured session is a bug, not a feature: with SILBERCUE_CHROME_HOST=10.9.9.9 in the orchestrator's environment, a session created with cdpPort: 9450 still talks to 127.0.0.1:9450. Use env to set one back deliberately.

Shutdown. close() resolves only once Chrome is actually gone — SIGTERM, SIGKILL after 5 s — so the port and the user-data-dir are free for the next launch instead of racing a process that was merely asked to exit.

One session per Chrome. Some CDP settings are browser-wide rather than per-session, Browser.setDownloadBehavior among them: two sessions attached to the same Chrome share one download directory, and whichever connected last wins.

This fails silently and it corrupts the record: the losing session keeps reporting paths under its downloadDir, but the file was written to the other one. path then points at nothing, with no error to notice. Give each session its own Chrome — its own port (or transport: "pipe") and its own user-data-dir — whenever downloadDir matters.

Shortened here. Read the whole README on GitHub.

Signals

GitHub stars
6
Last commit
Sep 2026
Weekly downloads
729
Advanced
Delivery
public-browser MCP server → your ahel gateway (mcp.ahel.ai) → every connected AI client.
Catalog kind
mcp-server
Gateway key
io-github-silbercue-public-browser
Source
github.com/silbercue/public-browser