Public Browser

September 4, 2026 · View on GitHub

Python client for Public Browser automation. 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. One codebase, one test suite (1600+ tests), two access paths.

Installation

pip install publicbrowser

To install from a source checkout instead, run python -m pip install ./python from the repository root, or python -m pip install . from inside this python/ directory. No manual Chrome launch is needed — Chrome.connect() starts everything automatically via a local public-browser binary or the npx fallback.

Dependencies: websockets (for the Escape Hatch / CdpClient low-level access). The main Shared Core API uses urllib (built-in).

Quick Start

from publicbrowser import Chrome

chrome = Chrome.connect()

with chrome.new_page() as page:
    page.navigate("https://example.com")
    title = page.evaluate("document.title")
    print(title)  # "Example Domain"

chrome.close()

Chrome.connect() auto-starts the Public Browser server as a subprocess, which in turn launches Chrome. When you call chrome.close(), the server subprocess is terminated.

How it works

Python Script                        Escape Hatch (Power User)
    │                                    │
    ▼                                    ▼
HTTP POST /tool/{name}              WebSocket (CDP)
Port 9223                           Port 9222
    │                                    │
    ▼                                    │
Public Browser Server                    │
    │                                    │
    ▼                                    │
registry.executeTool()                   │
    │                                    │
    ▼                                    │
Tool Handler                             │
(click.ts, navigate.ts, ...)             │
    │                                    │
    ▼                                    ▼
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 — selector resolution, Shadow DOM traversal, scroll-into-view, paint-order filtering, ambient context — all server-side.

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 (e.g. via Homebrew), 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

Login and Data Extraction

from publicbrowser import Chrome

chrome = Chrome.connect()

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

    # Fill login form
    page.fill({
        "#email": "user@example.com",
        "#password": "secret",
    })
    page.click("#submit")

    # Wait for dashboard
    page.wait_for("text=Dashboard")

    # Extract data
    data = page.evaluate("""
        Array.from(document.querySelectorAll('.item'))
            .map(el => ({ name: el.textContent, href: el.href }))
    """)
    print(data)

chrome.close()

API Reference

Chrome

MethodDescription
Chrome.connect(host="localhost", port=9223, *, server_path=None, auto_start=True)Connect to or auto-start the Public Browser server
chrome.new_page()Context manager: open a new tab, auto-closes on exit
chrome.close()Close the connection and terminate any auto-started server

Page (via chrome.new_page())

MethodDescription
page.navigate(url)Navigate to URL and wait for load
page.click(selector)Click element by CSS selector, text, or ref
page.type(selector, text)Type text into input element
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.cdpEscape Hatch — returns a CdpEscapeHatch for direct CDP access (see below)

Escape Hatch: page.cdp.send()

For use cases the high-level API doesn't cover — network interception, console log subscriptions, performance tracing, cookie management, PDF generation — you can drop down to raw CDP commands via page.cdp.send():

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"})

    # Register event handler
    page.cdp.on("Network.requestWillBeSent", lambda e: print(e["request"]["url"]))

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

MethodDescription
page.cdp.send(method, params=None, *, timeout=30.0)Send a CDP command and return the result
page.cdp.on(event, handler)Register a callback for a CDP event
page.cdp.close()Close the WebSocket (auto-called when the page context manager exits)

CdpClient (low-level, legacy)

For direct CDP access without the Shared Core server. This is the v1 code path — it works, but does not benefit from server-side improvements. Use page.cdp.send() instead for most Escape Hatch use cases.

from publicbrowser import CdpClient

# Async API
client = await CdpClient.connect(port=9222)
result = await client.send("Runtime.evaluate", {"expression": "1+1"})
await client.close()

# Sync API
client = CdpClient.connect_sync(port=9222)
result = client.send_sync("Runtime.evaluate", {"expression": "1+1"})
client.close_sync()

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 — each script works in its own tab, MCP tabs are never touched.

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"]
    }
  }
}

Legacy: Single-File Alternative

For quick prototyping, you can copy publicbrowser_standalone.py into your project. This uses the v1 code path (direct CDP via WebSocket) and does not benefit from server-side improvements. Use the local publicbrowser package for the full Shared Core experience.

License

MIT