@ricardochen1996/browser-use-mcp 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,457 @@
1
- # Temporary Holding Version
1
+ # @ricardochen1996/browser-use-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/ricardochen1996/browser-use-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/ricardochen1996/browser-use-mcp/actions/workflows/ci.yml)
4
+
5
+ **English** · [中文](README.zh-CN.md)
6
+
7
+ > An MCP server that gives **any agent** a browser it can *see and click*: a page is read as an
8
+ > **indexed action-space table**, and every step is one guarded operation on a target that came
9
+ > from that table.
10
+
11
+ `browser-use-mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server. Claude
12
+ Code, Claude Desktop, Cursor, Codex, a DeepSeek Harness conversation, or any other MCP client
13
+ connects to it, and its agent gets a browser: open a page, read the page as a table of numbered
14
+ targets, then name one operation on one target. The model never writes a selector, a coordinate, or
15
+ a script, and the server never retries a change by itself.
16
+
17
+ It is built on [`dsh-browser-use`](https://github.com/ricardochen1996/dsh-browser-use): the same
18
+ Python engine, the same sidecar protocol, the same page-as-indexed-action-space idea — with the
19
+ DeepSeek Harness plugin host replaced by an MCP host. So there is no browser logic of its own: this
20
+ package is the adapter that lets any MCP client drive the [Jev Ultrafast](https://github.com/ricardochen1996/jev-ultrafast)
21
+ engine. The engine and [`browser-harness`](https://github.com/browser-use/browser-harness) ship as a
22
+ pinned wheel plus hash-pinned requirements, and are installed into a managed Python environment on
23
+ first use. No engine clone, no manual Python setup.
24
+
25
+ ---
26
+
27
+ ## What it is
28
+
29
+ The browser tools expose one loop:
30
+
31
+ 1. **Read.** `browser_open` downloads the page and returns its **action space** as text: one line
32
+ per reachable element, the operations that element supports, and the exact target indices those
33
+ operations accept. The result carries an **observation number**.
34
+ 2. **Choose.** The agent picks one operation and one target *from that observation*:
35
+ `browser_act({ observation, operation, target })`.
36
+ 3. **Guard.** The server sends the operation together with the fingerprint of the observation it
37
+ came from. The engine re-reads the page first. If the page changed, **nothing is executed** and
38
+ the result starts with `Nothing was executed:` followed by the fresh table. That is a refusal,
39
+ not an error: the agent chooses again from something that is true.
40
+ 4. **Verify.** If the page matched, the one operation runs and the server returns the new
41
+ observation. The next step refers to the new number. A tool result is never proof of an
42
+ outcome; the next observation is.
43
+
44
+ Here is a real table, produced by the bundled test fixture (the visible-text line is clipped):
45
+
46
+ ```text
47
+ Page file:///…/test/fixtures/fixture.html — 🐴 Forma · Find a place to slow down
48
+ Observation 1. Targets below are valid only for this observation.
49
+ Visible text: forma. Find a stay Reading room PLACES WITH CHARACTER THE SLOWER WAY TO STAY …
50
+ [1] link forma. — CLICK
51
+ [2] link Find a stay — CLICK
52
+ [3] link Reading room — CLICK
53
+ [4] searchbox Destination — TYPE_TEXT, CLICK
54
+ [5] button Find stays — CLICK
55
+ [6] combobox Stay category · "All stays" — SELECT
56
+ 6:1 → Stay category → Design
57
+ 6:2 → Stay category → Nature
58
+ 6:3 → Stay category → Coastal
59
+ [7] checkbox Free cancellation · "on" (not checked) — CLICK
60
+ [8] button View Casa Flora — CLICK
61
+ [9] button View The Glasshouse — CLICK
62
+ [10] button View Serra Lodge — CLICK
63
+ Without a target: SCROLL_DOWN, WAIT
64
+ ```
65
+
66
+ A step against it is one object, not code:
67
+
68
+ ```jsonc
69
+ { "observation": 1, "operation": "CLICK", "target": "5" } // button Find stays
70
+ { "observation": 1, "operation": "TYPE_TEXT", "target": "4", "text": "Lisbon" }
71
+ { "observation": 1, "operation": "SELECT", "target": "6:1" } // Stay category → Design
72
+ ```
73
+
74
+ If the page moved between the read and the click, the answer is a refusal that hands back the
75
+ current observation:
76
+
77
+ ```text
78
+ Nothing was executed: The page changed since that observation. Choose from the new one.
79
+
80
+ Page … — …
81
+ Observation 3. Targets below are valid only for this observation.
82
+ ```
83
+
84
+ The engine reads a page over a single CDP connection that this server's session owns. Every tool —
85
+ `browser_eval` included — uses that one connection. Starting a second client yourself (a raw
86
+ websocket, a puppeteer/playwright script, another `browser-harness` daemon) is how Chrome 144+'s
87
+ per-connection "Allow remote debugging?" prompt turns into a stack of prompts, and how two loops end
88
+ up driving one page.
89
+
90
+ ## How it differs from selector-based browser MCPs
91
+
92
+ | | Selector-based browser MCP | `browser-use-mcp` |
93
+ | --- | --- | --- |
94
+ | What the model sends | A selector, a coordinate, or code | One operation on an index from the table it just read |
95
+ | 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 |
96
+ | Retries | Up to the client or the model | **Never** — a refusal is the retry point, not an automatic replay |
97
+ | What the model knows is possible | It has to guess | Each line lists that element's targets and operations, dropdown options included |
98
+ | CDP connections | Depends on the implementation | Exactly one per browser session, shared by every tool |
99
+ | Token cost per step | Often a screenshot or the whole DOM | Structured text: visible text plus the element table |
100
+
101
+ `browser_eval` is the one deliberate exception: it evaluates a single JavaScript expression over
102
+ the same connection, returns a value as data, and produces **no observation** — so the next guarded
103
+ action still refers to the last table. It exists so a model that needs a programmatic read does not
104
+ start a second CDP client. `--no-eval` withdraws it.
105
+
106
+ ---
107
+
108
+ ## Quick start
109
+
110
+ **Requires** Node.js `^22.19.0 || >=24.0.0`, Chrome or Chromium, and a way to get Python 3.12+:
111
+ either `python3.12`–`python3.14`, `python3` (3.12 or newer) on `PATH`, or [uv](https://docs.astral.sh/uv/),
112
+ which can fetch the Python itself.
113
+
114
+ **Which browser it drives.** The default mode is **attach**: the server drives the Chrome you already
115
+ use, so its logins and tabs are the ones the agent sees. Enable remote debugging once in that Chrome
116
+ (`chrome://inspect/#remote-debugging` → allow); on macOS the application that runs the server also
117
+ needs **Full Disk Access** (System Settings → Privacy & Security → Full Disk Access). Prefer an
118
+ isolated browser? Pass `--mode launch` and the server starts its own Chrome with a profile under the
119
+ state root instead.
120
+
121
+ The package is **not published to npm yet** — run it from a checkout:
122
+
123
+ ```bash
124
+ cd /Users/ricardo.chen/PersonalGit/browser-use-mcp
125
+ npm install
126
+ ```
127
+
128
+ `npm link` in the checkout additionally puts a `browser-use-mcp` command on your `PATH` for terminal
129
+ use; GUI clients are more reliable with the absolute path shown below.
130
+
131
+ Then check the machine (on a fresh machine the first browser call builds the environment;
132
+ `install` does it eagerly). Its output looks like this, with machine-specific paths shortened:
133
+
134
+ The example below asks for a dedicated browser (`--mode launch`); with the default attach mode the
135
+ `Connection:` line names the Chrome you browse with instead, and `Status: ready` requires that
136
+ browser to answer its debugging endpoint.
137
+
138
+ ```bash
139
+ node /Users/ricardo.chen/PersonalGit/browser-use-mcp/bin/browser-use-mcp.mjs doctor --mode launch
140
+ ```
141
+
142
+ ```text
143
+ browser-use-mcp v0.1.0 — self-check (Node v22.23.2, darwin-arm64)
144
+
145
+ Transport: stdio (MCP over stdin/stdout)
146
+ Connection: launch Chrome/Chromium, profile "default" at /Users/you/.browser-use-mcp/profiles/default
147
+ State : /Users/you/.browser-use-mcp (managed Python environment)
148
+ Timeouts: 180s per browser request
149
+ Logging : info (stderr)
150
+ Jev : on, typesafe decider: default endpoint (model default), key unset; …
151
+ Tools : browser_open, browser_page, browser_act, browser_eval, browser_screenshot, browser_console, browser_goal, browser_close, browser_doctor
152
+ Session : idle (this transport has no browser session yet)
153
+
154
+ Engine : /Users/you/.browser-use-mcp/env/bin/python (Python 3.12.14, chosen by managed environment)
155
+ jev_ultrafast 0.1.1 at …/env/lib/python3.12/site-packages/jev_ultrafast
156
+ browser-harness 0.1.13
157
+ Install : none in this process; the engine is already importable
158
+ Sidecar : /…/browser-use-mcp/sidecar/bridge.py
159
+ Browser : /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
160
+ Goal mode: enabled: browser_goal and browser_act intents spend model quota; …
161
+ Status : ready
162
+ ```
163
+
164
+ `Status: ready` means the browser tools work. `Jev … NOT READY` is expected until a policy key is
165
+ configured: only `browser_goal` and `browser_act`'s `intent` need one; everything else works.
166
+
167
+ To build the environment ahead of time instead of during the first browser call:
168
+
169
+ ```bash
170
+ node /Users/ricardo.chen/PersonalGit/browser-use-mcp/bin/browser-use-mcp.mjs install
171
+ ```
172
+
173
+ That creates `<state root>/env` (default `~/.browser-use-mcp/env`) with the bundled engine wheel and
174
+ `browser-harness`, and records what it built in `<state root>/install.json`. It is safe to re-run.
175
+ A browser tool that runs before the install finished waits for it instead of failing.
176
+
177
+ ### Put it in front of your agent
178
+
179
+ Every absolute path below is this checkout's — substitute where you cloned it.
180
+
181
+ Claude Code:
182
+
183
+ ```bash
184
+ # Use the Chrome you already browse with (the default attach mode):
185
+ claude mcp add --scope user browser-use \
186
+ -- node /Users/ricardo.chen/PersonalGit/browser-use-mcp/bin/browser-use-mcp.mjs
187
+
188
+ # Or give the agent a dedicated, unseen browser instead:
189
+ claude mcp add --scope user browser-use \
190
+ --env BROWSER_USE_MCP_MODE=launch --env BROWSER_USE_MCP_HEADLESS=1 \
191
+ -- node /Users/ricardo.chen/PersonalGit/browser-use-mcp/bin/browser-use-mcp.mjs
192
+ ```
193
+
194
+ Generic `mcpServers` JSON (Claude Desktop, Cursor, and most hosts accept this shape):
195
+
196
+ ```json
197
+ {
198
+ "mcpServers": {
199
+ "browser-use": {
200
+ "command": "node",
201
+ "args": ["/Users/ricardo.chen/PersonalGit/browser-use-mcp/bin/browser-use-mcp.mjs"],
202
+ "env": { "BROWSER_USE_MCP_HEADLESS": "1" }
203
+ }
204
+ }
205
+ }
206
+ ```
207
+
208
+ Copy-pasteable snippets for Claude Code, Claude Desktop, Cursor, Codex, DSH and Streamable HTTP —
209
+ including the absolute-path substitution rule — are in **[docs/clients.md](docs/clients.md)**.
210
+
211
+ Then ask the agent to `browser_open` a URL. A first call on a fresh machine may take a minute or two
212
+ while the environment is installed; later calls start at once.
213
+
214
+ ---
215
+
216
+ ## The nine tools
217
+
218
+ | Tool | Parameters | What it does |
219
+ | --- | --- | --- |
220
+ | `browser_open` | `url` (required) | Start (or reuse) the session's browser, open the page, return the first action-space table and observation number. |
221
+ | `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. |
222
+ | `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. |
223
+ | `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`. |
224
+ | `browser_screenshot` | — | The visible viewport as an MCP image (JPEG). Target boxes are not drawn into it. Hidden by `--no-screenshots`. |
225
+ | `browser_console` | — | Console messages, uncaught exceptions, failed requests and `4xx`/`5xx` responses since the last read. Each read drains the buffer. |
226
+ | `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`. |
227
+ | `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. |
228
+ | `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. |
229
+
230
+ A missing policy key, an unsupported target, missing `TYPE_TEXT` text and a stale page are
231
+ **refusals** in normal tool results. A broken browser or environment is a failed call (`isError`).
232
+ An agent should report a failure, not retry it blindly.
233
+
234
+ ---
235
+
236
+ ## Transports and concurrency
237
+
238
+ | Transport | What one agent gets | What two agents get |
239
+ | --- | --- | --- |
240
+ | **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 |
241
+ | **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 |
242
+
243
+ Start an HTTP endpoint when several agents should share one installed server:
244
+
245
+ ```bash
246
+ node /Users/ricardo.chen/PersonalGit/browser-use-mcp/bin/browser-use-mcp.mjs \
247
+ --transport http --host 127.0.0.1 --port 7331 --path /mcp
248
+ ```
249
+
250
+ The endpoint is `http://127.0.0.1:7331/mcp` by default. Each MCP `initialize` session gets its own
251
+ browser session; in the default attach mode all of them drive the Chrome you already use (see
252
+ `--attach-sharing` above), while `--mode launch` gives every session its own tab of a dedicated
253
+ browser. A session that goes quiet for 15 minutes has its browser closed and its anonymous profile
254
+ removed (`BROWSER_USE_MCP_HTTP_IDLE_MS` overrides the timeout). There is no authentication: the
255
+ default bind is loopback, and a non-loopback bind logs a warning at startup. Put it behind a tunnel
256
+ or proxy you control if it must leave the machine.
257
+
258
+ **Named profiles (launch mode).** In launch mode an anonymous HTTP session gets a throwaway profile
259
+ that is deleted when the session ends. A client that wants its logins to survive reconnects names its
260
+ profile on the initialize request:
261
+
262
+ ```text
263
+ http://127.0.0.1:7331/mcp?profile=claude (or header X-Browser-Use-Profile: claude)
264
+ ```
265
+
266
+ The named profile lives at `<state root>/profiles/<server profile>-<name>` and is kept when the
267
+ session ends; the next session with the same name reuses it. Different names run at the same time on
268
+ different profiles, and two live sessions with the same name are refused by the profile lock rather
269
+ than silently sharing one browser. Names are 1–32 characters from `[A-Za-z0-9_-]`.
270
+
271
+ Two servers configured for the **same browser profile** are refused rather than silently sharing a
272
+ browser: the first holds a lock in the profile directory and the second gets a tool error naming
273
+ `--profile` and the fix. A lock left behind by a crashed process is reclaimed.
274
+
275
+ ### Which browser: attach (default) or launch
276
+
277
+ **`attach` (default)** drives the Chrome you already use:
278
+
279
+ - With no endpoint, the server finds that browser through the `DevToolsActivePort` Chrome publishes
280
+ in its profile. Turn remote debugging on in that Chrome first
281
+ (`chrome://inspect/#remote-debugging`). On macOS that file is privacy-protected, so the
282
+ application running this server needs **Full Disk Access** (System Settings → Privacy & Security →
283
+ Full Disk Access), then must be restarted.
284
+ - **`--cdp-endpoint http://127.0.0.1:9333`** (or a `ws://…` endpoint) drives a specific browser you
285
+ started with remote debugging on.
286
+ - `--attach-sharing shared` (default) lets several MCP sessions — and several server processes —
287
+ drive that browser at once, each in its own tab over its own CDP connection. Chrome 144+ asks
288
+ "Allow remote debugging?" once per connection, so two agents mean at least two prompts; approve
289
+ each once and never retry an unanswered one. `--attach-sharing exclusive` refuses the second live
290
+ holder instead, with a fix, which is the safer choice when two agents must never touch the same
291
+ browser.
292
+ - `browser_close` stops the session's tab and daemon and never closes the attached browser.
293
+
294
+ **`--mode launch`** starts a dedicated Chrome with its own profile under the state root, so your
295
+ everyday browser and its logins stay out of automation. `browser_close` keeps that browser for the
296
+ next `browser_open`; the server closes it when the connection ends. It is the right mode for a
297
+ headless CI machine, or for an agent that should not see your real sessions.
298
+
299
+ ---
300
+
301
+ ## Configuration highlights
302
+
303
+ Every server setting is a flag with an environment-variable twin (the policy uses
304
+ `TYPESAFE_*`/`TEXT_MODEL_*`; a couple of extras are environment-only), and the flag wins when both
305
+ are present. The exhaustive tables, parsing rules, defaults and exit codes are in
306
+ **[docs/config.md](docs/config.md)**.
307
+
308
+ | Setting | Flag(s) | Env | Default |
309
+ | --- | --- | --- | --- |
310
+ | 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` |
311
+ | Own or attach | `--mode`, `--cdp-endpoint` | `BROWSER_USE_MCP_MODE`, `BROWSER_USE_MCP_CDP_ENDPOINT` | `attach` |
312
+ | Attach sharing | `--attach-sharing` | `BROWSER_USE_MCP_ATTACH_SHARING` | `shared` (attach only) |
313
+ | Browser window | `--headless` | `BROWSER_USE_MCP_HEADLESS` | visible window |
314
+ | Profile | `--profile`, `--user-data-dir` | `BROWSER_USE_MCP_PROFILE`, `BROWSER_USE_MCP_USER_DATA_DIR` | `default` under the state root |
315
+ | State root | `--home` | `BROWSER_USE_MCP_HOME` | `~/.browser-use-mcp` |
316
+ | Interpreter | `--python` | `BROWSER_USE_MCP_PYTHON` | managed environment |
317
+ | Tool surface | `--no-eval`, `--no-screenshots`, `--no-jev` | `BROWSER_USE_MCP_NO_EVAL`, `…_NO_SCREENSHOTS`, `…_NO_JEV` | all tools offered |
318
+ | Policy | `--jev-decider` and the `--typesafe-*` / `--text-model-*` flags | `JEV_DECIDER`, `TYPESAFE_*`, `TEXT_MODEL_*` | `typesafe` decider |
319
+ | Request budget | `--request-timeout-ms` | `BROWSER_USE_MCP_REQUEST_TIMEOUT_MS` | `180000` ms |
320
+ | Logging | `--log-level` | `BROWSER_USE_MCP_LOG_LEVEL` | `info`, always stderr |
321
+
322
+ State root layout:
323
+
324
+ ```text
325
+ ~/.browser-use-mcp/
326
+ ├── env/ managed Python virtualenv (engine + browser-harness)
327
+ ├── install.json what was installed, with which strategy and Python
328
+ ├── profiles/<name>/ launched Chrome profiles (logins, tabs); named HTTP profiles too
329
+ └── attachments/<hash>.lock exclusive attach-mode claims, one per DevTools endpoint
330
+ ```
331
+
332
+ ### The Jev policy
333
+
334
+ `browser_goal` runs a whole decision loop inside the engine; `browser_act` with an `intent` (and no
335
+ operation) delegates exactly one decision to the same policy. Both spend model quota. Who answers
336
+ is configuration:
337
+
338
+ - **`--jev-decider typesafe`** (default): Jev at the TypeSafe endpoint. Needs `TYPESAFE_API_KEY`;
339
+ optional `TYPESAFE_BASE_URL`, `TYPESAFE_MODEL`, `TYPESAFE_FALLBACK_URL`.
340
+ - **`--jev-decider chat`**: the engine serves its System One protocol on loopback and a chat model
341
+ answers it. Needs `TEXT_MODEL_BASE_URL` and `TEXT_MODEL` (plus `TEXT_MODEL_API_KEY` if the
342
+ endpoint needs one).
343
+
344
+ `TEXT_MODEL_*` is also what writes a `TYPE_TEXT` value when the agent gives an intent but no text.
345
+ Without either setup the rest of the tool surface is unchanged; the policy tools refuse with one
346
+ sentence naming the variable to set. `--no-jev` withdraws `browser_goal` and the `intent` parameter
347
+ entirely.
348
+
349
+ Put credentials in the client's `env` block (or the flags), never in `args`:
350
+
351
+ ```json
352
+ "env": {
353
+ "TYPESAFE_API_KEY": "…",
354
+ "TEXT_MODEL_BASE_URL": "https://your-endpoint/v1",
355
+ "TEXT_MODEL": "your-model",
356
+ "TEXT_MODEL_API_KEY": "…"
357
+ }
358
+ ```
359
+
360
+ The server passes those variables only with the request that spends quota, never leaves them in the
361
+ sidecar's environment, and the doctor prints `set`/`unset` instead of a value.
362
+
363
+ ---
364
+
365
+ ## Troubleshooting
366
+
367
+ **Start with `browser_doctor`.** The tool and the CLI run the same self-check; each problem line
368
+ carries the command that fixes it.
369
+
370
+ ```bash
371
+ node /Users/ricardo.chen/PersonalGit/browser-use-mcp/bin/browser-use-mcp.mjs doctor
372
+ ```
373
+
374
+ - **A profile is already in use.** The message names the profile and `--profile`: stop the other
375
+ server, or give this one its own `--profile <name>`. Two servers driving one browser profile is
376
+ the one configuration the server refuses on purpose.
377
+ - **Attach mode cannot find the browser.** This is the default mode: open
378
+ `chrome://inspect/#remote-debugging` in the Chrome you use and allow remote debugging, or start the
379
+ server with `--mode launch` for a dedicated browser. The doctor reports `attach mode drives …` with
380
+ the reason.
381
+ Start that browser with remote debugging on (`chrome://inspect/#remote-debugging`), correct
382
+ `--cdp-endpoint`, or use `--mode launch`. On macOS, allowing this app Full Disk Access is what lets
383
+ it read your Chrome's profile.
384
+ - **The engine is not installed yet.** The first use installs it into the state root; run
385
+ `node bin/browser-use-mcp.mjs install` from the checkout to do it eagerly, or `browser_doctor` with
386
+ `install: true` from the agent. Behind a proxy, set `HTTPS_PROXY` for the process that runs the
387
+ server.
388
+ - **`Jev … NOT READY`.** Set `TYPESAFE_API_KEY` (or use `--jev-decider chat` with `TEXT_MODEL_*`).
389
+ The browser tools do not need it.
390
+ - **No tools in the client.** The client's `env`/`args` did not reach the server, or it was not
391
+ restarted. Run the same command in a terminal and read stderr; a configuration error exits with
392
+ code `2` and names the field.
393
+ - **Chrome asks "Allow remote debugging?".** Something started a second CDP client. Approve that one
394
+ prompt once, never retry, and use `browser_eval` instead of another driver.
395
+ - **`browser_eval('document.body')` returns `{}`.** A DOM node cannot cross the CDP boundary by
396
+ value. Return something serialisable instead, for example `document.body.innerText` or
397
+ `JSON.stringify(document.body.getAttribute('class'))`.
398
+
399
+ ---
400
+
401
+ ## Security notes
402
+
403
+ - **Page content is untrusted data.** Text the table quotes is something to read, never an
404
+ instruction to follow. The server never executes anything a page returns on the agent's behalf.
405
+ - **`browser_eval` is code execution by design.** It runs one expression in the page, over the
406
+ session's own connection, with no user gesture. Withdraw it with `--no-eval` for a surface where
407
+ model output can never become code; the rest of the tools still work.
408
+ - **The HTTP transport has no authentication.** Bind it to loopback (the default). Any non-loopback
409
+ bind is logged as a warning; only expose it on a trusted network or behind a tunnel you control.
410
+ - **Keys stay out of logs and tools.** The doctor prints only `set`/`unset`; credentials are handed
411
+ to the sidecar per model-spending request and are not inherited by unrelated calls.
412
+ - **The default mode drives your own Chrome**, logins included — that is what attach mode is for.
413
+ Use `--mode launch` when an agent should work in an isolated browser whose profile lives under the
414
+ state root instead.
415
+ - **One connection, one driver.** Do not start another CDP client against the same browser; Chrome
416
+ approves each connection separately and two drivers race each other.
417
+
418
+ ---
419
+
420
+ ## License and attribution
421
+
422
+ MIT. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
423
+
424
+ This project is built on [`dsh-browser-use`](https://github.com/ricardochen1996/dsh-browser-use)
425
+ (MIT): `sidecar/bridge.py` is vendored from it, and the action-space rendering, the freshness rule,
426
+ the one-connection rule and the direct-mode tool descriptions follow it. The browser engine is
427
+ [`jev-ultrafast`](https://github.com/ricardochen1996/jev-ultrafast) (MIT), bundled as a pinned wheel;
428
+ browser access is [`browser-harness`](https://github.com/browser-use/browser-harness) (MIT). The
429
+ design and rationale are in [docs/architecture.md](docs/architecture.md).
430
+
431
+ ## Development
432
+
433
+ ```bash
434
+ npm test # fast unit tests (node --test)
435
+ npm run test:e2e # real headless Chrome through the real MCP protocol
436
+ # (stdio, HTTP, attach mode, Jev intent against a loopback stub,
437
+ # and a hand-written JSON-RPC client)
438
+ npm run release:check # manifest + changelog + the exact packed file list
439
+ npm run release:pack # build the tarball, verify it and record its integrity in dist/
440
+ npm run doctor # the CLI self-check
441
+ ```
442
+
443
+ The default suites spend no model calls and need no credentials; the e2e tests install the engine
444
+ into a test state root on their first run. Release history is in [CHANGELOG.md](CHANGELOG.md).
445
+
446
+ **Releases** are tagged `vX.Y.Z` on `main`; the [release workflow](.github/workflows/release.yml)
447
+ validates the annotated tag against `package.json`, reruns CI (unit plus real-browser e2e), publishes
448
+ to npm with provenance, and creates the GitHub Release with the verified tarball attached. It accepts
449
+ either an `NPM_TOKEN` repository secret (an automation or publish-capable granular token) or npm
450
+ **Trusted Publishing**: name this repository and `.github/workflows/release.yml` as the package's
451
+ trusted publisher and the OIDC token of the job is used instead. An already-published version is
452
+ skipped, so a re-run only refreshes the GitHub Release. Bump the version, add its `CHANGELOG.md`
453
+ section, then `git tag -a vX.Y.Z -m "vX.Y.Z"` and push the tag.
454
+
455
+ ---
456
+
457
+ [![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)