Zendriver MCP
README.md
# Zendriver MCP
[](https://github.com/coffeegrind123/zendriver-mcp-next/actions/workflows/ci.yml)

[](LICENSE)

A [Model Context Protocol](https://modelcontextprotocol.io) server that gives an AI agent
a real, headed Chrome, driven over the Chrome DevTools Protocol through
[zendriver](https://github.com/cdpdriver/zendriver), and built so the page it is looking at
cannot tell an agent is there.
- **98 browser tools** behind a small fixed tool list: navigation, elements, forms, tabs,
cookies and storage, network logs and mocking, emulation, accessibility, tracing,
Lighthouse, screencasts and more.
- **Hard to detect by design.** Helper scripts and element ids live in an isolated JavaScript
world, nothing is written into the DOM, and clicks and keys are trusted input events.
- **Built for the current MCP spec.** It serves 2026-07-28 and the 2025 handshake versions
from one server. Every tool returns structured results, carries annotations, and fails with
an error code and a recovery hint.
---
## Contents
- [Quick start](#quick-start)
- [How an agent uses it](#how-an-agent-uses-it)
- [Tools](#tools)
- [What a page can and cannot see](#what-a-page-can-and-cannot-see)
- [Configuration](#configuration)
- [Running over HTTP](#running-over-http)
- [Protocol support](#protocol-support)
- [Architecture](#architecture)
- [Development](#development)
- [Acknowledgements](#acknowledgements)
- [License](#license)
## Quick start
**Requirements:** Python 3.10+, Google Chrome or Chromium, and a display. On a headless Linux
host, run Xvfb (`Xvfb :99 &` and `DISPLAY=:99`). The server never runs Chrome headless; see
[why](#what-a-page-can-and-cannot-see).
**Claude Code**
```sh
claude mcp add browser -- uvx --from git+https://github.com/coffeegrind123/zendriver-mcp-next zendriver-mcp
```
**Any MCP client** (stdio):
```json
{
"mcpServers": {
"browser": {
"command": "uvx",
"args": ["--from", "git+https://github.com/coffeegrind123/zendriver-mcp-next", "zendriver-mcp"],
"env": { "DISPLAY": ":99" }
}
}
}
```
**From a checkout**
```sh
git clone https://github.com/coffeegrind123/zendriver-mcp-next
cd zendriver-mcp-next
uv sync
uv run zendriver-mcp # stdio
uv run zendriver-mcp --help # transports, tokens, options
```
## How an agent uses it
The server sends the model short instructions when it connects. The core loop is:
```
start_browser → navigate(url) → get_interaction_tree → click / type_text / press_key
→ get_text_content or screenshot → stop_browser
```
- **`navigate` waits for the page to settle.** It waits for `document.readyState` to reach
complete, then for the network to go idle, and reports which of the two happened.
- **Elements** are addressed by CSS selector, or by the numeric id that
`get_interaction_tree` assigns. Ids stay valid while the element exists. A stale id fails
with code `stale_id` instead of hitting the wrong element.
- **Tabs.** Every page tool takes an optional `tab="tab_2"`, which acts on that tab for one
call without switching to it. Tabs the page opens itself (popups, `target=_blank`) are
picked up automatically.
- **Everything else is one search away.** `search_tools("block requests")` finds the tool,
and `call_tool` runs it.
- **`batch`** runs a known sequence in one call. A later step can read an earlier result as
`"$N.field"`, or as `"${N.field}"` inside a longer string.
- **`jobs`** runs a slow tool in the background (Lighthouse, heap snapshots, video export)
and records its progress.
Every result has structured content plus a text summary. Every failure is an `isError` result
shaped like this:
```json
{ "error": "Something covers #buy at (640, 412): <div#cookie-banner>.",
"code": "element_obscured",
"hint": "Close the overlay on top (a cookie banner, a modal) or scroll so the element is unobstructed. force=true clicks at that point anyway." }
```
## Tools
The default `tools/list` has 19 entries: the core loop below, plus `search_tools`,
`describe_tool`, `call_tool`, `batch` and `jobs`. The core tools are tagged
`anthropic/alwaysLoad` so that Claude Code's tool search never defers them. Set
`ZENDRIVER_MCP_SURFACE=full` to list all 103 directly.
| Core (always listed) | Reached through `search_tools` |
|---|---|
| `start_browser` `stop_browser` `get_browser_status` `navigate` `get_interaction_tree` `click` `type_text` `press_key` `get_text_content` `screenshot` `execute_js` `wait_for_element` `list_tabs` `scroll` | tabs and history · element queries · shadow DOM · forms · human-like mouse and typing · per-page and whole-browser cookies · local storage · network and console logs · request blocking, extra headers, mocking and failure injection · proxy · user agent, locale, timezone, geolocation · device, CPU and network emulation · permissions · accessibility tree · tracing and heap snapshots · Lighthouse · screencast to MP4 · Cloudflare challenge handling · security audit |
The full list, with each tool's effect, parameters and summary, is in
**[docs/tools.md](docs/tools.md)**. That file is generated from the code.
**Resources.** Files that tools write are served back as resources, so a remote client can
fetch them:
- `browser://artifacts/{name}`: screenshots, traces, heap snapshots, videos, cookie exports;
- `browser://tabs`;
- `browser://tabs/{tab}/info`, `/console` and `/network`.
`{tab}` and `{name}` support completion. On 2026-07-28 clients, opening or closing a tab
publishes `resources/list_changed` to `subscriptions/listen` streams.
**Prompts.** `read_page`, `fill_form_task` and `audit_page`.
## What a page can and cannot see
Pages detect automation by looking for side effects. Each row below is a side effect that
other CDP tools leave behind and this server avoids. Each claim is checked by the live test
in `tests/test_live.py`, which records events from a listener inside the page itself.
| Signal a page can check | Common CDP tooling | This server |
|---|---|---|
| `event.isTrusted` on clicks and keys | `false` (`el.click()`, synthetic events) | `true`: real `Input.dispatchMouseEvent` and key events, with the full pointer sequence |
| Marker attributes in the DOM (`data-*` ids) | Written onto every element | None; ids live in an isolated world |
| Helper code in the page's JS world (wrapped `querySelector`, globals) | Runs in the main world | Runs in an isolated world the page cannot reach |
| Injected highlight elements and CSS rules | zendriver's `flash()` adds a `div` and `@keyframes` | Never called |
| `headless` user agent and rendering differences | Common | Refused: every launch is headed |
| `Runtime.enable` console-serialisation side channel | Historically observable | Measured not observable on Chrome 157 with the classic stack-getter probe |
The remaining exposure is documented, not hidden:
- `execute_js` runs in the page's own world by design. Prefer the dedicated tools.
- `select_option` sets a native `<select>` by script, so its `change` event is untrusted. For
a trusted change, focus the select and use `press_key`.
- `start_browser(low_memory=true)` adds container-friendly flags such as `--disable-gpu`,
which produce detectable software WebGL. Use it only where Chrome cannot start without it.
## Configuration
All settings are optional environment variables.
| Variable | Default | Effect |
|---|---|---|
| `ZENDRIVER_MCP_SURFACE` | `catalog` | `catalog` lists the core loop plus the meta-tools; `full` lists every tool. |
| `ZENDRIVER_MCP_PROFILE` | – | Restricts which tools exist: `minimal`, `browse`, `interact`, `scrape`, `stealth`, `audit`, `network`, `full`. |
| `ZENDRIVER_MCP_GROUPS` | – | Comma-separated tool groups to keep (the group names are in docs/tools.md). |
| `ZENDRIVER_MCP_ALLOW` / `ZENDRIVER_MCP_DENY` | – | Exact tool names to keep or remove. |
| `ZENDRIVER_MCP_AUTOSTART_BROWSER` | off | Starts Chrome on the first call that needs it, and replaces a browser that died. |
| `ZENDRIVER_MCP_TOOL_TIMEOUT` | `120` | Per-call time budget, in seconds. A hung CDP call returns an error rather than hanging the session. |
| `ZENDRIVER_MCP_LAUNCH_TIMEOUT` | budget − reserve | Time allowed for Chrome to start. |
| `ZENDRIVER_MCP_ARTIFACT_ROOT` | – | Extra directories, besides `$HOME` and the temp dir, that tools may write files to. |
| `ZENDRIVER_MCP_SKIP_DISPLAY_CHECK` | off | Skips the X display pre-flight check before launching. |
| `ZENDRIVER_MCP_CONFIG_DIR` | `~/.config/zendriver-mcp` | Where HTTP tokens are stored. |
| `ZENDRIVER_MCP_EXTENSIONS_DIR` | `~/.cache/zendriver-mcp/extensions` | Where the bundled extensions are kept (uBlock Origin Lite and I Still Don't Care About Cookies, downloaded on first launch). Falls back to the temp dir when the cache cannot be written; a launch never fails over an extension. |
`ZENDRIVER_MCP_GATEWAY=1` and `ZENDRIVER_MCP_PROFILE=gateway` are still accepted, and mean
`SURFACE=catalog`.
## Running over HTTP
stdio gives each client its own browser. To let several clients share one long-lived browser,
use Streamable HTTP:
```sh
zendriver-mcp --create-token laptop # prints the token once; only its hash is stored
zendriver-mcp --transport streamable-http --host 127.0.0.1 --port 8931
claude mcp add --transport http browser http://127.0.0.1:8931/mcp \
--header "Authorization: Bearer zmcp_..."
```
- **Tokens.** Bearer tokens are compared in constant time and stored as SHA-256 hashes with
mode 0600. Each token's name becomes the caller identity, so one client cannot see or drive
another client's tabs.
- **Without tokens.** The server runs unauthenticated on loopback only. It refuses to bind
any other address.
- **Host and Origin checks** protect against DNS rebinding. Add LAN names with
`--allowed-host`.
- **Rate limit.** 600 requests per minute per token by default (`--rate-limit`), answered
with `429` and `Retry-After`.
## Protocol support
| Spec revision | Supported | Notes |
|---|---|---|
| 2026-07-28 | yes | Stateless, `server/discover`, `subscriptions/listen` |
| 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05 | yes | Initialize handshake; `--stateful` keeps per-client sessions |
Features used:
- tool annotations and titles;
- `outputSchema` with `structuredContent`;
- `isError` results with codes;
- resources, resource templates and completions;
- prompts;
- progress notifications, and cancellation that stops child processes;
- `resources/list_changed`;
- server instructions, kept under Claude Code's 4,096-character limit.
The `jobs` tool gives every client, whatever it supports, a way to poll long-running work.
## Architecture
```
MCP client ──stdio / Streamable HTTP──► MCPServer (SDK 2.3)
│ auth middleware: token → caller (HTTP only)
▼
┌──────────── catalog.py ────────────┐
│ core tools · search · call · batch │
│ jobs · resources · prompts │
└──────────────┬─────────────────────┘
▼
tools/*.py ── ToolBase._register: annotations, budget,
tab handle, Result → structured + text,
exception → {error, code, hint}
▼
dom.py: isolated world, element ids, trusted input, CDP geometry
session.py: browser, tabs and owners, logs, Fetch (proxy auth + interception)
▼
zendriver ──CDP──► headed Chrome
```
## Development
```sh
uv sync --dev
uv run ruff check . && uv run ruff format --check .
uv run mypy src
uv run pytest # unit, contract and protocol tests
DISPLAY=:99 ZENDRIVER_MCP_LIVE=1 uv run pytest tests/test_live.py # real Chrome
uv run python scripts/gen_tool_docs.py # regenerate docs/tools.md
```
The test suite enforces the conventions every tool must follow:
- every tool has a title, annotations, an output schema and described parameters;
- no tool returns an error as a success string;
- no selector is interpolated into page JavaScript;
- the default surface stays under a size budget;
- the instructions name only tools that exist and stay under the size limit.
CI runs all of this, plus the live test under Xvfb.
## Acknowledgements
Built on [zendriver](https://github.com/cdpdriver/zendriver) and the
[MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk). The tool set started
from [Zendriver-MCP](https://github.com/ShubhamChoulkar/Zendriver-MCP) by Shubham Choulkar;
the MCP layer, element access, error model, HTTP security and test suite were rewritten for
this project.
## License
[MIT](LICENSE)
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues