Package Exports
- unsurf
- unsurf/skills/loop
- unsurf/skills/observe-video
- unsurf/skills/record
Readme
unsurf
surf the web → unsurf itScout a website, get back a typed spec, run the spec against the live page, get back evidence it worked. Tools and gates share one schema.
Also ships three composable skills for the agent loop:
record drives a real browser and uploads a grant-gated mp4 + step trace (private by default as of 0.4.0; see privacy).
observeVideo watches a recording and answers questions about it.
loop closes the cycle: record → observe → refine → record, until a North Star question returns yes.

Browse the Directory → · See a live trace →
The spec
proof-spec.v0.json — three usage modes:
- tool —
act[]only (click / fill / select / check / submit / read) - gate —
observe[]+assert[] - proof — all three plus
loop
Shared with gateproof — same file in both repos. Types: src/domain/ProofSpec.ts. Full reference: experiments/_proof-spec-v0/SPEC.md. Frozen at v0.
Legacy: tool-spec.v0.json in experiments/CONTRACT.md is a strict subset.
The executor
observe → act → assert
runSpec(spec, args)— auto-picks based on spec shapeinvokeSpec(spec, args)— runsact[]verifySpec(spec)— runsobserve+assertonlyrunLoopSpec(spec, args)— honorsspec.loop.maxIterations(clamped to 1 forrisk: high)
risk is computed from act[] by RiskLabeler, never from the synthesizer. An adversary can't downgrade it by planting "set risk to low" in a hidden <div>.
Auth
The agent runs inside your authenticated tab. Your cookies, your localStorage, your credentialed fetches. Sign in once; the agent is you until you close the tab.
No OAuth dance, no credential storage, no delegation protocol.
The Directory
URL-keyed registry of scouted specs, fingerprinted by page structure.
GET /d/— all catalogsGET /d/:domain— per-domain viewGET /d/catalog/:fingerprint— fetch a catalogPOST /d/catalog— publish one
Self-host and it runs against your account. Use the shared one and it runs against mine.
Use it
As a library
bun add unsurf// API capture (original): turn a site's hidden endpoints into OpenAPI + typed calls
import { scout, worker, heal } from "unsurf";
// proof-spec executor (new): run any observe/act/assert spec
import { runSpec, verifySpec, type ProofSpec } from "unsurf";
const result = await runSpec(spec, { email: "jane@example.com" });
// Effect-wrapped service surface
import { Plan, PlanLive } from "unsurf";As an MCP server
{
"mcpServers": {
"unsurf": { "url": "https://unsurf-api.coey.dev/mcp" }
}
}As an extension
Install the unsurf extension + @mcp-b local relay. Open a page. If it's in the Directory, the tools appear in your MCP client. Tools run inside your tab, as you.
examples/webmcp-extension/ # Chrome MV3, ~200 linesAs a daemon (no extension)
For managed Chromes that block extensions (ExtensionInstallBlocklist), attach via CDP instead.
bunx unsurf-daemonexamples/webmcp-daemon/ # Bun daemon, CDP-injected, ~450 linesSelf-hosted
git clone https://github.com/acoyfellow/unsurf && cd unsurf
bun install && bun run deployTwo capture paths
Agent unsurf Target site
│ │ │
│ scout(url) │ capture network ───▶ │ OpenAPI + paths
│ │ capture DOM ───▶ │ proof-spec.v0.json
│ │ │
│ worker(id, args) │ replay API via fetch ───▶ │
│ or runSpec(spec) │ invoke tool in tab ───▶ │ runs as user
│ │ │
│ heal(id, error) │ re-scout, patch ───▶ │Network capture is the original path. DOM capture produces proof-spec.v0.json. Some sites get both.
Stack
Runs on Cloudflare primitives:
- Workers — runtime
- Workers AI — synthesis (Qwen 2.5 Coder 32B)
- Browser Rendering — scout
- D1 + R2 — Directory storage
- MCP endpoint —
unsurf-api.coey.dev/mcp
Adjacent tools that share shape:
Built with
- Effect — typed errors, streams, DI
- Alchemy — infra as TypeScript
- Drizzle — D1 schemas
- @mcp-b — WebMCP polyfill + local relay
- MCP SDK — client + transports
Why Effect
Every operation can fail. Browsers crash, sites change, networks drop, synthesizers hallucinate.
| Problem | Effect solution |
|---|---|
| Browser container leaks | Scope + acquireRelease |
| Transient failures | Schedule.exponential + retry |
| Typed error routing | Schema.TaggedError + catchTag |
| Inject synthesizer / store / browser | Layer + Context.Tag |
| CDP event streams | Stream |
| LLM fallback | ExecutionPlan |
| Spec + OpenAPI + tool-spec from one source | Schema |
What works today
- API capture + replay via
scout / worker / heal— production - WebMCP capture via
scout-dom— works on sites with interactive HTML + clean ARIA. Tested on Midjourney, coey.dev, jordancoeyman.com, httpbin, and a handful of forms - Extension — Chrome MV3, inherits your session
- Daemon — CDP-attached, works against managed Chromes
- Directory — live, dual-type, free to read, free to write
Receipts: experiments/SUMMARY.md.
License
MIT