The fastest way to read any website from Claude Code.
Open-source tool that gives Claude Code 15 MCP tools + 6 ready-to-use Skills for JavaScript-rendered web pages ā content extraction, screenshots, PDFs, accessibility snapshots, AI-powered data extraction, multi-page crawling, and browser interaction (click, type, form submit, JS eval, action chains). Powered by Cloudflare Browser Rendering with zero-cost free tier. Supports Direct Mode (no Worker needed) and Worker Mode (with caching, rate limiting, and interaction).
Claude Code's built-in WebFetch only returns raw HTML. Single-page apps, dynamic content, and JS-rendered pages come back empty. CF Browser solves this:
- JS execution ā full headless Chrome renders the page before extraction
- 15 purpose-built tools ā markdown, screenshots, PDFs, accessibility snapshots, AI extraction, crawling, plus click/type/evaluate/interact/form-submit
- Browser interaction ā click buttons, fill forms, execute JS, chain multi-step actions (Worker mode)
- Authenticated scraping ā inject cookies and custom headers for logged-in pages
- Zero cost ā read-only tools run on Cloudflare's free tier; interaction tools require Workers Paid ($5/mo)
- Edge-based ā global low latency from 300+ Cloudflare locations
Two ways to use CF Browser ā pick the one that fits:
| Direct Mode | Worker Mode | |
|---|---|---|
| Setup | pip install + 2 env vars |
Deploy Worker + pip install |
| Time to start | 2 minutes | 10 minutes |
| Requirements | CF Account ID + API Token | Worker + KV + R2 |
| Available tools | 10 read-only tools | All 15 tools |
| Caching | None | KV + R2 (saves ~70% API quota) |
| Rate limiting | None | 60 req/min per key |
| Multi-user | No (shares your CF credentials) | Yes (each user gets own API key) |
| Best for | Personal use, quick start | Teams, production, high volume |
Calls Cloudflare Browser Rendering API directly ā no Worker deployment needed.
pip install cf-browser cf-browser-mcpAdd to your .mcp.json:
{
"mcpServers": {
"cf-browser": {
"type": "stdio",
"command": "python",
"args": ["-m", "cf_browser_mcp.server"],
"env": {
"CF_ACCOUNT_ID": "<your-account-id>",
"CF_API_TOKEN": "<your-api-token>"
}
}
}
}Get your credentials:
- Account ID:
wrangler whoamior Cloudflare Dashboard ā any domain ā Overview ā right sidebar - API Token: dash.cloudflare.com/profile/api-tokens ā Create Token ā use "Edit Cloudflare Workers" template
Restart Claude Code. The 10 read-only tools work immediately; the 5 interaction tools require Worker Mode.
Deploy a Cloudflare Worker as an edge proxy with built-in caching and auth.
One-Command Setup:
git clone https://github.com/claude-world/cf-browser.git
cd cf-browser
bash setup.shThe setup script creates all Cloudflare resources, deploys the Worker, installs Python packages, and outputs a ready-to-paste .mcp.json config.
Click to expand manual Worker setup
- Node.js 18+, Python 3.10+
- Cloudflare account with Browser Rendering enabled
wranglerCLI authenticated (npm i -g wrangler && wrangler login)
cd worker
cp wrangler.toml.example wrangler.toml
npm installCreate resources and paste the namespace IDs into wrangler.toml:
wrangler kv namespace create CACHE
wrangler kv namespace create RATE_LIMIT
wrangler r2 bucket create cf-browser-storageSet secrets:
wrangler secret put CF_ACCOUNT_ID # from: wrangler whoami
wrangler secret put CF_API_TOKEN # from: https://dash.cloudflare.com/profile/api-tokens
echo "$(openssl rand -hex 32)" | wrangler secret put API_KEYSDeploy:
wrangler deploy
# ā https://cf-browser.<your-subdomain>.workers.devpip install cf-browser cf-browser-mcpOr install from source:
cd sdk && pip install -e .
cd ../mcp-server && pip install -e .Add to your project's .mcp.json:
{
"mcpServers": {
"cf-browser": {
"type": "stdio",
"command": "python",
"args": ["-m", "cf_browser_mcp.server"],
"env": {
"CF_BROWSER_URL": "https://cf-browser.<your-subdomain>.workers.dev",
"CF_BROWSER_API_KEY": "<your-api-key>"
}
}
}
}Restart Claude Code. You'll see 15 browser_* tools available.
āāāāāāāāāāāāāāāāāāāāāāā
ā Claude Code ā
āāāāāāāāāāāā¬āāāāāāāāāāāā
ā
āāāāāāāāāāāā¼āāāāāāāāāāāā
ā MCP Server (15 tools)ā
āāāāāāāāāāāā¬āāāāāāāāāāāā
ā
āāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāā
ā ā
Direct Mode Worker Mode
(CF_ACCOUNT_ID (CF_BROWSER_URL
+ CF_API_TOKEN) + CF_BROWSER_API_KEY)
ā ā
ā āāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāā
ā ā Cloudflare Worker ā
ā ā āāā Auth (timing-safe) ā
ā ā āāā Rate limit (KV) ā
ā ā āāā Cache (KV + R2) ā
ā āāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāā
ā ā
āāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāā
ā
āāāāāāāāāāāā¼āāāāāāāāāāāā
ā CF Browser Rendering ā
ā API (Chrome) ā
āāāāāāāāāāāāāāāāāāāāāāāā
Three independent packages:
| Package | Language | Purpose |
|---|---|---|
worker/ |
TypeScript (Hono + Puppeteer) | Edge proxy with auth, cache, rate limiting, browser interaction |
sdk/ (cf-browser on PyPI) |
Python (httpx) | Async client library |
mcp-server/ (cf-browser-mcp on PyPI) |
Python (FastMCP) | 15 MCP tools for Claude Code |
| Tool | Input | Output | Use case |
|---|---|---|---|
browser_markdown |
url | Markdown string | Read any web page as clean text |
browser_content |
url | HTML string | Get fully rendered HTML (JS executed) |
browser_screenshot |
url, width, height | PNG file path | Visual verification, multi-device testing |
browser_pdf |
url, format | PDF file path | Generate reports, archive pages |
browser_scrape |
url, selectors[] | {"elements":[...]} |
Extract selector matches with normalized metadata |
browser_json |
url, prompt | JSON | AI-powered structured data extraction |
browser_links |
url | [{href, text}] |
Discover all hyperlinks on a page |
browser_a11y |
url | JSON | Accessibility-oriented snapshot with screenshot stripped |
browser_crawl |
url, limit | {"job_id","status"} |
Start async multi-page crawl |
browser_crawl_status |
job_id, wait | JSON | Poll or wait for crawl results |
| Tool | Input | Output | Use case |
|---|---|---|---|
browser_click |
url, selector | JSON | Click a button/link and get resulting page |
browser_type |
url, selector, text | JSON | Type into input fields |
browser_evaluate |
url, script | JSON | Execute JavaScript and get return value |
browser_interact |
url, actions[] | JSON | Chain multiple actions (click, type, wait, screenshot, etc.) |
browser_submit_form |
url, fields | JSON | Fill and submit forms in one call |
All tools accept optional cookies, headers, wait_for, wait_until, and user_agent parameters. Use wait_until="networkidle0" for SPA sites (React, Next.js, X/Twitter).
"Read the React 19 migration guide"
ā browser_markdown("https://react.dev/blog/2024/12/05/react-19")
"Show me what our homepage looks like on mobile"
ā browser_screenshot("https://example.com", width=375, height=667)
"Extract the top 5 products with name, price, and rating"
ā browser_json("https://example.com/products", prompt="Extract top 5 products...")
"Get the page structure for accessibility analysis"
ā browser_a11y("https://example.com")
"Scrape our dashboard (requires login)"
ā browser_markdown("https://app.example.com/dashboard", cookies='[{"name":"session","value":"abc"}]')
"Find all broken links on our site"
ā browser_crawl("https://example.com", limit=50) ā browser_crawl_status(job_id, wait=True)
"Log into our staging site and check the dashboard"
ā browser_interact("https://staging.example.com/login", actions=[
{"action":"type", "selector":"#email", "text":"admin@example.com"},
{"action":"type", "selector":"#password", "text":"secret"},
{"action":"click", "selector":"button[type=submit]"},
{"action":"wait", "selector":".dashboard"},
{"action":"screenshot"}
])
"Fill out the contact form"
ā browser_submit_form("https://example.com/contact",
fields={"#name":"Claude", "#email":"claude@example.com", "#message":"Hello!"},
submit_selector="button.submit")
All routes (except /health) require Authorization: Bearer <api-key> header.
| Route | Method | Body | Cache | Response |
|---|---|---|---|---|
/health |
GET | ā | ā | {"status":"ok","version":"2.0.1","capabilities":{"interact":...}} |
/content |
POST | {url, wait_for?, wait_until?, user_agent?, cookies?, headers?, no_cache?} |
KV 1hr | HTML |
/markdown |
POST | {url, wait_for?, wait_until?, user_agent?, cookies?, headers?, no_cache?} |
KV 1hr | Markdown |
/screenshot |
POST | {url, width?, height?, full_page?, wait_for?, wait_until?, user_agent?, cookies?, headers?, no_cache?} |
R2 24hr | PNG |
/pdf |
POST | {url, format?, landscape?, wait_for?, wait_until?, user_agent?, cookies?, headers?, no_cache?} |
R2 24hr | |
/snapshot |
POST | {url, wait_for?, wait_until?, user_agent?, cookies?, headers?, no_cache?} |
KV 30min | JSON |
/scrape |
POST | {url, elements[], wait_for?, wait_until?, user_agent?, cookies?, headers?, no_cache?} |
KV 30min | {"elements":[...]} |
/json |
POST | {url, prompt, schema?, wait_for?, wait_until?, user_agent?, cookies?, headers?, no_cache?} |
None | JSON |
/links |
POST | {url, wait_for?, wait_until?, user_agent?, cookies?, headers?, no_cache?} |
KV 1hr | [{href, text}] |
/a11y |
POST | {url, wait_for?, wait_until?, user_agent?, cookies?, headers?, no_cache?} |
KV 5min | {"type":"accessibility_snapshot", ...} |
/crawl |
POST | {url, limit?, user_agent?, cookies?, headers?, no_cache?} |
ā | {"job_id":"..."} |
/crawl/:id |
GET | ā | R2 | JSON |
/crawl/:id |
DELETE | ā | ā | 204 No Content |
/click |
POST | {url, selector, wait_for?, ...} |
None | JSON |
/type |
POST | {url, selector, text, clear?, wait_for?, ...} |
None | JSON |
/evaluate |
POST | {url, script, wait_for?, ...} |
None | JSON |
/interact |
POST | {url, actions[], wait_for?, ...} |
None | JSON |
/submit-form |
POST | {url, fields, submit_selector?, wait_for?, ...} |
None | JSON |
Interaction routes (/click, /type, /evaluate, /interact, /submit-form) require the BROWSER binding. They return 501 if the binding is not configured. If these routes return 404 instead, the Worker deployment is stale; redeploy and verify /health reports version: "2.0.1".
Response shapes are normalized across Worker, SDK, and MCP:
/scrapereturns{"elements":[{"selector":"...", "results":[...]}]}even if the upstream API returns a raw list./linksreturns an array of{href, text}objects; bare URL strings are promoted to{href, text: null}./a11yis derived from/snapshot, strips base64 screenshot payloads, and addstype: "accessibility_snapshot".
All endpoints accept optional cookies and headers fields for accessing authenticated pages:
curl -X POST https://cf-browser.example.workers.dev/markdown \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://app.example.com/dashboard",
"cookies": [{"name": "session_id", "value": "abc123", "domain": ".example.com"}],
"headers": {"X-Custom-Auth": "token"}
}'# Get markdown
curl -X POST https://cf-browser.example.workers.dev/markdown \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://react.dev"}'
# Screenshot with viewport
curl -X POST https://cf-browser.example.workers.dev/screenshot \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "width": 1280, "height": 720}' \
-o screenshot.png
# Accessibility snapshot
curl -X POST https://cf-browser.example.workers.dev/a11y \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'
# AI extraction
curl -X POST https://cf-browser.example.workers.dev/json \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://news.ycombinator.com", "prompt": "Extract top 5 stories with title and score"}'- Set
"no_cache": truein the request body to bypass cache - Cached responses include
X-Cache: HITheader - Text content (HTML, Markdown, JSON) is stored in KV
- Binary content (PNG, PDF) is stored in R2
- Completed crawl results are persisted to R2
- Default: 60 requests per minute per API key
- Response headers:
X-RateLimit-Limit,X-RateLimit-Remaining - Exceeded: HTTP 429 with
Retry-Afterheader
pip install cf-browser# Direct mode ā no Worker needed
from cf_browser import CFBrowserDirect
async with CFBrowserDirect(
account_id="your-cf-account-id",
api_token="your-cf-api-token",
) as browser:
md = await browser.markdown("https://example.com")
# Worker mode ā via deployed Worker
from cf_browser import CFBrowser
async with CFBrowser(
base_url="https://cf-browser.example.workers.dev",
api_key="your-key",
) as browser:
# Read a page
markdown = await browser.markdown("https://react.dev")
# Take a screenshot
png_bytes = await browser.screenshot("https://example.com", width=1280, height=720)
# AI-powered extraction
data = await browser.json_extract(
"https://news.ycombinator.com",
prompt="Extract the top 5 stories with title and score",
)
# Accessibility snapshot (LLM-friendly, screenshot stripped)
tree = await browser.a11y("https://example.com")
# Scrape by CSS selectors
elements = await browser.scrape("https://example.com", selectors=["h1", ".price"])
# Authenticated scraping with cookies
md = await browser.markdown(
"https://app.example.com/dashboard",
cookies=[{"name": "session", "value": "abc", "domain": ".example.com"}],
)
# Async crawl
job_id = await browser.crawl("https://example.com", limit=10)
result = await browser.crawl_wait(job_id, timeout=120)Read-only (Direct + Worker mode):
| Method | Returns | Description |
|---|---|---|
content(url, **opts) |
str |
Rendered HTML |
markdown(url, **opts) |
str |
Clean Markdown |
screenshot(url, **opts) |
bytes |
PNG image |
pdf(url, **opts) |
bytes |
PDF document |
snapshot(url, **opts) |
dict |
HTML + metadata |
scrape(url, selectors, **opts) |
dict |
Normalized as {"elements": [...]} |
json_extract(url, prompt, **opts) |
dict |
AI-extracted data |
links(url, **opts) |
list[dict] |
Normalized list of {href, text} objects |
a11y(url, **opts) |
dict |
Accessibility-oriented snapshot with screenshot stripped |
crawl(url, **opts) |
str |
Job ID |
crawl_status(job_id) |
dict |
Job status |
crawl_wait(job_id, timeout, poll_interval) |
dict |
Wait for completion |
Interaction (Worker mode only):
| Method | Returns | Description |
|---|---|---|
click(url, selector, **opts) |
dict |
Click element, return page state |
type_text(url, selector, text, clear?, **opts) |
dict |
Type into input field |
evaluate(url, script, **opts) |
dict |
Execute JS, return result |
interact(url, actions, **opts) |
dict |
Chain multiple actions |
submit_form(url, fields, submit_selector?, **opts) |
dict |
Fill and submit form |
delete_crawl(job_id) |
None |
Delete cached crawl result |
All methods accept no_cache=True to bypass caching, cookies/headers for authenticated access, wait_for to wait for a CSS selector, wait_until for navigation strategy (networkidle0 for SPAs), and user_agent for custom User-Agent. Interaction methods raise NotImplementedError in Direct mode. In Worker mode, 404 Not Found on interaction methods usually means you are pointing at a stale Worker deployment and should redeploy.
- Auth: Timing-safe Bearer token comparison using SHA-256 (prevents timing attacks)
- Rate limiting: Per-key tracking with hashed key material in KV (no raw keys stored)
- SSRF prevention: Only
http://andhttps://URLs allowed; localhost, private IP literals, and hostnames that DNS-resolve to private IPs are blocked - Secrets: All credentials stored via
wrangler secret put, never in code - Cookie isolation: Cookies are injected per-request, never persisted
CF Browser includes 6 ready-to-use Claude Code Skills in the skills/ directory. Copy a skill folder to your project's .claude/skills/ to activate.
| Skill | Command | What it does |
|---|---|---|
| content-extractor | /content-extractor |
Read pages, extract structured data, scrape elements, discover links |
| site-auditor | /site-auditor |
Crawl a site and generate SEO / link / accessibility audit report |
| doc-fetcher | /doc-fetcher |
Crawl an entire docs site to local Markdown for RAG |
| visual-qa | /visual-qa |
Multi-device viewport screenshots (mobile/tablet/laptop/desktop) + visual checks |
| changelog-monitor | /changelog-monitor |
Track version updates and breaking changes for any project |
| competitor-watch | /competitor-watch |
Extract and compare competitor pricing / features |
# Copy a single skill
cp -r skills/content-extractor .claude/skills/
# Or copy all
cp -r skills/* .claude/skills/| Component | Free Tier | Paid ($5/mo Workers) |
|---|---|---|
| Browser Rendering | 10 min/day, 5 crawl jobs | Higher limits |
| KV | 100K reads/day | 10M reads/mo |
| R2 | 10GB storage | 10GB included |
| Workers | 100K requests/day | 10M requests/mo |
For most Claude Code usage, the free tier is sufficient. Interaction tools (click, type, evaluate, interact, submit-form) require the Workers Paid plan ($5/mo) for the BROWSER binding.
browser_click/browser_type/browser_evaluate/browser_interact/browser_submit_formreturn501: the Worker is deployed without[browser] binding = "BROWSER".- Those same tools return
404: the Worker deployment is older than the current repo. Redeploy and verify/healthreturnsversion: "2.0.1". browser_scrapeorbrowser_linkslook different between environments: current SDK and MCP normalize legacy upstream shapes, but the cleanest fix is still to redeploy the Worker.
cd worker
npm install
npm run dev # Local dev server at :8787
npm run type-check # TypeScript checks
npm test # Run testscd sdk
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest tests/ -vcd mcp-server
python -m venv .venv && source .venv/bin/activate
pip install -e ../sdk # Install SDK first
pip install -e ".[dev]"
pytest tests/ -vcf-browser/
āāā worker/ Cloudflare Worker (TypeScript)
ā āāā src/
ā ā āāā index.ts Hono app entry point
ā ā āāā types.ts Env bindings & request types
ā ā āāā middleware/
ā ā ā āāā auth.ts Bearer token validation
ā ā ā āāā cache.ts KV/R2 cache layer
ā ā ā āāā rate-limit.ts Per-key rate limiting
ā ā āāā routes/
ā ā ā āāā content.ts POST /content ā HTML
ā ā ā āāā markdown.ts POST /markdown ā Markdown
ā ā ā āāā screenshot.ts POST /screenshot ā PNG
ā ā ā āāā pdf.ts POST /pdf ā PDF
ā ā ā āāā snapshot.ts POST /snapshot ā JSON
ā ā ā āāā scrape.ts POST /scrape ā JSON
ā ā ā āāā json.ts POST /json ā JSON (AI)
ā ā ā āāā links.ts POST /links ā JSON
ā ā ā āāā a11y.ts POST /a11y ā JSON (accessibility snapshot)
ā ā ā āāā crawl.ts POST/GET/DELETE /crawl
ā ā ā āāā click.ts POST /click (interaction)
ā ā ā āāā type.ts POST /type (interaction)
ā ā ā āāā evaluate.ts POST /evaluate (interaction)
ā ā ā āāā interact.ts POST /interact (action chains)
ā ā ā āāā submit-form.ts POST /submit-form (interaction)
ā ā āāā lib/
ā ā āāā cf-api.ts CF Browser Rendering client
ā ā āāā puppeteer.ts Puppeteer lifecycle helper (interaction)
ā ā āāā param-map.ts snake_case ā CF API camelCase mapping
ā ā āāā response-normalizers.ts scrape/links response normalization
ā ā āāā cache-key.ts SHA-256 cache keys
ā ā āāā validate-url.ts SSRF prevention
ā āāā tests/
ā āāā wrangler.toml.example
ā āāā package.json
āāā sdk/ Python SDK (cf-browser on PyPI)
ā āāā src/cf_browser/
ā ā āāā client.py CFBrowser client (Worker mode)
ā ā āāā direct.py CFBrowserDirect client (Direct mode)
ā ā āāā _normalizers.py Response-shape normalization helpers
ā ā āāā _shared.py Shared helpers (crawl polling)
ā ā āāā models.py Pydantic response models
ā ā āāā exceptions.py Typed error hierarchy
ā āāā tests/
ā āāā pyproject.toml
āāā mcp-server/ MCP Server (cf-browser-mcp on PyPI)
ā āāā src/cf_browser_mcp/
ā ā āāā server.py 15 MCP tool definitions
ā āāā pyproject.toml
āāā examples/ Usage examples
āāā setup.sh One-command setup script
āāā CHANGELOG.md
āāā LICENSE
āāā README.md
- Fork the repository
- Create a feature branch
- Make your changes with tests
- Run
npm test(worker) andpytest(SDK + MCP Server) to verify - Submit a pull request
