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:
- Running server — checks if port 9223 already responds, connects immediately
- PATH binary — finds
public-browserin PATH (e.g. via Homebrew), starts it with--script - npx fallback — runs
npx -y public-browser@latest -- --script - Explicit path —
Chrome.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
| Method | Description |
|---|---|
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())
| Method | Description |
|---|---|
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.cdp | Escape 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.
| Method | Description |
|---|---|
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