@ricardochen1996/browser-use-mcp
English · 中文
An MCP server that gives any agent a browser it can see and click: a page is read as an indexed action-space table, and every step is one guarded operation on a target that came from that table.
browser-use-mcp is a Model Context Protocol server. Claude
Code, Claude Desktop, Cursor, Codex, a DeepSeek Harness conversation, or any other MCP client
connects to it, and its agent gets a browser: open a page, read the page as a table of numbered
targets, then name one operation on one target. The model never writes a selector, a coordinate, or
a script, and the server never retries a change by itself.
It is built on dsh-browser-use: the same
Python engine, the same sidecar protocol, the same page-as-indexed-action-space idea — with the
DeepSeek Harness plugin host replaced by an MCP host. So there is no browser logic of its own: this
package is the adapter that lets any MCP client drive the Jev Ultrafast
engine. The engine and browser-harness ship as a
pinned wheel plus hash-pinned requirements, and are installed into a managed Python environment on
first use. No engine clone, no manual Python setup.
What it is
The browser tools expose one loop:
- Read.
browser_opendownloads the page and returns its action space as text: one line per reachable element, the operations that element supports, and the exact target indices those operations accept. The result carries an observation number. - Choose. The agent picks one operation and one target from that observation:
browser_act({ observation, operation, target }). - Guard. The server sends the operation together with the fingerprint of the observation it
came from. The engine re-reads the page first. If the page changed, nothing is executed and
the result starts with
Nothing was executed:followed by the fresh table. That is a refusal, not an error: the agent chooses again from something that is true. - Verify. If the page matched, the one operation runs and the server returns the new observation. The next step refers to the new number. A tool result is never proof of an outcome; the next observation is.
Here is a real table, produced by the bundled test fixture (the visible-text line is clipped):
Page file:///…/test/fixtures/fixture.html — 🐴 Forma · Find a place to slow down
Observation 1. Targets below are valid only for this observation.
Visible text: forma. Find a stay Reading room PLACES WITH CHARACTER THE SLOWER WAY TO STAY …
[1] link forma. — CLICK
[2] link Find a stay — CLICK
[3] link Reading room — CLICK
[4] searchbox Destination — TYPE_TEXT, CLICK
[5] button Find stays — CLICK
[6] combobox Stay category · "All stays" — SELECT
6:1 → Stay category → Design
6:2 → Stay category → Nature
6:3 → Stay category → Coastal
[7] checkbox Free cancellation · "on" (not checked) — CLICK
[8] button View Casa Flora — CLICK
[9] button View The Glasshouse — CLICK
[10] button View Serra Lodge — CLICK
Without a target: SCROLL_DOWN, WAIT
A step against it is one object, not code:
{ "observation": 1, "operation": "CLICK", "target": "5" } // button Find stays
{ "observation": 1, "operation": "TYPE_TEXT", "target": "4", "text": "Lisbon" }
{ "observation": 1, "operation": "SELECT", "target": "6:1" } // Stay category → Design
If the page moved between the read and the click, the answer is a refusal that hands back the current observation:
Nothing was executed: The page changed since that observation. Choose from the new one.
Page … — …
Observation 3. Targets below are valid only for this observation.
The engine reads a page over a single CDP connection that this server's session owns. Every tool —
browser_eval included — uses that one connection. Starting a second client yourself (a raw
websocket, a puppeteer/playwright script, another browser-harness daemon) is how Chrome 144+'s
per-connection "Allow remote debugging?" prompt turns into a stack of prompts, and how two loops end
up driving one page.
How it differs from selector-based browser MCPs
| Selector-based browser MCP | browser-use-mcp |
|
|---|---|---|
| What the model sends | A selector, a coordinate, or code | One operation on an index from the table it just read |
| What happens when the page moved | The selector may miss, or hit the wrong element | The action is refused, and the fresh table is returned |
| Retries | Up to the client or the model | Never — a refusal is the retry point, not an automatic replay |
| What the model knows is possible | It has to guess | Each line lists that element's targets and operations, dropdown options included |
| CDP connections | Depends on the implementation | Exactly one per browser session, shared by every tool |
| Token cost per step | Often a screenshot or the whole DOM | Structured text: visible text plus the element table |
browser_eval is the one deliberate exception: it evaluates a single JavaScript expression over
the same connection, returns a value as data, and produces no observation — so the next guarded
action still refers to the last table. It exists so a model that needs a programmatic read does not
start a second CDP client. --no-eval withdraws it.
Quick start
Requires Node.js ^22.19.0 || >=24.0.0, Chrome or Chromium, and a way to get Python 3.12+:
either python3.12–python3.14, python3 (3.12 or newer) on PATH, or uv,
which can fetch the Python itself.
Which browser it drives. The default mode is attach: the server drives the Chrome you already
use, so its logins and tabs are the ones the agent sees. Enable remote debugging once in that Chrome
(chrome://inspect/#remote-debugging → allow); on macOS the application that runs the server also
needs Full Disk Access (System Settings → Privacy & Security → Full Disk Access). Prefer an
isolated browser? Pass --mode launch and the server starts its own Chrome with a profile under the
state root instead.
The package is not published to npm yet — run it from a checkout:
cd /Users/ricardo.chen/PersonalGit/browser-use-mcp
npm install
npm link in the checkout additionally puts a browser-use-mcp command on your PATH for terminal
use; GUI clients are more reliable with the absolute path shown below.
Then check the machine (on a fresh machine the first browser call builds the environment;
install does it eagerly). Its output looks like this, with machine-specific paths shortened:
The example below asks for a dedicated browser (--mode launch); with the default attach mode the
Connection: line names the Chrome you browse with instead, and Status: ready requires that
browser to answer its debugging endpoint.
node /Users/ricardo.chen/PersonalGit/browser-use-mcp/bin/browser-use-mcp.mjs doctor --mode launch
browser-use-mcp v0.1.0 — self-check (Node v22.23.2, darwin-arm64)
Transport: stdio (MCP over stdin/stdout)
Connection: launch Chrome/Chromium, profile "default" at /Users/you/.browser-use-mcp/profiles/default
State : /Users/you/.browser-use-mcp (managed Python environment)
Timeouts: 180s per browser request
Logging : info (stderr)
Jev : on, typesafe decider: default endpoint (model default), key unset; …
Tools : browser_open, browser_page, browser_act, browser_eval, browser_screenshot, browser_console, browser_goal, browser_close, browser_doctor
Session : idle (this transport has no browser session yet)
Engine : /Users/you/.browser-use-mcp/env/bin/python (Python 3.12.14, chosen by managed environment)
jev_ultrafast 0.1.1 at …/env/lib/python3.12/site-packages/jev_ultrafast
browser-harness 0.1.13
Install : none in this process; the engine is already importable
Sidecar : /…/browser-use-mcp/sidecar/bridge.py
Browser : /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
Goal mode: enabled: browser_goal and browser_act intents spend model quota; …
Status : ready
Status: ready means the browser tools work. Jev … NOT READY is expected until a policy key is
configured: only browser_goal and browser_act's intent need one; everything else works.
To build the environment ahead of time instead of during the first browser call:
node /Users/ricardo.chen/PersonalGit/browser-use-mcp/bin/browser-use-mcp.mjs install
That creates <state root>/env (default ~/.browser-use-mcp/env) with the bundled engine wheel and
browser-harness, and records what it built in <state root>/install.json. It is safe to re-run.
A browser tool that runs before the install finished waits for it instead of failing.
Put it in front of your agent
Every absolute path below is this checkout's — substitute where you cloned it.
Claude Code:
# Use the Chrome you already browse with (the default attach mode):
claude mcp add --scope user browser-use \
-- node /Users/ricardo.chen/PersonalGit/browser-use-mcp/bin/browser-use-mcp.mjs
# Or give the agent a dedicated, unseen browser instead:
claude mcp add --scope user browser-use \
--env BROWSER_USE_MCP_MODE=launch --env BROWSER_USE_MCP_HEADLESS=1 \
-- node /Users/ricardo.chen/PersonalGit/browser-use-mcp/bin/browser-use-mcp.mjs
Generic mcpServers JSON (Claude Desktop, Cursor, and most hosts accept this shape):
{
"mcpServers": {
"browser-use": {
"command": "node",
"args": ["/Users/ricardo.chen/PersonalGit/browser-use-mcp/bin/browser-use-mcp.mjs"],
"env": { "BROWSER_USE_MCP_HEADLESS": "1" }
}
}
}
Copy-pasteable snippets for Claude Code, Claude Desktop, Cursor, Codex, DSH and Streamable HTTP — including the absolute-path substitution rule — are in docs/clients.md.
Then ask the agent to browser_open a URL. A first call on a fresh machine may take a minute or two
while the environment is installed; later calls start at once.
The nine tools
| Tool | Parameters | What it does |
|---|---|---|
browser_open |
url (required) |
Start (or reuse) the session's browser, open the page, return the first action-space table and observation number. |
browser_page |
— | Re-read the current page under a new observation number. Use it when the page changed on its own, or after a refusal. |
browser_act |
observation (required), operation, target, text, intent |
Run one operation on one target from that observation: CLICK, TYPE_TEXT, SELECT, SCROLL_UP, SCROLL_DOWN, WAIT. A stale page is refused with the fresh table. With Jev on, an intent (and no operation) delegates that one decision to the policy. |
browser_eval |
expression (required), timeout_ms |
Evaluate one JavaScript expression over the session's CDP connection; returns a value as data and produces no observation. Hidden by --no-eval. |
browser_screenshot |
— | The visible viewport as an MCP image (JPEG). Target boxes are not drawn into it. Hidden by --no-screenshots. |
browser_console |
— | Console messages, uncaught exceptions, failed requests and 4xx/5xx responses since the last read. Each read drains the buffer. |
browser_goal |
url, goal |
Hand one goal to the policy in its own tab; it chooses every operation and target itself. Spends model quota. Hidden by --no-jev. |
browser_close |
— | Close the session's tab. In launch mode the browser stays for the next browser_open; in attach mode the attached browser is never closed. |
browser_doctor |
install (optional bool) |
Self-check: interpreter, engine, browser, attach target, config, live session, and a fix per problem. install: true builds a missing environment first. |
A missing policy key, an unsupported target, missing TYPE_TEXT text and a stale page are
refusals in normal tool results. A broken browser or environment is a failed call (isError).
An agent should report a failure, not retry it blindly.
Transports and concurrency
| Transport | What one agent gets | What two agents get |
|---|---|---|
| stdio (default) | One server process, one browser session | Two processes; in attach mode both drive the same browser in their own tabs, in launch mode two browsers |
| Streamable HTTP | One MCP session, one browser session | One long-running server; attach mode shares the running browser, launch mode gives each session its own tab of a dedicated browser |
Start an HTTP endpoint when several agents should share one installed server:
node /Users/ricardo.chen/PersonalGit/browser-use-mcp/bin/browser-use-mcp.mjs \
--transport http --host 127.0.0.1 --port 7331 --path /mcp
The endpoint is http://127.0.0.1:7331/mcp by default. Each MCP initialize session gets its own
browser session; in the default attach mode all of them drive the Chrome you already use (see
--attach-sharing above), while --mode launch gives every session its own tab of a dedicated
browser. A session that goes quiet for 15 minutes has its browser closed and its anonymous profile
removed (BROWSER_USE_MCP_HTTP_IDLE_MS overrides the timeout). There is no authentication: the
default bind is loopback, and a non-loopback bind logs a warning at startup. Put it behind a tunnel
or proxy you control if it must leave the machine.
Named profiles (launch mode). In launch mode an anonymous HTTP session gets a throwaway profile that is deleted when the session ends. A client that wants its logins to survive reconnects names its profile on the initialize request:
http://127.0.0.1:7331/mcp?profile=claude (or header X-Browser-Use-Profile: claude)
The named profile lives at <state root>/profiles/<server profile>-<name> and is kept when the
session ends; the next session with the same name reuses it. Different names run at the same time on
different profiles, and two live sessions with the same name are refused by the profile lock rather
than silently sharing one browser. Names are 1–32 characters from [A-Za-z0-9_-].
Two servers configured for the same browser profile are refused rather than silently sharing a
browser: the first holds a lock in the profile directory and the second gets a tool error naming
--profile and the fix. A lock left behind by a crashed process is reclaimed.
Which browser: attach (default) or launch
attach (default) drives the Chrome you already use:
- With no endpoint, the server finds that browser through the
DevToolsActivePortChrome publishes in its profile. Turn remote debugging on in that Chrome first (chrome://inspect/#remote-debugging). On macOS that file is privacy-protected, so the application running this server needs Full Disk Access (System Settings → Privacy & Security → Full Disk Access), then must be restarted. --cdp-endpoint http://127.0.0.1:9333(or aws://…endpoint) drives a specific browser you started with remote debugging on.--attach-sharing shared(default) lets several MCP sessions — and several server processes — drive that browser at once, each in its own tab over its own CDP connection. Chrome 144+ asks "Allow remote debugging?" once per connection, so two agents mean at least two prompts; approve each once and never retry an unanswered one.--attach-sharing exclusiverefuses the second live holder instead, with a fix, which is the safer choice when two agents must never touch the same browser.browser_closestops the session's tab and daemon and never closes the attached browser.
--mode launch starts a dedicated Chrome with its own profile under the state root, so your
everyday browser and its logins stay out of automation. browser_close keeps that browser for the
next browser_open; the server closes it when the connection ends. It is the right mode for a
headless CI machine, or for an agent that should not see your real sessions.
Configuration highlights
Every server setting is a flag with an environment-variable twin (the policy uses
TYPESAFE_*/TEXT_MODEL_*; a couple of extras are environment-only), and the flag wins when both
are present. The exhaustive tables, parsing rules, defaults and exit codes are in
docs/config.md.
| Setting | Flag(s) | Env | Default |
|---|---|---|---|
| Transport | --transport, --host, --port, --path |
BROWSER_USE_MCP_TRANSPORT, BROWSER_USE_MCP_HOST, BROWSER_USE_MCP_PORT, BROWSER_USE_MCP_PATH |
stdio, 127.0.0.1, 7331, /mcp |
| Own or attach | --mode, --cdp-endpoint |
BROWSER_USE_MCP_MODE, BROWSER_USE_MCP_CDP_ENDPOINT |
attach |
| Attach sharing | --attach-sharing |
BROWSER_USE_MCP_ATTACH_SHARING |
shared (attach only) |
| Browser window | --headless |
BROWSER_USE_MCP_HEADLESS |
visible window |
| Profile | --profile, --user-data-dir |
BROWSER_USE_MCP_PROFILE, BROWSER_USE_MCP_USER_DATA_DIR |
default under the state root |
| State root | --home |
BROWSER_USE_MCP_HOME |
~/.browser-use-mcp |
| Interpreter | --python |
BROWSER_USE_MCP_PYTHON |
managed environment |
| Tool surface | --no-eval, --no-screenshots, --no-jev |
BROWSER_USE_MCP_NO_EVAL, …_NO_SCREENSHOTS, …_NO_JEV |
all tools offered |
| Policy | --jev-decider and the --typesafe-* / --text-model-* flags |
JEV_DECIDER, TYPESAFE_*, TEXT_MODEL_* |
typesafe decider |
| Request budget | --request-timeout-ms |
BROWSER_USE_MCP_REQUEST_TIMEOUT_MS |
180000 ms |
| Logging | --log-level |
BROWSER_USE_MCP_LOG_LEVEL |
info, always stderr |
State root layout:
~/.browser-use-mcp/
├── env/ managed Python virtualenv (engine + browser-harness)
├── install.json what was installed, with which strategy and Python
├── profiles/<name>/ launched Chrome profiles (logins, tabs); named HTTP profiles too
└── attachments/<hash>.lock exclusive attach-mode claims, one per DevTools endpoint
The Jev policy
browser_goal runs a whole decision loop inside the engine; browser_act with an intent (and no
operation) delegates exactly one decision to the same policy. Both spend model quota. Who answers
is configuration:
--jev-decider typesafe(default): Jev at the TypeSafe endpoint. NeedsTYPESAFE_API_KEY; optionalTYPESAFE_BASE_URL,TYPESAFE_MODEL,TYPESAFE_FALLBACK_URL.--jev-decider chat: the engine serves its System One protocol on loopback and a chat model answers it. NeedsTEXT_MODEL_BASE_URLandTEXT_MODEL(plusTEXT_MODEL_API_KEYif the endpoint needs one).
TEXT_MODEL_* is also what writes a TYPE_TEXT value when the agent gives an intent but no text.
Without either setup the rest of the tool surface is unchanged; the policy tools refuse with one
sentence naming the variable to set. --no-jev withdraws browser_goal and the intent parameter
entirely.
Put credentials in the client's env block (or the flags), never in args:
"env": {
"TYPESAFE_API_KEY": "…",
"TEXT_MODEL_BASE_URL": "https://your-endpoint/v1",
"TEXT_MODEL": "your-model",
"TEXT_MODEL_API_KEY": "…"
}
The server passes those variables only with the request that spends quota, never leaves them in the
sidecar's environment, and the doctor prints set/unset instead of a value.
Troubleshooting
Start with browser_doctor. The tool and the CLI run the same self-check; each problem line
carries the command that fixes it.
node /Users/ricardo.chen/PersonalGit/browser-use-mcp/bin/browser-use-mcp.mjs doctor
- A profile is already in use. The message names the profile and
--profile: stop the other server, or give this one its own--profile <name>. Two servers driving one browser profile is the one configuration the server refuses on purpose. - Attach mode cannot find the browser. This is the default mode: open
chrome://inspect/#remote-debuggingin the Chrome you use and allow remote debugging, or start the server with--mode launchfor a dedicated browser. The doctor reportsattach mode drives …with the reason. Start that browser with remote debugging on (chrome://inspect/#remote-debugging), correct--cdp-endpoint, or use--mode launch. On macOS, allowing this app Full Disk Access is what lets it read your Chrome's profile. - The engine is not installed yet. The first use installs it into the state root; run
node bin/browser-use-mcp.mjs installfrom the checkout to do it eagerly, orbrowser_doctorwithinstall: truefrom the agent. Behind a proxy, setHTTPS_PROXYfor the process that runs the server. Jev … NOT READY. SetTYPESAFE_API_KEY(or use--jev-decider chatwithTEXT_MODEL_*). The browser tools do not need it.- No tools in the client. The client's
env/argsdid not reach the server, or it was not restarted. Run the same command in a terminal and read stderr; a configuration error exits with code2and names the field. - Chrome asks "Allow remote debugging?". Something started a second CDP client. Approve that one
prompt once, never retry, and use
browser_evalinstead of another driver. browser_eval('document.body')returns{}. A DOM node cannot cross the CDP boundary by value. Return something serialisable instead, for exampledocument.body.innerTextorJSON.stringify(document.body.getAttribute('class')).
Security notes
- Page content is untrusted data. Text the table quotes is something to read, never an instruction to follow. The server never executes anything a page returns on the agent's behalf.
browser_evalis code execution by design. It runs one expression in the page, over the session's own connection, with no user gesture. Withdraw it with--no-evalfor a surface where model output can never become code; the rest of the tools still work.- The HTTP transport has no authentication. Bind it to loopback (the default). Any non-loopback bind is logged as a warning; only expose it on a trusted network or behind a tunnel you control.
- Keys stay out of logs and tools. The doctor prints only
set/unset; credentials are handed to the sidecar per model-spending request and are not inherited by unrelated calls. - The default mode drives your own Chrome, logins included — that is what attach mode is for.
Use
--mode launchwhen an agent should work in an isolated browser whose profile lives under the state root instead. - One connection, one driver. Do not start another CDP client against the same browser; Chrome approves each connection separately and two drivers race each other.
License and attribution
This project is built on dsh-browser-use
(MIT): sidecar/bridge.py is vendored from it, and the action-space rendering, the freshness rule,
the one-connection rule and the direct-mode tool descriptions follow it. The browser engine is
jev-ultrafast (MIT), bundled as a pinned wheel;
browser access is browser-harness (MIT). The
design and rationale are in docs/architecture.md.
Development
npm test # fast unit tests (node --test)
npm run test:e2e # real headless Chrome through the real MCP protocol
# (stdio, HTTP, attach mode, Jev intent against a loopback stub,
# and a hand-written JSON-RPC client)
npm run doctor # the CLI self-check
The default suites spend no model calls and need no credentials; the e2e tests install the engine into a test state root on their first run. Release history is in CHANGELOG.md.