browser-for-ai
browser-for-ai (bfa)
English · ภาษาไทย
A CDP-native MCP server that lets an AI agent (Claude Code and any other MCP client) drive a real Chrome at full depth — reading the network and console the way a human does with DevTools open, operating the page robustly, and reverse-engineering a site's API flow into runnable code.
Why bfa
The things a screenshot-only browser tool can't do:
⭐ Turn a real session into runnable code. Mark a flow, perform it in the browser, and bfa synthesizes replay code (curl / TypeScript / Go / Python) with cross-call dependencies chained automatically — an auth token from one response becomes a variable the next request re-uses, not a baked-in literal.
flow_replaythen runs it for real to prove the reversal reproduces.🔬 See the whole network. Full request/response bodies (text and binary base64), the complete on-the-wire headers (
Cookieand custom signing headers included, merged from CDP ExtraInfo), timing, redirect hops, and WebSocket frames — surfaced by the exact question you're asking:failures,pending(hangs),slow.🎮 Drive anything — by ref, by sight, or by pixel. Ref / CSS interaction, raw coordinate + touch for
<canvas>/ WebGL, and see-then-click:page_lookreturns a screenshot with a numbered badge on every clickable element (Set-of-Mark), 1:1 with CSS px, so the model reads the picture and clicks by number — no coordinate math, no misclicks. Every action reports the network / console / URL delta it caused.🧪 Shape traffic. Block / mock / modify requests, and throttle to Slow 3G / offline / custom bandwidth with CPU slowdown.
🗂️ Real sessions. Many concurrent sessions, incognito, attach to your logged-in Chrome, save/restore cookies + storage, and complete cache clearing.
🎯 Drives the right tab, on a viewport that stays put — game after game. The session you launch is the active one and every launch loads the URL you gave (it never silently reuses an earlier game's tab); bfa auto-follows a tab the page opens and self-heals a detached / closed / chrome-error tab (actions re-attach and retry;
browser_recoverre-attaches a stale frame or hands you a fresh tab) instead of forcing a relaunch. Launch withdevice:"mobile"for a stable 390×844 phone viewport (touch on, sopage_tap_atandpage_batch {action:"tap_at"}drive Cocos/canvas games) that will not self-shrink, so coordinates stay accurate across a whole 12-game batch.⚡ Fast, and it never hangs.
page_batchruns a whole sequence in one round-trip and can end with a look; actions settle on quiet instead of fixed sleeps;page_wait_for/net_waitreturn the moment a condition holds. Every call is time-bounded: a runaway script is terminated (page_evalbudget,browser_recover),browser_closeforce-kills an owned Chrome that won't exit, and the recorder is ring-bounded for day-long sessions.
How it compares
Capability | bfa | typical browser MCPs |
Reverse a captured flow → runnable code, dependency-chained + replay-verified | ✅ | ✗ (at most Playwright-script codegen from UI actions) |
Full response bodies (text + binary) & WebSocket frames, on by default | ✅ | mostly metadata only |
Secret redaction in the emitted code | ✅ | ✗ |
Coordinate + touch interaction for canvas / WebGL | ✅ | some (vision mode) |
Attach to your logged-in Chrome | ✅ | ✅ (common) |
Network / CPU throttling presets | ✅ | some |
Multi-step batch + see-the-result in ONE call; bounded calls, runaway-script recovery | ✅ | rare |
Cloud-scaled browsers · stealth · proxies · CAPTCHA | ✗ (local by design) | some cloud tools |
bfa is a local, developer-facing inspection & reverse-engineering tool, not a cloud scraping farm — that focus is why the first three rows are rare elsewhere.
Requirements
Node.js ≥ 20
Google Chrome installed (or set
BFA_CHROME_PATHto your Chrome binary)
Install
Published on npm as browser-for-ai — no clone or build required.
# zero-install (recommended) — pulls the latest and runs on demand
npx -y browser-for-ai
# …or install globally, exposing a `browser-for-ai` command
npm install -g browser-for-aigit clone https://github.com/icueth/browser-for-ai.git
cd browser-for-ai
npm install
npm run build # → dist/server.jsRegister with an MCP client
Claude Code — via the published package (no path needed):
claude mcp add browser-for-ai --scope user -- npx -y browser-for-aiVerify with claude mcp get browser-for-ai (should say ✔ Connected). Tools load into a new session, so start a fresh Claude Code session afterward.
AgentSpace ships browser-for-ai as a default MCP server (npx -y browser-for-ai) — it appears under Settings → MCP / Integrations out of the box.
Any MCP client (raw stdio config):
{ "command": "npx", "args": ["-y", "browser-for-ai"] }claude mcp add browser-for-ai --scope user -- node /absolute/path/to/browser-for-ai/dist/server.jsIf
nodecomes from a version manager (nvm, asdf, …), pass the absolute path to the node binary — the MCP server is spawned by a non-interactive shell that won't resolve aliases.
Quick start
browser_launch { "mode": "fresh", "url": "https://example.com" } // real window
page_screenshot
net_list // recent requests
net_failures // anything that errored
net_pending // anything still hanging
page_snapshot // ref-annotated DOM
page_click { "selector": "#login" }
net_get { "url": "/api/login" } // one call in full: headers + bodies
browser_close { "all": true }Sessions
browser_launch { mode, url?, port?, profile?, incognito?, headless?, viewport? }
fresh— launch our own Chrome (headful by default;headless: truefor none).attach— connect to a Chrome started with--remote-debugging-port(onlyportis used; default9222).incognito: true— isolated context, no prior state.Profiles. No
profile→ ephemeral temp profile wiped on close. A named{ "profile": "work" }persists under~/.bfa/profiles/workso logins survive. Two concurrent sessions on the same named profile collide; unnamed ones are always safe.device: "mobile"— a stable 390×844 phone viewport (dpr 3, mobile layout + UA) that does not track the OS window, so it never self-shrinks and click coordinates stay put. Use it for phone / PG-style games.device: "desktop"is 1280×800. An explicitviewportoverrides a preset; with neither, the page tracks the real window (which can resize).Viewport at launch, or
page_set_viewport { device }/{ width, height }on a live session.
The session you launch becomes the active one; a second launch makes that one active.
In fresh mode bfa auto-follows a tab the page opens and self-heals to another live tab
if the driven one closes. Manage with browser_sessions, browser_use { sessionId },
browser_tabs, browser_use_tab { index }, browser_close. Most tools accept an optional
sessionId; without it they target the active session.
Tool reference (52)
Sessions & lifecycle
tool | purpose |
| launch fresh / attach a session |
| list open sessions |
| set the default session |
| list a session's tabs/targets |
| switch the driven tab (carries recorder + intercept rules; bfa auto-follows page-opened tabs in fresh and attach mode) |
| close one tab by index without closing the session (heals to a live tab if it was the driven one) |
| close one session, or |
| clear cache + cookies + storage |
| bypass-cache reload |
| unfreeze a page whose JS is pinned (terminate script → scripts off → still readable/closable) |
Navigation, state & read
tool | purpose |
| navigate to a URL |
| url, title, readyState, viewport |
| resize a live session's viewport |
| compact, ref-annotated DOM (source of element refs) |
| find element(s) by text / ARIA role / CSS → refs (targeted vs snapshot) |
| read/search the page's text content (optionally by selector + query) |
| see-then-click: 1:1 screenshot with numbered badges on every clickable element + legend → |
| wait until a selector / text / URL / network-idle condition holds (instead of sleeping) |
| delta since last observe — new console/network/URL/DOM |
| PNG of viewport, full page, or one element |
| evaluate JS in the page, return the value |
Interaction
tool | purpose |
| click a ref / selector (reports the delta) |
| type into a field ( |
| fill several fields in one call |
| choose an |
| press a key or combo (e.g. |
| hover an element |
| scroll the window, or an element into view |
| attach file(s) to a file |
| click at raw |
| touch-tap at |
| drag between two points/elements |
| many steps in one call (fill → click → wait_for …, target by selector/text/ref), one combined delta, optional final look |
Network (deep read)
tool | purpose |
| recent requests (filter by url/method/type/status; |
| one request in full: headers, request & response bodies |
| 4xx/5xx + transport failures with error detail |
| requests still in flight (hang candidates) |
| finished requests slower than a threshold |
| WebSocket connections + recent frames |
| wait until a matching request appears / settles (only requests since your last action) |
Traffic shaping & emulation
tool | purpose |
| block / mock / modify matching requests (CDP Fetch) |
| list active intercept rules |
| remove intercept rules |
| emulate network (offline / 3G / 4G / custom) + CPU slowdown |
Console
tool | purpose |
| console messages (filterable by regex) |
| errors + uncaught exceptions with stacks |
API-flow extraction
tool | purpose |
| mark the start of a flow in the recording |
| export captured calls as JSON summary or HAR |
| generate replay code (curl/ts/go/python) with deps chained |
| execute the reversed flow for real (Node fetch) to verify |
Session persistence
tool | purpose |
| save cookies + local/session storage to |
| re-apply a saved session (origin-scoped) |
Reverse-engineering an API flow → runnable code
The flagship workflow. A page logs in with POST /api/login (returns a token),
then calls GET /api/me with Authorization: Bearer <token>:
browser_launch { "mode": "fresh", "url": "https://app.example.com/login" }
flow_mark { "label": "login flow" }
page_fill { "fields": [
{ "selector": "#user", "value": "alice" },
{ "selector": "#pass", "value": "s3cret" }
]}
page_click { "selector": "#submit" }
flow_synthesize { "target": "curl" }produces:
resp0=$(curl -s -X POST 'https://app.example.com/api/login' \
-H 'content-type: application/json' \
-d '{"user":"alice","pass":"s3cret"}')
token=$(echo "$resp0" | jq -r '.token') # ← lifted from the response
curl -s -X GET 'https://app.example.com/api/me' \
-H "authorization: Bearer $token" # ← re-used, not a literalflow_synthesize also emits TypeScript / Go / Python, flow_replay runs the
sequence for real (deps resolved from each live response) and reports ✓ / ✗
per call, and { "redact": true } swaps secret-bearing header values and
whole-token bodies for env placeholders.
Dependency detection is heuristic (exact / url-encoded / base64 / JWT-claim / substring). Unmatched values stay literal for you to review; always read the generated code before shipping it.
Cookbook
A. Debug a slow or hung page
browser_launch { "mode": "fresh", "url": "https://myapp.com" }
net_pending // the request that never finishes → the hang
net_slow { "thresholdMs": 1000 } // finished-but-slow calls, slowest first
net_failures // 4xx/5xx + transport errors
console_errors // the thrown stack trace
net_get { "url": "/api/user" } // the failing call in fullB. Reverse-engineer an API into runnable code
browser_launch { "mode": "fresh", "url": "https://app.com/login" }
flow_mark { "label": "login+fetch" }
page_fill { "fields": [
{ "selector": "#user", "value": "me" },
{ "selector": "#pass", "value": "pw" }
]}
page_click { "selector": "#submit" }
flow_synthesize { "target": "python" } // code with the token chained in
flow_replay // ✓/✗ per call — verifiedC. Stay logged in across runs
session_save { "name": "myapp" } // first run, after logging in
// later:
browser_launch { "mode": "fresh" }
session_restore { "name": "myapp" } // back in, no re-loginD. Drive a canvas / WebGL app
browser_launch { "mode": "fresh", "incognito": true, "url": "https://game.example",
"viewport": { "width": 390, "height": 844 } } // portrait
page_click_at { "x": 195, "y": 700 } // press a button drawn on the canvas
net_ws // read the app's WebSocket frames
net_pending // catch asset-load hangsE. Test under a bad network / mocked endpoint
net_throttle { "preset": "slow-3g", "cpuRate": 4 } // degrade the connection + CPU
net_intercept_add { "urlIncludes": "/api/config", "action": "mock",
"status": 200, "body": "{\"feature_x\":true}" }
browser_hard_reload
net_slow // see what drags under 3G
net_throttle { "preset": "none" } // reset to full speedF. Upload a file through a form
page_snapshot
page_upload { "selector": "input[type=file]", "files": ["/abs/path/resume.pdf"] }
page_click { "selector": "#submit" }
net_get { "url": "/upload" } // confirm the multipart requestG. See it, then click it (vision mode)
page_look // screenshot with badges 1,2,3… on every clickable element + legend
// legend: [e7] button "ชำระเงิน" — read the picture, pick the badge, click the ref:
page_click { "ref": "e7" }
page_look { "text": "สมัคร" } // badge only the elements whose text matches
page_screenshot // plain 1:1 image; any point (x,y) you read IS the click coord
page_click_at { "x": 640, "y": 412 }H. A whole flow in one round-trip (page_batch)
page_batch { "steps": [
{ "action": "fill", "selector": "#user", "value": "alice" },
{ "action": "fill", "selector": "#pass", "value": "s3cret" },
{ "action": "click", "text": "Login" }, // target by visible text
{ "action": "wait_for", "url": "/dashboard", "timeoutMs": 8000 }
], "look": true }
// → one combined network/console/url delta + a badged screenshot of the dashboard,
// so the next page_click {ref} is chosen from the same reply. Stops at the first failing step.I. Phone / PG-style game (stable viewport, auto-followed tab)
browser_launch { "mode": "fresh", "device": "mobile", "url": "https://game.example/lobby" }
// 390x844 phone viewport that WON'T self-shrink; this session is now active.
page_look // badged screenshot, 1:1 with page_click_at coordinates
page_click { "selector": ".play" } // opens the game in a new tab → bfa auto-follows it
page_state // confirms you are now on the game tab
// if the game tab ever closes, the next tool self-heals to a live tab instead of erroring.
net_list { "since": "nav" } // only this page's requests — earlier lobby polling is hiddenCanvas / WebGL games
Puppeteer defaults to an 800×600 landscape viewport. A portrait game then renders letterboxed, and its full-screen input overlay can swallow coordinate clicks. Launch (or resize) with a portrait viewport so the canvas fills the screen:
browser_launch { "mode": "fresh", "incognito": true, "url": "…",
"viewport": { "width": 390, "height": 844 } }
page_set_viewport { "width": 390, "height": 844 } // on a live sessiondevice:"mobile" turns touch ON, so page_tap_at and page_batch {action:"tap_at"}
drive Cocos/canvas games out of the box (mouse clicks still work). With an explicit
viewport instead, hasTouch defaults to false — set hasTouch:true there if you need page_tap_at.
Which mode do I want?
fresh(default) — a throwaway Chrome, zero setup. Use for reverse-engineering a public flow or any site that does not need your existing login.attach— connect to a Chrome you started with a debug port. Use when you need real logins/cookies or a human-looking browser:navigator.webdriverisfalse, real profile & fingerprint, so it passes basic bot checks that a puppeteer-launched Chrome fails. Setup below.
Attach to a real, logged-in Chrome
A normally-opened Chrome has no debug port, and Chrome 136+ refuses one on the default profile (an anti-cookie-theft hardening) — so attach always uses a separate profile:
# dedicated profile (recommended) — a window opens; log in there once, it persists
./bfa-chrome 9222
# …or reuse your existing logins via a COPY of your profile (a non-default dir)
cp -R "$HOME/Library/Application Support/Google/Chrome" "$HOME/.bfa/real-copy"
./bfa-chrome 9222 "$HOME/.bfa/real-copy"Then: browser_launch { "mode": "attach", "port": 9222 }. (If the port isn't up, the
tool's error tells you this exact recipe.)
⚠️ A copied real profile hands the agent every logged-in session it contains — email, cloud consoles, banking, source control. It can read those pages and act as you. Prefer the dedicated profile; use a real-profile copy only when you need those logins and accept that blast radius.
Do not point
bfa-chromeat your live default profile: on Chrome 136+ the debug port silently won't open, and it would also collide with your running Chrome (one process per profile dir).
Roadmap
Gaps we know about, in rough priority order:
iframe-aware refs —
page_snapshot/ interaction currently resolve the top document only; cross-frame ref support is the next correctness item.Device emulation presets — bundle UA + viewport + touch + geolocation + permission grants into one call.
PDF export —
Page.printToPDFfor report/invoice-style pages.Playwright/Puppeteer test emission — a new
flow_synthesizetarget that outputs a runnable test script, not just replay code.Natural-language element targeting — an optional LLM-assisted layer over the existing deterministic ref model.
Performance tracing — a thin
Tracing.start/stopwrapper.
Out of scope by design: cloud-scaled browsers, stealth/anti-bot, and residential proxies — bfa stays a local inspection tool.
Notes & limitations
The agent sees whatever the attached/launched browser sees. Treat an attached real-profile Chrome as full access to your logged-in accounts.
Persistent logins: a named profile keeps the real OS keystore, so its cookies/logins survive across launches. (Puppeteer's default
--use-mock-keychain/--password-store=basiccan't decrypt real-keystore cookies and makes Chrome wipe the whole jar — a silent logout; bfa drops those for named profiles. The first launch may prompt for keychain access.) Ephemeral profiles don't persist and keep the mock store.Automation fingerprint: a
fresh(puppeteer-launched) Chrome hasnavigator.webdriver === trueand automation switches, so bot-detection can spot it. Anattached Chrome is an ordinary browser (navigator.webdriver === false, real profile & fingerprint). bfa ships no fingerprint spoofing or anti-bot evasion by design — if a site blocks automation and you're authorized to operate there, useattach(a genuine browser), not a spoofing trick.Never hangs, never needs a force-quit. Native
alert/confirmare dismissed, butbeforeunloadis accepted (= leave) so your own Cmd+W / Cmd+Q / reload is never vetoed. CDP calls time out at 30 s;page_evalhas a budget and terminates a busy loop;browser_recoverunfreezes a page whose own script spins;browser_close/ shutdown are bounded and force-kill an owned Chrome that won't exit (attach sessions are only disconnected).net_throttleCPU is capped at 20x and any active throttle shows inpage_state; Fetch interception is switched off when no rules remain.Bounded memory. The recorder keeps the newest 3000 requests / 200 sockets × 500 frames / 2000 console lines, and asks Chrome to retain at most 64 MB of response bodies — a day-long attach session no longer grows until the browser crawls.
browser_clear_cachedefaults to the current origin in attach mode (your real profile); passscope:"all"to wipe the whole profile's cache + cookies.flow_replayonly replayshttp/https, times out per request, is capped overall (60 s / 200 steps), and never touches the live browser session.Headers are captured from the actual wire (CDP ExtraInfo), so
Cookieand network-added headers are recorded — not just whatrequestWillBeSentfirst saw — andnet_getshows every one, including custom signing headers (x-api-key,x-signature,agent, …), not just a well-known subset.Dependency detection and secret redaction are best-effort heuristics — review generated code and exported HAR before sharing or running against production. A computed value bfa can't reverse (e.g. a signature like
MD5(secret + timestamp)) stays a literal; a failingflow_replayusually means exactly such a header still needs to be reproduced in your own code.
Development
npm run typecheck
npm test # unit + real-Chrome integration + in-memory MCP e2e
npm run build