Give an agent a browser, and everything it saw along the way.
browse4me is a Model Context Protocol server that hands an AI agent a real Chromium session over HTTP. Not just click and type — the console, the network, the screenshots, and a set of notes the agent writes to itself and finds again on the next run.
- MCP tools
- 43
- Browser engines
- 2 Playwright · Puppeteer
- Search strategies
- 5
- MCP resources
- 7 + live session data
- Max viewport
- 4K 3840×2160
Two engines behind one interface
Pick the engine when you create the session. The tool schemas do not change, the arguments do not change, and neither does your agent’s code.
- Auto-waiting on actionability — fewer explicit waits in agent code
- Chromium out of the box, with Firefox and WebKit available to the engine
- Native request routing, which the network capture layer builds on
- The engine to reach for unless a site actively fights automation
- puppeteer-extra-plugin-stealth strips the usual automation fingerprints
- Direct CDP access when a tool call needs to go lower than the abstraction
- Leaner startup and memory footprint per session
- The engine for bot-protected targets
browser_create_session({
engine: "puppeteer", // or "playwright" — the default
viewportWidth: 1920, // 1920×1080 default, 3840×2160 ceiling
viewportHeight: 1080,
locale: "de-DE",
timezone: "Europe/Berlin",
headers: { "X-Feature-Flag": "checkout-v2" }
})
// Tabs inherit locale, timezone and headers from the session.
// Override any of them per tab. Ten tabs per session.
browser_launch_tab({ sessionId, url: "https://shop.example.com", locale: "fr-FR" })
All 43 tools
Each one is a JSON Schema the MCP client validates before the call ever reaches a browser. This is the complete surface — nothing here is planned or partial.
Sessions & tabs
6browser_create_sessionOpen a session on a chosen engine, viewport, locale, timezone and header set.browser_launch_tabNew tab in a session, inheriting or overriding its context.browser_close_tabClose one tab and free its resources.browser_get_current_urlWhere a tab actually ended up after redirects.browser_list_sessionsPast and live sessions with status and duration, persisted in Postgres.browser_get_sessionOne session in full, including every note written against it.
Navigation
1browser_navigateGo to a URL with awaitUntilstrategy: load, domcontentloaded or networkidle.
Interaction
8browser_click_elementClick by XPath, withforceJsClickfor framework handlers.browser_click_coordinatesClick a pixel position — canvases, maps, custom widgets.browser_input_textType into an input by XPath.browser_hoverTrigger hover menus and tooltips.browser_scrollScroll the page or a scrollable element.browser_press_keyKeys with Ctrl, Shift and Alt modifiers.browser_get_element_contentRead text, HTML or attributes off an element.browser_upload_fileAttach a file to a file input, confined to the server’s upload root.
Form controls
3browser_select_dropdownChoose an option by value, label or index.browser_set_checkboxSet checked state explicitly rather than toggling blind.browser_select_radioPick one option out of a radio group.
Waiting
3browser_wait_for_elementBlock until an element is visible.browser_waitA flat millisecond pause, for animations and timing.browser_wait_for_element_valueWait for content or value to arrive — AJAX-populated selects and legacy screens.
Finding
5browser_find_element_by_textFuzzy text match with a configurable threshold.browser_find_input_by_labelResolve an input from its label, aria-label, wrapper or placeholder.browser_find_element_nearSpatial search around an anchor by direction and distance.browser_query_domA plain-English query parsed into a DOM filter.browser_find_elements_in_areaEverything inside a pixel bounding box.
Screenshots
1browser_take_snapshotFull page, viewport or a single element. Uploads to S3 and returns a URL; falls back to base64 when storage is off.
Recording & replay
5browser_start_recordingBegin capturing the agent’s own actions.browser_stop_recordingStop and return Chrome Recorder JSON.browser_list_user_recordingsRecordings uploaded from the Chrome extension.browser_get_user_recordingFetch one recording’s steps.browser_replay_recordingReplay any recording in any session, server-side.
Network
6browser_start_network_captureRecord requests and responses with filters.browser_stop_network_captureStop and export as HAR 1.2 or JSON.browser_get_network_entriesQuery captured traffic mid-session.browser_set_request_interceptionBlock, rewrite or mock requests by priority-ordered rules.browser_wait_for_requestBlock until a request matches a URL pattern.browser_wait_for_responseBlock until its response comes back.
Console
1browser_get_console_logsConsole output captured continuously per tab — log, warn, error, plus uncaught page exceptions. Filter by level.
Session notes
4browser_add_noteWrite an observation, DOM quirk or workaround against the session.browser_list_notesNotes in order, filterable by tag.browser_get_noteOne note by id.browser_delete_noteSoft-delete a note.
Five ways to find an element
Agents do not have your CSS selectors, and pages rarely hand out stable ids. Every strategy returns the same shape: XPath, tag, text, visibility and a bounding box, ranked.
Levenshtein distance over visible text, so a typo or a re-worded label still resolves.
browser_find_element_by_text({
tabId,
text: "Sbumit Order",
threshold: 0.8,
elementType: "button"
})
// → matches "Submit Order"
Handles both HTML patterns — explicit <label for> and an input wrapped inside its label — plus aria-label and placeholder.
browser_find_input_by_label({
tabId,
labelText: "Email Address"
})
// → the input, however it was wired up
When nothing connects a field to its caption but position on screen, search outward from an anchor.
browser_find_element_near({
tabId,
anchorXpath: "//span[text()='Username']",
direction: "below",
maxDistance: 100
})
A parser, not a model call — it reads element type, text filter, visibility and state out of the phrase.
browser_query_dom({
tabId,
query: "visible clickable elements with delete text"
})
// → tag:button|a + text:delete + visible
Pair it with a screenshot: the agent sees a region, then asks what is actually inside those coordinates.
browser_find_elements_in_area({
tabId,
x: 840, y: 120,
width: 360, height: 480
})
browser_create_session({ engine: "playwright" })
browser_launch_tab({ sessionId, url: "https://app.example.com/login" })
browser_find_input_by_label({ tabId, labelText: "Email" })
browser_input_text({ tabId, xpath: "//input[@id='email']", text: "agent@co.com" })
browser_click_element({ tabId, xpath: "//button[@type='submit']" })
browser_wait_for_element({ tabId, xpath: "//div[@data-view='dashboard']" })
browser_take_snapshot({ tabId, fullPage: true })
Clicks that survive a synthetic event system
React, Vue, Svelte and Angular attach their own listeners. A native click can land on the page and never reach the handler — and the failure is silent, which is worse than an error.
// Native click — the default, and correct most of the timebrowser_click_element({ tabId, xpath: "//button[@data-testid='checkout']" })// Nothing happened? Dispatch the full sequence instead.browser_click_element({ tabId, xpath: "//button[@data-testid='checkout']", forceJsClick: true })// → mousedown → mouseup → click, each bubbling, each with real coordinates// Also available on browser_click_coordinates.
- Overlays stop mattering. The event goes to the element you named, not to whatever sits on top of it.
- Shadow DOM boundaries stop mattering for the same reason.
- Reach for it as a second attempt, not a default — a native click is still the more faithful simulation.
Record once, replay anywhere
Two ways in, one format out. Both produce Chrome Recorder JSON, which DevTools already reads and any agent can replay.
Start a recording and keep working. Every MCP tool call the agent makes lands in the transcript — navigations, clicks, typing, key presses, scrolls and waits.
browser_start_recording({ tabId })
// ... the agent does its work ...
browser_stop_recording({ recordingId })
Show the agent how it is done. Record yourself doing the flow by hand, upload it, and the agent replays your steps instead of rediscovering them.
- Point the extension at your server and project key
- Start, click through the flow, stop, upload
- The agent finds it with
browser_list_user_recordings
{
"title": "Checkout flow",
"steps": [
{ "type": "navigate", "url": "https://shop.example.com/cart" },
{ "type": "click", "selectors": [["#promo-code"]] },
{ "type": "change", "selectors": [["#promo-code"]], "value": "SPRING25" },
{ "type": "click", "selectors": [["button[type=submit]"]] },
{ "type": "waitForElement", "selectors": [[".order-confirmed"]] }
]
}
Replay runs on the server, through the same tool layer as a direct call — so a replayed step gets the same URL checks, the same quota accounting and the same audit trail as anything else.
The network layer is a first-class tool, not a log file
An agent that can only see the DOM is guessing about why a page misbehaved. Six tools let it watch the traffic, wait on specific calls, and change what comes back.
Requests and responses with headers, timings and bodies. Filter by resource type or URL pattern so the transcript stays readable.
Block, rewrite or mock. Rules are ordered by priority, so a specific mock can sit in front of a broad block.
Wait on the request or the response itself instead of guessing at a sleep length after a click.
HAR 1.2 out of browser_stop_network_capture. Opens in DevTools, Charles, or anything that reads HAR.
browser_start_network_capture({
tabId,
resourceTypes: ["xhr", "fetch"],
urlPatterns: ["*api.example.com*"]
})
// Force the failure path instead of waiting for it to happen
browser_set_request_interception({
tabId,
rules: [{
urlPattern: "**/api/payment",
response: { status: 502, body: { error: "upstream_timeout" }, delay: 500 },
priority: 1
}]
})
browser_click_element({ tabId, xpath: "//button[text()='Pay now']" })
browser_wait_for_response({ tabId, urlPattern: "**/api/payment" })
browser_get_console_logs({ tabId, level: "error" })
browser_stop_network_capture({ tabId, format: "har" })
What the agent learned, still there next time
Sessions, notes, recordings and screenshot metadata all live in Postgres. A run that ends is not a run that disappears.
Console output is collected per tab from the moment it opens — no flag to set, nothing to start. That includes uncaught exceptions, which is usually the line that actually explains the failure.
browser_get_console_logs({ tabId, level: "error", limit: 50 })
An agent that works out a page’s quirk can write it down and read it back later, tagged and searchable, instead of solving the same puzzle on every run.
browser_add_note({
sessionId,
content: "Cookie banner intercepts the first click. "
"Dismiss #cc-accept before anything else.",
tags: ["quirk", "shop.example.com"]
})
browser_list_notes({ sessionId, tag: "quirk" })
- Modes
- Full-page scrolling capture, viewport only, or a single element.
- Storage
- Uploaded to any S3-compatible bucket; the tool returns a URL. Without storage configured it returns base64 instead.
- Private buckets
- A proxy endpoint serves images the bucket will not expose directly, with a one-year cache header.
- Retention
- A cron job soft-deletes on your schedule, then hard-deletes from both database and bucket.
The same browser, from your shell
A browser session is stateful; a CLI is a series of short-lived processes. b4m bridges the two — it remembers the session and tab it opened, so you never paste back an id it just printed you.
curl -fsSL https://browse4.me/cli/install.sh | bash b4m login b4m project join my-project b4m open https://app.example.com
find and query number what they return, so you act on !1 rather than copying an XPath out of a table.
b4m open https://app.example.com# session + tab, rememberedb4m find "Sign in"# numbered matchesb4m click !1 b4m type !2 "hunter2" b4m shot# prints the screenshot URLb4m logs -l error b4m shell https://app.example.com# interactive
-o json emits records rather than a rendered table, and colour disappears when stdout is not a terminal — so redirecting gives you a file, not escape codes.
b4m query links --json | jq -r '.[].attributes.href' b4m run checkout-flow.json# replay a Recorder file# CI cannot complete a device flow — use a project keyexport BROWSE4ME_API_KEY=proj_... b4m open https://staging.example.com
b4m tools is built from the live server, so it cannot drift out of date the way a hand-written list does.
b4m tools# all 43b4m tools browser_wait_for_element# its argumentsb4m call browser_wait_for_element '{"xpath":"//h1","timeout":5000}' b4m whoami# which project, which URL, and where each setting came from
Teach it by doing it once
- Clicks, text input, scrolls and navigation, as you perform them
- Written out as Chrome Recorder JSON — the same format the agent records
- Uploaded to your project, scoped to your API key
- Load the unpacked extension, set server URL and project key in options
- Hit Start recording in the popup and use the site normally
- Stop, then Upload
- The agent lists and replays it over MCP
An agent with a browser is an agent with reach
Every endpoint that touches tenant data authenticates, and every project is isolated from every other. Sessions, tabs, notes, recordings and screenshots are only ever reachable by the project that created them.
proj_-prefixed, bcrypt-hashed, scoped to one project. Rotate or revoke without touching anything else.
Federated identity verified against BitBot’s JWKS, with admin and user roles — and a system token for platform-to-platform calls.
Navigation to loopback, private, link-local, cloud-metadata and in-cluster addresses is refused — checked before the request and again after every redirect.
Restrict a project to the domains it is supposed to visit, so a misled agent cannot wander off with your data.
Confined to a configured upload root, and switched off entirely unless you turn it on.
Exact-match origin checking for credentialed requests. No wildcard reflection.
Plan limits arrive from BitBot on the project record. What /bitbot/limits advertises is what the server actually enforces — counters live in Postgres, behind a short cache so a tool call does not pay for a query every time.
- browsesPerMin
- Navigations per rolling minute
- sessionsPerDay
- Sessions created per UTC day
- screenshotsPerDay
- Screenshots per UTC day
- recordingsPerMonth
- Recordings per UTC month
- maxConcurrentSessions
- Live sessions at once
- networkCaptureEnabled
- Feature flag — capture and interception
- harExportEnabled
- Feature flag — HAR export specifically
An unset limit, or -1, means unlimited. Feature flags describe what a plan includes rather than how hard it may be used, so the operator kill switch that disables rate limits deliberately does not grant them.
How a tool call reaches a browser
Seven guides the agent can read without leaving the session — plus live resources for current sessions, tabs and recordings.
welcome://message docs://best-practices docs://search-strategies docs://framework-clicks docs://network-capture docs://console-logs docs://recording-guide
Point an agent at it
Connect any MCP-compatible client to the server’s /mcp endpoint with a project API key, and the 43 tools show up in its tool list.
npm install npm run build npm start# http://localhost:3000# Chromium and Chrome are installed for you on postinstall.# Configure PORT, DEFAULT_BROWSER_ENGINE, S3_* and the auth# settings in .env before pointing anything real at it.