Kitewright
Browser automation for AI agents as a single small binary. An MCP server (Streamable HTTP + stdio) that gives LLM clients navigate / screenshot / extract — without carrying the Node.js + Playwright stack.

Install in one line:
claude mcp add kitewright -- npx -y @kitewright/mcp
Why — measured, not claimed
Head-to-head vs @playwright/mcp 0.0.78, same machine, same Chromium headless-shell build, same page (full methodology):
| @playwright/mcp | Kitewright |
|---|
| Cold start → listening | 354 ms | 75 ms |
| Server RSS (idle) | 102–125 MB | 7.6 MB |
| Server RSS (after work) | 93 MB | 10.9 MB |
| Distribution | 18 MB pkg + Node.js runtime | 6.9 MB static binary |
| First navigate (incl. browser launch) | 2623 ms | 822 ms (session pre-warming) |
| Warm navigate latency | 80–116 ms | 99–105 ms (tie) |
| Idle behavior | browser kept alive | browser reaped after idle TTL, pre-warmed again on next session |
The browser itself (Chromium) costs the same in any language — warm latency is a tie because both speak CDP to the same browser. The wins are everything around it: startup, distribution, idle footprint, and lifecycle management. The honest gap: playwright-mcp ships ~25 tools today, we ship 21 — closing that is the roadmap.
Reliability — actionability auto-waiting
click / type / fill_form / select_option / hover don't fire blindly. Before acting, the engine polls (100 ms, up to a 5 s per-op budget) until the target element is present, visible (not display:none / visibility:hidden / zero-size), enabled (no disabled / aria-disabled), not covered by another element at its click point, and geometrically stable across two consecutive frames. A settled element passes on the first poll, so this is invisible when things are fine — but when an action can't happen, you get a cause-specific error (not found / not visible / disabled / covered / unstable) instead of a silent misclick or a generic timeout. Transient CDP errors are retried twice. Pass timeout_ms on any interaction tool to override the 5 s per-op budget (e.g. a short timeout to fail fast when you expect an element to already be there).
Performance
External-site latency is network-bound — DNS + TLS + TTFB (~400–600 ms) is a floor no tool beats, and Kitewright does not claim to. What it does attack is every controllable cost around the network:
- Prewarm + warm-context pool. The moment an MCP session initializes, the server launches the browser and fills a small pool of pre-created blank browser contexts (
MCP_CONTEXT_POOL, default 2) in the background. A new session is then handed a ready context+page, so its first navigate pays zero browser-launch and zero context-creation cost. Measured localhost first-navigate: ~31 ms prewarmed vs ~709 ms cold (Apple Silicon; details). The pool drains with the browser on idle-reap and refills lazily on next demand, so idle footprint stays at the ~8 MB baseline.
- Lite mode.
browser_navigate {lite:true} (and the default for extract / extract_markdown) blocks images/media/fonts + ad/analytics hosts before the load — 30–70 % faster DOM-ready on heavy pages by skipping the bulk of the bytes. Screenshots/PDF never block resources.
- Shared disk cache + connection pre-warm. A stable
--disk-cache-dir (KITE_CACHE_DIR) lets repeat asset fetches hit cache across runs; KITE_PREWARM_URL establishes DNS+TLS to a known origin during prewarm.
The honest framing: the wins are browser launch, page weight, session start, and connection setup — not the network round-trip to a remote origin.
Architecture
crates/
├── engine/ kitewright-engine — CDP core (chromiumoxide): lazy launch, idle reaper,
│ per-session browser contexts, capped text extraction + AX snapshots.
│ Shared by all frontends.
└── server/ kitewright — rmcp Streamable HTTP server exposing the engine as MCP tools.
bindings/
└── node/ @kitewright/node — napi-rs bindings exposing a Puppeteer-compatible
(experimental) API over the same engine (built separately; kept out
of the core cargo workspace). See "@kitewright/node" below.
Each MCP session owns one persistent page inside its own Chromium browser context (cookie isolation between agents): log in once, keep clicking. The page and context are closed when the session ends; the browser itself is still reaped after the idle TTL and transparently relaunched on the next call.
Install
The zero-install way — run it straight from npx, like @playwright/mcp:
claude mcp add kitewright -- npx -y @kitewright/mcp
npx @kitewright/mcp resolves the prebuilt kite binary for your platform (an
optional per-platform dependency, esbuild-style) and starts a stdio MCP server —
no Rust toolchain, no build. See npm/kitewright-mcp.
Prefer the raw kite binary? (npx above is the easy path; these need a Rust toolchain.)
# From the public repo — builds + installs the `kite` binary:
cargo install --git https://github.com/kitewright/kitewright kitewright # → ~/.cargo/bin/kite
# From a local checkout:
cargo install --path crates/server
# Docker (headless Chromium bundled in the image):
docker run --rm -p 8090:8090 kitewright # build locally: docker build -t kitewright .
Not yet published to crates.io or Homebrew — cargo binstall/brew aren't wired up. Use npx @kitewright/mcp (no toolchain) or the cargo install --git line above.
Get a browser
Kitewright drives Chromium over CDP but does not embed one. It uses a system
Chrome/Chromium when present, honors BROWSER_EXECUTABLE, and — if neither is
found — falls back to a browser downloaded by kite install:
kite install # fetch the latest stable chrome-headless-shell into the kite cache
kite install downloads the current Chrome-for-Testing chrome-headless-shell
build for your platform into $KITE_CACHE_DIR (or the OS cache dir) and the
engine picks it up automatically — no BROWSER_EXECUTABLE needed. Re-running is
a no-op once a build is present. The Docker image already ships Chromium.
Run & connect
kite with no arguments serves MCP over Streamable HTTP (networked, default,
supports auth + many sessions); kite --stdio serves a single session over
stdio for local clients.
kite
# → kitewright listening on http://0.0.0.0:8090/mcp
HTTP transport — connect from Claude Code:
claude mcp add kite --transport http http://localhost:8090/mcp
stdio transport — MCP client config (Claude Desktop, Cursor, …):
{
"mcpServers": {
"kite": {
"command": "kite",
"args": ["--stdio"]
}
}
}
Configuration
| Env var | Default | Meaning |
|---|
MCP_HTTP_BIND | 0.0.0.0:8090 | Listen address |
MCP_AUTH_TOKEN | unset | When set, /mcp requires Authorization: Bearer <token> (401 otherwise). Unset = open access + startup warning |
MCP_RATE_LIMIT_PER_MINUTE | 300 | Per-client-IP request limit (fixed 60s window); 429 when exceeded |
BROWSER_EXECUTABLE | auto-detect | Path to chrome / chromium / chrome-headless-shell. When unset: a system Chrome/Chromium is detected, else a kite install-managed build in the cache dir |
BROWSER_NO_SANDBOX | unset | Set (any value) to pass --no-sandbox (containers) |
KITE_HEADLESS | unset | Kite launches a headed (visible) browser by default so you can watch automation. Set (any value) to run headless — required on servers, CI, and containers with no display, where a headed Chrome fails to launch |
KITE_IDLE_TIMEOUT_SECS | 1800 | Idle seconds before a headless browser is reaped to free memory. Default 30min keeps a session alive across normal pauses (headed never reaps). A reap that does happen is recovered by cookie auto-restore, so an authenticated session survives it. Lower it on a memory-constrained multi-session server |
KITE_ALLOW_SECRET_FILES | unset | Set (any value) to let browser_fill_secret read file:/path secrets from disk. Requires KITE_SECRET_DIR (the directory reads are fenced to) so arbitrary host files can't be read/exfiltrated. env: secrets need no opt-in |
KITE_VIEWPORT | 1440x900 | Default viewport / window size as WIDTHxHEIGHT (Chromium's own default is a cramped 800x600). Adjust at runtime with the browser_resize tool |
MCP_CONTEXT_POOL | 2 | Number of pre-warmed blank browser contexts kept ready so a new session gets an instantly-usable context+page (zero context-creation latency). 0 disables. The pool refills in the background and drains with the browser on idle-reap (it never keeps the process alive) |
KITE_CACHE_DIR | <tmp>/kitewright-cache | Shared on-disk HTTP cache (--disk-cache-dir), stable across launches so repeat asset fetches hit cache. NOTE: per-session isolated contexts (cookie isolation) use an ephemeral cache; this benefits the browser's default context |
KITE_PREWARM_URL | unset | If set, prewarm navigates a throwaway page to this origin to establish DNS+TLS+connection before the first real navigate. No-op when unset |
BROWSER_PREWARM | unset | Set (any value) to launch + pre-warm the browser at server boot (otherwise prewarm fires when an MCP session initializes) |
RUST_LOG | info | Log filter |
Security
Kitewright drives a real browser, so treat the endpoint as privileged. The
defaults are safe for local use; harden before exposing it.