mare-browser-mcp
An LLM-first browser automation MCP server that gives an agent a real Chromium browser to navigate, interact with, and debug web apps without raw Playwright APIs.
Navigate to any URL, optionally clearing console/network logs for a fresh task.
Act on the page in batched sequences: click (left/right/middle), hover, drag (element-to-element or by pixel offset), clicklink by text, fill, select, keypress, waitfor, scrollto, wait, and clearconsole.
Target elements via accessibility refs from
browser_snapshot(preferred, churn-resistant) or raw CSS selectors.Inspect the DOM with
browser_query: read text, value, visibility, disabled state, className, href, innerHTML; count elements; filter to visible only.Snapshot the accessibility tree with refs, optional compact mode and depth control, plus
data-testidanchors.Debug in one call: URL, title, console logs, dialogs, and rich network requests (method, headers with auth masked, status, body, timing), with filters.
Wait for conditions: URL changes/redirects (
browser_wait_for_url) and specific network responses (browser_wait_for_network).Run JavaScript in the page context as an escape hatch for computed styles, app state, custom events, and anything the other tools miss.
Make authenticated requests with
browser_fetch, inheriting the page's cookies and session.Scroll the page, a specific container (grids, chat panels), or an element into view.
Upload files to file inputs by CSS selector.
Emulate devices — iPhone, Galaxy, iPad, Galaxy Tab presets, desktop reset, or custom — with viewport/UA verification; emulation persists until swapped or restarted.
Take screenshots as a last resort, in thumbnail, normal, or full-page resolution.
Restart the browser to recover from crashes or stuck pages, optionally navigating afterward.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mare-browser-mcp go to example.com and get the page title"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mare-browser-mcp
A lean, LLM-first browser automation MCP server. Gives Claude (or any MCP client) a real Chromium browser to navigate, interact with, and debug web apps — without the overhead of raw Playwright APIs.
Built with Playwright + MCP SDK. Isolated mode keeps one browser per MCP server; existing-browser commands from multiple MCP clients are serialized through one local bridge.
Mare can use either its own isolated Playwright browser or your already-running, signed-in Chromium through the bundled Mare Browser Bridge extension.
Free to use. If it saves you time, buy me a coffee ☕
Install (recommended)
Prerequisites: Node.js 18+, pnpm
git clone https://github.com/emadklenka/mare_browser_mcp
cd mare_browser_mcp
pnpm install
npx playwright install chromiumThis is the fastest way to run the server — starts instantly with no registry lookups.
Related MCP server: Browser Jet Pilot
Alternative installs
Global install — no cloning, still fast:
pnpm add -g mare-browser-mcp
npx playwright install chromiumRegister with Claude Code
If you cloned the repo, the setup script does it for you:
pnpm run setupThat's it. The script detects the correct path automatically and registers the MCP with Claude Code. Restart Claude Code and the browser tools are ready.
Manual config — add to ~/.claude.json under mcpServers:
{
"mcpServers": {
"mare-browser": {
"command": "node",
"args": ["/absolute/path/to/mare_browser_mcp/src/index.js"],
"env": { "HEADLESS": "false" }
}
}
}If installed globally:
{
"mcpServers": {
"mare-browser": {
"command": "mare-browser-mcp",
"env": { "HEADLESS": "false" }
}
}
}Register with OpenCode
Add this to ~/.config/opencode/opencode.json (global) or opencode.json (project root):
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"mare_browser_mcp": {
"type": "local",
"command": [
"node",
"/absolute/path/to/mare_browser_mcp/src/index.js"
]
}
}
}If installed globally:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"mare_browser_mcp": {
"type": "local",
"command": ["mare-browser-mcp"]
}
}
}Tools
Choose a browser
Mare can drive two kinds of browser:
Your real Chrome (recommended) through the bundled extension: your own profiles, cookies and logins.
Mare's isolated Chromium (Playwright): for when no extension is connected, or to test without your cookies or logins.
Set up the extension once:
Open
chrome://extensions, enable Developer mode, and click Load unpacked.Select this repository's
extensiondirectory. Repeat in every Chrome profile you want the AI to use.Open Mare Browser Bridge from the toolbar, enable browser control and give the profile a name (for example "Work" or "CPT").
Several profiles can be connected at the same time. Each one is a separate browser to the AI, labelled with the name you gave it.
Session tools
Tool | Does |
| List tabs in every connected profile: |
| Take control of a tab. See below. |
| Release the tab and remove its AI marker. |
| Backend in use, the attached tab ( |
| Switch to Mare's own Chromium; |
// The tab you offered with the popup's "Let AI use this tab" button,
// otherwise the current tab.
browser_attach()
// An exact tab from browser_tabs (the id also selects its profile).
browser_attach({ tab_id: 42 })
// A new tab in a new real Chrome window, logged in like the rest of the profile.
browser_attach({ new: true })
browser_attach({ new: true, browser: "<profile id from browser_tabs>" })
// Separate Chromium with no saved login/cookies/storage.
browser_isolated({ fresh: true })The popup buttons. Let AI use this tab offers the tab you are looking at;
the next browser_attach() takes exactly that tab, in that profile, for 5
minutes. Release all AI tabs detaches every AI session in that profile and
clears the markers; tabs stay open.
Dedicated windows. By default an attached tab is moved (not duplicated) into
its own window, so you and the AI do not fight over focus. Its page and login are
untouched. Moving the last tab may close its now-empty original window. Use
dedicated_window:false only when you explicitly want in-place control. A window
in the same profile shares login state; it is not a security boundary.
Pinned tabs. A session is pinned to one exact tab. Mare marks it with a teal
AI favicon and an AI badge. If the tab is closed or navigated by hand, Mare
stops with a clear error (TAB_CLOSED, TAB_NAVIGATED) instead of taking over
another tab. Attach with recovery: "reopen" to reopen a closed tab at its last
URL; the default is recovery: "ask".
Several AI sessions. Sessions attach different tabs; give each a label.
When two want the same tab, Mare uses a cooperative handoff:
The second session calls
browser_attach({ tab_id, request_release: true }).The owner sees a
releaseRequestednotice in its next result and grants it withbrowser_detach({ handoff: true }).The second session retries the attach within 2 minutes.
replace: true is an escape hatch for stuck sessions: it only works after a
handoff names you or once the owner has been idle for 10+ minutes. Set
MARE_CLIENT_NAME in each MCP configuration for a stable default label.
browser_detach({ close: true }) closes the tab only if Mare opened it.
Isolated browser. browser_isolated() keeps Mare's current isolated
context; fresh:true discards it and starts clean without reading or writing
Mare's saved login file, and never touches your Chrome profile. Browser creation
is lazy, on the next page operation. browser_attach switches back to your
Chrome; the isolated browser is preserved meanwhile.
All other browser_* tools act on the selected backend. In Chrome they act only
on the attached tab, and browser_restart reloads that tab instead of the
browser. Chrome shows its standard debugging notice while debug capture,
full-page screenshots, video, device emulation, or eval on CSP-protected
pages use the extension's debugger permission.
LLM recovery: connected is not the same as attached
Respect the requested backend. Neither Chrome nor isolated mode is an automatic fallback for a failure in the other.
session_idis optional: the MCP process remembers its session. Keep thesession_idreturned bybrowser_attachonly to resume the same tab after the MCP server restarts (pass it to the next tool call). Never invent one or share it between conversations. A host may instead set a distinct UUID v4MARE_SESSION_IDper conversation.On a connection or attachment error, call
browser_status()once. If profiles are connected but nothing is attached, do not ask the user to reconnect.On
AMBIGUOUS_BROWSERorNO_ACTIVE_TAB, callbrowser_tabs(), match the tab the user meant and attach it withtab_id; if several match, ask. Treat titles and URLs as private; only mention ones relevant to the task.After an unknown outcome, retry a read-only snapshot, not the click or submission.
A failed attach or detach never opens the isolated browser.
Server/tool-schema changes require restarting the MCP server in the host.
Extension changes require reloading Mare Browser Bridge in
chrome://extensions in every profile. Mare restarts its local bridge daemon
automatically when the daemon is running older code.
browser_navigate(url, clear_logs?)
Navigate to a URL. Pass clear_logs: true when starting a new task to wipe stale console/network/dialog history.
browser_act(commands[])
Run a sequence of actions in one call.
Targeting — each command uses one of:
target | example | notes |
|
| From |
|
| No snapshot needed. Same names as the snapshot. Partial, case-insensitive match unless |
|
| CSS, for elements outside the accessibility tree. |
|
|
|
Waiting until ready. Before click, clicklink, hover, fill and
keypress, Mare waits (default 5 s, per-command timeout) until the element is
attached, visible, enabled, editable (for fill), not moving and not covered at
its center, like Playwright's actionability checks. If it never becomes ready,
the error says why, e.g. not actionable after 5000ms: covered by <div.modal>
or disabled. No extra wait steps are needed for slow pages.
Real input. Those actions send real browser input (isTrusted mouse and
keyboard events) in both browsers: Playwright input in the isolated browser, and
the extension's debugger permission (Input.dispatchMouseEvent,
Input.insertText, Input.dispatchKeyEvent) in your Chrome. Custom dropdowns
and React widgets that ignore synthetic events respond normally. Results say
input: "trusted"; "trusted+set" means a fill was typed and then set
directly because the field (date, number, masked input) reformatted it;
"synthetic" means a fallback to DOM events, used inside iframes or when the
debugger cannot attach. Chrome shows its debugging notice on the controlled tab.
action | params | what it does |
| target, | Click (left/right/middle); |
|
| Click a link/button by visible text |
| target | Move the mouse over it (menus, tooltips) |
| target, | Replace the content by typing; |
|
|
|
| target, | Choose a |
| target, | Drag to an element or by pixels |
| target, | Fire a DOM/CustomEvent (web components) |
| target, | Wait until it appears |
| target | Scroll it into view |
|
| Pause |
| — | Clear the console buffer |
Every command also accepts frame, using an id returned by browser_frames.
Refs captured by a frame-targeted snapshot remember their frame automatically.
New tabs and popups. If the page opens a tab or popup (target=_blank,
window.open, an OAuth sign-in window), the next result in your Chrome includes
openedTabs: [{ tab_id, url, title, popup }]. Call browser_attach({ tab_id })
to work in it. (The isolated browser does not follow new pages yet.)
click_mode: "mouse" forces the older synthetic mousedown/mouseup/click
sequence in your Chrome; it is rarely needed now.
Outcome checks
expect accepts exactly one visible CSS selector or literal url_pattern
substring, an optional destination frame (id/name/URL; defaults to the action
frame), and a 1–30000 ms timeout. For a newly created iframe, use a stable
frame name. Choose a destination-specific condition: an already visible element
or matching URL also satisfies the check; this does not prove a transition.
Results distinguish action_status: "dispatched" from outcome: "observed".
Without an expectation, the outcome is not_requested. An unmet expectation
returns success: false and outcome: "timed_out" (or "unavailable" when the
final probe failed), and stops the remaining batch. Only observations are
retried, never the click. Do not repeat a possibly submitted action automatically.
An interrupted script during full navigation can leave dispatch status unknown;
inspect the destination before deciding what to do next.
Requires Bridge 1.1.2+. Reload the unpacked extension and restart the MCP client after updating to expose the new tool parameters. The server rejects these options with an older loaded Bridge, instead of silently ignoring them.
browser_frames()
List the main document and all nested iframes, including cross-origin frames.
Each result includes an id such as f0 or f7, its URL, name, parent, and
whether it is the main frame. Pass that id to browser_snapshot,
browser_query, browser_eval, or an action command:
browser_frames()
browser_snapshot({ frame: "f7", compact: true })
browser_act({ commands: [{ action: "click", ref: "e1" }] })
browser_eval({ frame: "f7", code: "location.origin" })browser_snapshot(max_depth?, compact?, frame?)
Return a compact accessibility tree plus a flat refs list (ref, role,
name). Both browsers use the same walker. Every interactive element gets a ref
however deeply it is nested: max_depth (default 10) limits only how many
semantic levels are shown, and past it interactive elements are listed flat.
Inputs are named from aria-label, aria-labelledby, <label>, placeholder or
title; containers such as lists are not named after their whole text. Open
shadow roots are included (their refs are marked shadow: true and work with
browser_act). Hidden (display:none, aria-hidden) content is skipped.
compact defaults to true; pass false to keep wrapper elements. Very large
pages are capped and report truncated: true.
It targets the main document by default; pass a frame id, exact frame name, or frame URL to inspect an embedded application directly.
browser_downloads(wait_ms?)
Files downloaded by the current page, e.g. a PDF quote after a click:
{ downloads: [{ path, url, mime, state, bytes, ... }] }. path is the file on
disk (your Chrome's download folder, or Mare's capture directory for the isolated
browser), so it can be opened directly. wait_ms waits for a download to start
or finish. In your Chrome, a download is linked to the AI's tab when it starts
within 15 seconds of a command on that tab.
browser_debug()
Start here when something goes wrong. Returns in one call:
Current URL and page title
Console logs (filterable by type:
error,warning,log,pageerror)Network request metadata with: method, URL, redacted query params, request headers (auth masked), status code, and
duration_mstimingDialog history (alert/confirm/prompt — auto-accepted, text captured)
Filter with url_filter, method_filter, console_types, or last_n.
Request and response bodies are omitted by default. Set include_bodies: true only when necessary; credential-like keys are recursively redacted in requests, responses, and query parameters.
Console messages and page errors are also scrubbed for JWTs, bearer values,
tokens, API keys, authorization values, cookies, session ids, and passwords
before they enter Mare's log buffer.
browser_query(selector, all?, fields?, visible_only?, limit?, count_only?, frame?)
Read the DOM without a screenshot. Query any element by CSS selector.
param | what it does |
| Return all matching elements (default: first only) |
| Pick fields: |
| Filter to visible elements only — recommended for broad selectors |
| Cap the number of results (e.g. |
| Just return the count — fast way to check "how many rows?" without fetching data |
browser_eval(code, frame?)
Escape hatch for anything the other tools don't cover:
Read computed styles:
getComputedStyle(el).backgroundColorAppend text to inputs without clearing
Type character-by-character for autocomplete
Drag-and-drop via manual DOM events
Call
fetch()to hit APIs directlyRead JS app state (
window.__store__, etc.)Check CSS visibility (
display,opacity,visibility)
browser_scroll(direction?, pixels?, selector?, container?)
Three modes:
Page scroll:
direction: "down", pixels: 500Scroll into view:
selector: ".my-element"Scroll within a container:
container: ".ag-body-viewport", direction: "down", pixels: 300— for scrollable divs, grid viewports, chat panels
browser_wait_for_network(url_pattern?, method?, timeout?)
Wait for a specific network response after triggering an action — smarter than guessing with wait.
browser_screenshot()
Returns a PNG screenshot. Use as a last resort — prefer browser_debug and browser_query first.
browser_save_screenshot(filename?, full_page?, format?, hide_recording_pointer?)
Save a screenshot as an artifact under the OS temp directory and return its absolute path, MIME type, byte size, physical pixel dimensions, CSS viewport, device-pixel ratio, URL, and page title. This is the preferred screenshot tool for QA evidence, documentation, and marketing assets because it avoids returning a large base64 payload.
Mare hides its recording pointer before saved screenshots by default, preventing a completed action clip from contaminating later clean or target stills. Set hide_recording_pointer: false only when intentionally documenting the pointer itself.
browser_save_screenshot({ filename: "candidate-grid", full_page: true, format: "png" })
// -> { ok: true, path: "/tmp/mare-browser-mcp/candidate-grid.png", ... }Set CAPTURE_DIR to override the default temp artifact directory.
browser_video(action, filename?, format?, ...)
Record a precise Playwright screencast. action is start, stop, status, or capture_click; format is webm (default) or mp4. Screencast start and stop operate on the live page without recreating the browser context, so in-memory application state is preserved. While recording, click actions show a translucent yellow pointer and pulse. Mare now hides that pointer automatically after every stop.
The default capture_scale: "device" records at device-pixel dimensions so video and ordinary viewport PNGs share the same native canvas on high-DPI displays. Use capture_scale: "css" for a smaller CSS-pixel recording. Start/status/stop responses report the Mare version, CSS viewport, device-pixel ratio, capture scale, and output size.
browser_video({ action: "start", filename: "candidate-walkthrough", format: "mp4" })
// perform browser actions
browser_video({ action: "stop" })
// -> { ok: true, path: "/tmp/mare-browser-mcp/candidate-walkthrough.mp4", mode: "screencast", ... }For short product-storyboard actions, prefer the atomic form. It performs the start, one click, optional URL wait, short click-pulse tail, and stop inside one MCP call, avoiding static padding caused by model/tool round trips:
browser_video({
action: "capture_click",
filename: "open-candidate",
format: "mp4",
selector: "[data-testid='candidate-link']",
wait_for_url: "/cnd/",
timeout: 2500,
post_click_ms: 450
})capture_click returns the source and destination URLs, action success, URL-match result, finalized artifact metadata, and pointer-cleanup result. MP4 output is automatically transcoded to high-quality H.264 and requires ffmpeg on PATH.
On Playwright versions older than 1.59, Mare retains the previous context-level WebM recorder as a compatibility fallback. Stop an active recording before calling browser_restart.
browser_upload(selector, files[])
Upload files to a file input element.
browser_restart(url?)
Kill the browser and start fresh. Clears all logs. Optionally navigate to a URL after restart.
browser_emulate_device(device, orientation?, custom?)
Switch the browser into a device profile for responsive QA. Emulation persists across navigations until you swap devices or call browser_restart.
Presets (natural portrait viewport):
iphone-15-pro-max(430×932),iphone-15-pro(393×852),iphone-15(393×852),iphone-se(375×667)galaxy-s24(360×800)ipad-pro-13(1024×1366),ipad-pro-11(834×1194),ipad-mini(768×1024)galaxy-tab-s9(800×1280)desktop-chrome(1280×800) — resets to desktopcustom— requirescustom.userAgent+custom.viewport.{width, height}
Swapping devices recreates the browser context, so cookies and localStorage are lost and auth'd pages may land on login. innerWidth: 980 on a mobile emulation viewing a page without <meta name="viewport"> is Chrome's legacy fallback, not a bug — pointer_coarse, hasTouch, and userAgent are the authoritative signals. browser_debug surfaces the active emulation under an emulation field.
Example workflow
1. browser_navigate("https://myapp.com", clear_logs: true)
2. browser_act([
{ action: "fill", selector: "#email", value: "user@example.com" },
{ action: "fill", selector: "#password", value: "secret" },
{ action: "click", selector: "button[type=submit]" }
])
3. browser_wait_for_network({ url_pattern: "/api/session", method: "POST" })
4. browser_debug({ console_types: ["error"] }) <- check for login errors
5. browser_query(".dashboard-title") <- confirm we're logged inHover + tooltip example
1. browser_act([{ action: "hover", selector: ".info-icon" }])
2. browser_query(".tooltip", { fields: ["text", "visible"] })Drag-and-drop example
// Reorder columns
browser_act([{ action: "drag", selector: ".col-name", target: ".col-age" }])
// Resize a column by 100px
browser_act([{ action: "drag", selector: ".resize-handle", offsetX: 100, offsetY: 0 }])Right-click context menu
1. browser_act([{ action: "click", selector: ".grid-row", button: "right" }])
2. browser_query(".context-menu-item", { all: true, fields: ["text"] })Scroll inside a container
browser_scroll({ container: ".ag-body-viewport", direction: "down", pixels: 500 })Count elements quickly
browser_query({ selector: ".ag-row", count_only: true })
// -> { selector: ".ag-row", count: 47 }Emulate a mobile device
1. browser_emulate_device({ device: "iphone-15-pro-max" })
2. browser_navigate({ url: "https://www.youtube.com" })
// redirects to m.youtube.com because of the iPhone UA
3. browser_screenshot() // mobile layout
4. browser_emulate_device({ device: "ipad-pro-13", orientation: "landscape" })
5. browser_emulate_device({ device: "desktop-chrome" }) // resetEnvironment
Variable | Default | Description |
|
| Run browser headless ( |
|
| Use your installed Chrome instead of Playwright's Chromium |
|
| Chrome profile name (when |
| OS temp + | Screenshot and video artifact directory |
|
| Startup backend: |
|
| Shared localhost extension-daemon port (the unpacked extension scans |
|
| Default label used for this MCP session's pinned tab |
| Random UUID per process | Optional UUID v4, unique per logical conversation, to retain attachment identity across process restarts. Never share across conversations. |
The shared extension bridge listens only on 127.0.0.1; the extension scans ports 37431–37440.
It accepts browser clients only from a chrome-extension:// origin. No cookies,
passwords, or browser profile data are copied into Mare; commands execute in the
selected existing tab.
The browser launches lazily — it won't open until the first tool call.
License
MIT — free to use, modify, and distribute.
If this project helps you, buy me a coffee ☕
Available Tools
14 toolsbrowser_actA
Perform one or more browser actions in sequence. Batch multiple steps into one call.
Two ways to target an element — use whichever is more stable: • ref — accessibility ref from browser_snapshot (preferred). LLM-friendly: no selector guessing, survives CSS class churn, resistant to obfuscated build output. Each ref is pinned to the exact element captured in the snapshot, so it never silently drifts to a different element when the page reflows. If that element is removed or re-rendered, the action fails loudly ("stale ref" / element not found) — re-snapshot rather than retrying. Call browser_snapshot first to get refs like "e9", "e42", then pass them to actions: { action: "click", ref: "e9" }. • selector — raw CSS selector. Use when you already know it, or for elements not in the a11y tree.
Example ref flow:
browser_snapshot() → { snapshot, refs: [{ref: "e9", role: "button", name: "Sign in"}] }
browser_act({ commands: [{ action: "click", ref: "e9" }] })
Available actions: • click — click an element (supports left/right/middle button). Use button:'right' for context menus • hover — hover over an element for tooltips, dropdown menus, hover states • drag — drag an element to a target selector (column reorder, kanban) OR by pixel offset (column resize, sliders). Use target for element-to-element, offsetX/offsetY for precise pixel drag • clicklink — click a link/button by visible text (text-based; does not use ref/selector) • fill — fill an input field (clears first) • select — select a dropdown option • keypress — press a key (Enter, Tab, Escape, etc.) — keyboard action, no ref/selector needed • waitfor — wait for an element to appear • scrollto — scroll an element into view • wait — pause for N milliseconds (no target) • clearconsole — clear captured console logs (no target)
click, hover, drag, fill, select, waitfor, scrollto all accept either 'ref' or 'selector'. If both are provided, 'ref' wins. Refs are invalidated on navigation — re-snapshot if the page has changed.
Inputs: commands (required).
browser_debugA
PREFERRED DEBUGGING TOOL. Returns current URL, page title, console logs, dialogs (alert/confirm/prompt), and rich network requests in one call. Always call this before browser_screenshot. Network entries include: method, URL, query params (parsed), request body (JSON/form), request headers (auth masked), status code, response body (JSON), and duration_ms for performance analysis. Use url_filter and method_filter to focus on specific API calls. Use console_types to filter log levels.
Inputs: last_n, url_filter, console_types, method_filter.
browser_emulate_deviceA
Switch the browser session into a device emulation profile (iPhone / iPad / Android tablet / desktop reset / custom). Emulation lives on the browser context — it persists across browser_navigate calls until you swap to another device or call browser_restart.
IMPORTANT behaviors: • Swapping devices recreates the browser context but cookies + localStorage are PRESERVED (via persistent storageState). Pages behind auth stay logged in across swaps. The tool auto-navigates back to the URL you were on. IndexedDB is not preserved — PWAs that store auth tokens there may require re-login. • innerWidth: 980 on a mobile emulation is NOT a bug. It means the current page has no and is using Chrome's legacy fallback. Check verified.layout_mode — it will be "legacy-980-fallback" in this case. The authoritative signals that emulation is working are: userAgent, pointer_coarse, hasTouch, devicePixelRatio. • browser_restart always clears emulation (back to desktop). • Not supported in REAL_CHROME mode (returns an error).
Presets and natural viewports (portrait): • iphone-15-pro-max (430×932), iphone-15-pro (393×852), iphone-15 (393×852), iphone-se (375×667) • galaxy-s24 (360×800) • ipad-pro-13 (1024×1366), ipad-pro-11 (834×1194), ipad-mini (768×1024) • galaxy-tab-s9 (800×1280) • desktop-chrome (1280×800, DPR 1, no touch — use this to reset) • custom — requires custom.userAgent + custom.viewport.{width,height}
Returns { ok, active, previous_url, previous_url_restored, verified, checks, warnings? }. ok: true as long as the essential fields (UA substring + devicePixelRatio) match; soft drift in hasTouch or pointer_coarse surfaces as warnings[] without flipping ok. On hard failure: { ok: false, error, verified, checks }.
Inputs: device (required), custom, orientation.
browser_evalA
Execute JavaScript in the page context and return the result. This is your ESCAPE HATCH for anything the other tools don't cover. Common use cases: • Read computed styles: getComputedStyle(el).backgroundColor • Append text to inputs without clearing: el.value += '...'; el.dispatchEvent(new Event('input', {bubbles:true})) • Type character-by-character for autocomplete: use dispatchEvent with input events per character • Drag-and-drop: create and dispatch mousedown/mousemove/mouseup or dragstart/drop events • Read JS app state: window.store, React devtools, etc. • Check visibility via CSS: getComputedStyle(el).display, opacity, visibility • Scroll inside a container: document.querySelector('.container').scrollTop += 500 The code is evaluated as an expression — use an IIFE for multi-statement code.
Inputs: code (required).
browser_fetchA
Execute an authenticated fetch() request inside the page context. Inherits the page's cookies and session — same-origin requests work automatically, cross-origin requires CORS. Returns status, headers, and parsed body. Appears in browser_debug network log.
Inputs: url (required), body, parse, method, +1 more.
browser_queryA
Query DOM elements by CSS selector. Use to check element state, text, visibility, or values without taking a screenshot. Use visible_only and limit to avoid huge result sets on broad selectors.
Inputs: selector (required), all, limit, fields, +2 more.
browser_restartA
Close and reopen the browser session. Use when the page is dead, crashed, or stuck after navigating to an external site. Optionally navigate to a URL immediately after restart.
Inputs: url.
browser_screenshotA
LAST RESORT. Returns a screenshot as base64. Expensive and unstructured. Only use when the problem is purely visual (layout, rendering glitch). Always try browser_debug and browser_query first.
Inputs: quality.
browser_scrollA
Scroll the page, scroll within a specific container element (e.g. AG-Grid viewport, chat panel, sidebar), or scroll an element into view. Use 'container' to scroll inside scrollable divs instead of the page.
Inputs: pixels, selector, container, direction.
browser_snapshotA
Return the accessibility tree of the current page with refs attached to interactive elements (buttons, links, textboxes, checkboxes, etc.). Use this INSTEAD of guessing CSS selectors from a screenshot — selectors break on class-name churn, refs don't.
Returns { url, snapshot: [...tree], refs: [{ref, selector, role, name, testId}, ...] }.
Each ref like "e9" maps internally to a stable selector. Pass refs to browser_act: browser_act({ commands: [ { action: "fill", ref: "e42", value: "user@example.com" }, { action: "click", ref: "e9" } ]})
Every node includes testId when the element has data-testid, data-test, or data-qa — use these as additional stability anchors. Refs are invalidated automatically on navigation — re-snapshot after any browser_navigate or URL change.
Use compact: true to drop pure layout wrappers (divs/spans with no role, no testId) and return a flatter tree focused on interactive content. Recommended for noisy apps with deeply-nested div soup.
Inputs: compact, max_depth.
browser_uploadA
Upload one or more files to a file input element. Use the CSS selector to target the input and provide absolute file paths.
Inputs: files (required), selector (required).
browser_wait_for_networkA
Wait for a specific network response matching URL pattern and/or method. Returns the response with status and JSON body. Use after triggering an action to wait for its API call to complete instead of guessing with wait times. url_pattern accepts a single substring OR an array of substrings (any-of match — resolves on the first matching response).
Inputs: method, timeout, url_pattern.
browser_wait_for_urlA
Wait for the page URL to change and match a substring pattern. Use after actions that trigger redirects (JS redirects, auth redirects, SPA route changes). Returns current URL on timeout. Optionally chain a readiness gate via wait_for so you don't have to follow up with setTimeout/wait.
Inputs: pattern (required), exact, timeout, wait_for.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 14 tools
Each tool has a clearly distinct purpose: navigation, action execution, debugging, DOM querying, snapshotting, waiting, screenshotting, JS eval, scrolling, restarting, uploading, network waiting, and device emulation. Even the inspection tools (debug, query, snapshot, eval) are well separated by what they return and when to use them.
All 14 tools follow the consistent pattern of 'browser_' prefix followed by a lowercase verb (navigate, act, debug, query, fetch, snapshot, wait_for_url, screenshot, eval, scroll, restart, upload, wait_for_network, emulate_device). No mixing of camelCase or inconsistent verb styles.
14 tools is within the ideal 3-15 range and each tool serves a distinct aspect of browser automation—navigation, interaction, inspection, waiting, debugging, device emulation, and file upload. The count is well-scoped for a comprehensive browser control server without being bloated.
The server covers the full lifecycle of browser interaction: navigate, act, inspect, wait, debug, screenshot, and emulate. Minor gaps exist such as no explicit dialog acceptance (alert/confirm/prompt) or tab management, but agents can work around these with browser_eval in most cases, so they are not critical dead ends.
Related MCP Connectors
Official Browserless MCP server: browser automation, scraping, screenshots and PDFs for AI agents.
Hosted Web Crawler MCP server for Claude, ChatGPT and Cursor, with each action switched per account.
Live browser debugging for AI assistants — DOM, console, network via MCP.
Web scraping, browser automation, screenshots and AI data extraction for AI agents via MCP.
Related MCP Servers
- AlicenseCqualityDmaintenanceAn MCP server that gives Claude Code real browser control for web automation, testing, and screenshots.3263 npm157MIT
- AlicenseAqualityDmaintenanceSelf-hosted MCP server for AI browser automation. Connects to your own Chromium instance via CDP, providing tools for browser control, navigation, interaction, and content extraction.191MIT
- AlicenseNot gradedqualityDmaintenanceLocal browser automation MCP server for Claude Code, enabling navigation, clicking, typing, and inspecting real web pages via Google Chrome.20 npm1MIT
- FlicenseNot gradedqualityDmaintenanceA Chrome browser automation MCP server. Connect Claude (or any MCP client) to a real Chrome session — using your existing profiles with their cookies, saved passwords, and history.3-