kitesurf-bridge
by TrueNix
README.md
# kitesurf-bridge
Drive **[Cloudflare Kitesurf](https://developers.cloudflare.com/browser-run/kitesurf/)** — the agent-first browser that runs in V8 isolates on Cloudflare Workers — from anywhere. Zero dependencies, no local Chrome.
Ships four ways to use one engine:
| Surface | Install | Use it for |
| --- | --- | --- |
| **MCP server** | `npx -y github:TrueNix/kitesurf-bridge mcp` | Claude Code, Cursor, Codex, any MCP client |
| **CLI** | `npx -y github:TrueNix/kitesurf-bridge markdown <url>` | shells, scripts, CI |
| **Library** | `import { withSession } from 'kitesurf-bridge'` | your own Node code |
| **DSH / Cordis plugin** | composition row | native tools in a DSH harness |
> Install commands below use the GitHub spec, which works today with no registry
> account. Once published to npm as `@truenix/kitesurf-bridge`, every
> `github:TrueNix/kitesurf-bridge` shortens to `@truenix/kitesurf-bridge`.
```bash
npx -y github:TrueNix/kitesurf-bridge markdown https://news.ycombinator.com
```
That renders a real page in a real browser engine, on Cloudflare's network, with **no browser installed locally** and **no API token**.
---
## Why this exists
Kitesurf is not open source and cannot run on your machine. Cloudflare [says](https://blog.cloudflare.com/kitesurf/) they intend to open source it "once we're ready", and even then the stated goal is for customers to *"deploy their own version of Kitesurf on their own accounts"* — still on Workers.
There is also no local Kitesurf in the dev loop: `wrangler dev` launches **your local Chrome**, not Kitesurf. Kitesurf only exists behind `browser=kitesurf` on remote endpoints.
So the practical question is not "can I run it locally" but "can I *drive* it from local code". This package is that bridge.
## Install
### As an MCP server
<details open>
<summary><b>Claude Code</b></summary>
```bash
claude mcp add kitesurf -- npx -y github:TrueNix/kitesurf-bridge mcp
```
</details>
<details>
<summary><b>Cursor / Windsurf / generic <code>mcpServers</code> JSON</b></summary>
```json
{
"mcpServers": {
"kitesurf": {
"command": "npx",
"args": ["-y", "github:TrueNix/kitesurf-bridge", "mcp"]
}
}
}
```
</details>
<details>
<summary><b>With a Cloudflare account (production)</b></summary>
```json
{
"mcpServers": {
"kitesurf": {
"command": "npx",
"args": ["-y", "github:TrueNix/kitesurf-bridge", "mcp"],
"env": {
"CLOUDFLARE_ACCOUNT_ID": "your-account-id",
"CLOUDFLARE_API_TOKEN": "your-browser-run-token"
}
}
}
}
```
</details>
Tools exposed: `kitesurf_markdown`, `kitesurf_text`, `kitesurf_html`, `kitesurf_links`, `kitesurf_screenshot`, `kitesurf_evaluate`, `kitesurf_accessibility_tree`, `kitesurf_probe`.
### As a DSH / Cordis plugin
```yaml
# in an agent preset composition
- '@truenix/kitesurf-bridge/cordis':
cli: npx -y github:TrueNix/kitesurf-bridge
timeoutMs: 120000
```
The plugin registers the same tools on the host. It deliberately shells out to the CLI: a dynamic Cordis host half has no `WebSocket`, `fetch` or `node:*` access, so CDP cannot be opened inside the sandbox. See [`cordis/plugin.mjs`](cordis/plugin.mjs).
### As a library
```bash
npm install github:TrueNix/kitesurf-bridge
```
```js
import { withSession } from '@truenix/kitesurf-bridge';
const md = await withSession({}, async (session) => {
await session.navigate('https://example.com');
return session.markdown();
});
```
## CLI
```
kitesurf-bridge <command> [options]
markdown <url> Extract the page as Markdown (main content by default)
text <url> Visible text only
html <url> Full serialized DOM after JS runs
links <url> Every anchor as JSON
screenshot <url> PNG/JPEG (-o file, --full)
pdf <url> PDF (-o file)
a11y <url> Filtered accessibility tree
eval <url> <expr> Evaluate JS in the page
probe Endpoint + engine capability report
mcp Run as an MCP server on stdio
```
Useful options: `--main`, `--raw`, `--full`, `--width`, `--height`, `--json`, `--endpoint`, `--account`, `--token`, `--timeout`.
## Endpoints
| | Playground (default) | Account |
| --- | --- | --- |
| URL | `wss://kitesurf.cloudflare.app/devtools/page/kitesurf` | `wss://api.cloudflare.com/.../devtools/browser?browser=kitesurf` |
| Auth | none | `Authorization: Bearer <token>` |
| Target | page | browser (a page is created + attached automatically) |
| Suitable for | evaluation | production |
Set `CLOUDFLARE_ACCOUNT_ID` + `CLOUDFLARE_API_TOKEN` (or `CF_*`) to switch. Supplying an account id **without** a token is a hard error rather than a silent downgrade to the shared playground.
> [!WARNING]
> The playground is a free, shared, unauthenticated resource with no SLA. Fine for evaluation and local agent work — do not build production on it.
## Things worth knowing about Kitesurf
These are verified against the live service, not copied from docs. `kitesurf-bridge probe` reproduces them.
**Kitesurf does not run V8 for page script — it runs [Boa](https://boajs.dev/), a Rust JS engine.** Boa enforces a much lower recursion limit and throws `RuntimeLimit: exceeded maximum number of recursive calls`. A natural recursive DOM walk dies on any large page (Wikipedia, docs sites). This package's Markdown converter therefore walks the DOM with an **explicit stack**, keeping JS call depth at O(1). If you use `kitesurf_evaluate`, prefer iterative expressions.
**Navigation failures arrive as Cloudflare edge status codes, not CDP errors.** `Page.navigate` returns a normal `frameId`/`loaderId` even for a nonexistent host, and no `Network.loadingFailed` fires. A missing domain shows up as **HTTP 530**, a broken origin as **520**, leaving a ~16-character placeholder document. Trusting `Page.navigate` hands an agent a blank page and calls it success — so this package classifies outcomes from the Network domain and throws when a `>=400` status comes with an empty document, while still returning real error pages (with `status`) that have readable content.
**Capability flags** (verified):
| | |
| --- | --- |
| ✅ canvas2d, WebAssembly, shadow DOM, localStorage, cookies, `fetch`/XHR, IntersectionObserver, MutationObserver | |
| ❌ **WebGL**, **ServiceWorker**, video/audio playback, real TLS-fingerprint bot-challenge handshakes, long-lived authenticated sessions | |
For those, use Browser Run's default Chromium browser instead.
**Performance trade** (Cloudflare's own figures): Kitesurf uses **3–7× less CPU and memory** than warm Chromium, but is **1.7–1.8× slower in wall time**. That win is on *Cloudflare's* bill for bursty cloud agent workloads — it saves nothing on your own hardware. If you just want local browser automation and already have Chrome, local Playwright is faster and does WebGL and video.
## Zero dependencies
`package.json` has an empty `dependencies` block, including for the WebSocket transport.
Node's global `WebSocket` (WHATWG) cannot send request headers, and the account endpoint needs `Authorization: Bearer …`. `undici` is not importable as a standalone module. So [`src/ws.mjs`](src/ws.mjs) implements the RFC 6455 client directly over `node:http(s)` — handshake, masking, continuation fragments, 64-bit lengths, ping/pong, close — which is everything CDP needs, with header support.
## Tests
```bash
npm test # live tests against the playground
KITESURF_SKIP_NETWORK=1 npm test # offline only
```
The suite hits the real service on purpose: the interesting failures (Boa's recursion limit, pipe truncation, edge status codes) only appear against the real thing.
## Requirements
Node ≥ 18. No browser, no API token, no build step.
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing