浏览器自动化MCP服务

Mare Browser MCP 是一个轻量级、面向大语言模型的浏览器自动化 MCP 服务器,通过 Playwright 和 MCP SDK 为 Claude 等客户端提供真实 Chromium 浏览器的导航、交互与调试能力,并支持连接用户现有 Chrome 浏览器或使用隔离浏览器。

  • by emadklenka
  • JavaScript
  • 本地部署
  • 6 星标
  • 更新于
在 GitHub 查看 emadklenka/mare_browser_mcp

使用说明

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 chromium

This is the fastest way to run the server — starts instantly with no registry lookups.


Alternative installs

Global install — no cloning, still fast:

pnpm add -g mare-browser-mcp
npx playwright install chromium

Register with Claude Code

If you cloned the repo, the setup script does it for you:

pnpm run setup

That'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:

  1. Open chrome://extensions, enable Developer mode, and click Load unpacked.
  2. Select this repository's extension directory. Repeat in every Chrome profile you want the AI to use.
  3. 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
browser_tabs() List tabs in every connected profile: id, title, URL, browser/browserLabel (the profile), controlledBy. Read-only.
browser_attach({...}) Take control of a tab. See below.
browser_detach({ close?, handoff? }) Release the tab and remove its AI marker.
browser_status() Backend in use, the attached tab (attached / navigated / closed), connected profiles.
browser_isolated({ fresh? }) Switch to Mare's own Chromium; fresh:true starts with no cookies or storage.
// 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:

  1. The second session calls browser_attach({ tab_id, request_release: true }).
  2. The owner sees a releaseRequested notice in its next result and grants it with browser_detach({ handoff: true }).
  3. 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_id is optional: the MCP process remembers its session. Keep the session_id returned by browser_attach only 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 v4 MARE_SESSION_ID per 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_BROWSER or NO_ACTIVE_TAB, call browser_tabs(), match the tab the user meant and attach it with tab_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
ref { action: "click", ref: "e9" } From browser_snapshot; pinned to that exact element. Preferred.
role + name { action: "click", role: "button", name: "Create repository" } No snapshot needed. Same names as the snapshot. Partial, case-insensitive match unless exact: true; nth picks among several; ambiguity is an error listing the matches.
selector { action: "fill", selector: "#email", value: "a@b.co" } CSS, for elements outside the accessibility tree.
text { action: "clicklink", text: "Download quote" } clicklink only.

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
click target, button, delay_ms, expect Click (left/right/middle); delay_ms holds the press
clicklink text Click a link/button by visible text
hover target Move the mouse over it (menus, tooltips)
fill target, value Replace the content by typing; "" clears
keypress key, optional target Enter, Tab, Escape, ArrowDown, a, Control+a, Shift+Tab… Focuses the target first if given
select target, value Choose a <select> option
drag target, target or offsetX/offsetY Drag to an element or by pixels
dispatch target, event, detail Fire a DOM/CustomEvent (web components)
waitfor target, timeout Wait until it appears
scrollto target Scroll it into view
wait ms Pause
clearconsole — 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_ms timing
  • Dialog 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
all Return all matching elements (default: first only)
fields Pick fields: text, value, visible, disabled, className, href, innerHTML
visible_only Filter to visible elements only — recommended for broad selectors
limit Cap the number of results (e.g. 10) to prevent huge payloads
count_only 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).backgroundColor
  • Append text to inputs without clearing
  • Type character-by-character for autocomplete
  • Drag-and-drop via manual DOM events
  • Call fetch() to hit APIs directly
  • Read 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: 500
  • Scroll 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 desktop
  • custom — requires custom.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 in

Hover + 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" })  // reset

Environment

Variable Default Description
HEADLESS false Run browser headless (true) or visible (false)
REAL_CHROME false Use your installed Chrome instead of Playwright's Chromium
CHROME_PROFILE Default Chrome profile name (when REAL_CHROME=true)
CAPTURE_DIR OS temp + mare-browser-mcp Screenshot and video artifact directory
BROWSER_MODE isolated Startup backend: isolated or existing
MARE_BRIDGE_PORT 37431 Shared localhost extension-daemon port (the unpacked extension scans 37431–37440)
MARE_CLIENT_NAME Mare <pid> Default label used for this MCP session's pinned tab
MARE_SESSION_ID 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 ☕