jev-reach
# jev-reach
[chrome-devtools-mcp](https://github.com/ChromeDevTools/chrome-devtools-mcp) plus one
tool. `reach` walks the browser to the spot where you want to look, using
[TypeSafe Jev](https://docs.typesafe.ai) to pick each click. Then you make one
devtools call there instead of one call per click.
Everything chrome-devtools-mcp does still works, unchanged, in the same process
and the same Chrome. jev-reach registers one extra tool on top.
## Why
Verifying a frontend bug usually starts with a walk: dismiss the cookie banner,
log in, open Settings, expand the third row. With a plain browser MCP each of
those is a full model turn, and each turn reads a page snapshot into context.
The reasoning only starts once you arrive.
`reach` moves the walk to Jev. Jev is a decision model: it picks one element and
one action from a typed list in about 300 ms and never reads or writes free text.
Your model states the goal once, gets back a status and a step trace, and then
calls `list_console_messages`, `list_network_requests`, `take_snapshot`, or
`take_screenshot` on the same tab. The walk is recorded for those calls, because
it happened on the same page objects the stock collectors listen to.
## Install
Requires Node 20.19+ or 22.12+, Chrome, and a TypeSafe API key from
[console.typesafe.ai](https://console.typesafe.ai/settings/keys).
Replace your chrome-devtools-mcp entry with jev-reach and keep the flags:
```bash
claude mcp remove chrome-devtools
claude mcp add chrome-devtools -e TYPESAFE_API_KEY=your-key -- npx -y jev-reach --isolated
```
Any MCP client:
```json
{
"mcpServers": {
"chrome-devtools": {
"command": "npx",
"args": ["-y", "jev-reach", "--isolated"],
"env": { "TYPESAFE_API_KEY": "your-key" }
}
}
}
```
Every chrome-devtools-mcp flag is accepted and none is required. `--isolated` is
only the example because it gives each agent session its own throwaway Chrome.
The other stock modes work the same way, verified with the same walk:
| mode | flags | what reach walks |
|---|---|---|
| isolated | `--isolated` | a fresh Chrome per session, temp profile |
| persistent | no flag | the stock profile in `~/.cache/chrome-devtools-mcp`, logins kept |
| attach | `--browserUrl http://127.0.0.1:9222` or `--autoConnect` | your own running Chrome |
The stock rule applies: one server per persistent profile at a time. Running a
second session against the same profile fails with the stock "browser is
already running" error, which is what `--isolated` is for.
The key can also live in `~/.config/typesafe/key` with mode 0600.
## Use
```text
reach({
goal: "Sign in with the supplied email and password so the Settings page is showing.",
url: "http://127.0.0.1:3000/login.html",
values: { email: "dev@example.test", password: "..." }
})
```
returns (a real run against the test fixture, headless Chrome launch included):
```json
{
"status": "done",
"page_id": 1,
"url": "http://127.0.0.1:3000/settings.html",
"title": "Settings",
"steps": [
{ "step": 1, "action": "type", "target": "input[email] \"Email\"", "confidence": 0.96, "goal": 0.02, "latency_ms": 1032 },
{ "step": 2, "action": "type", "target": "input[password] \"Password\"", "confidence": 0.95, "goal": 0.02, "latency_ms": 432 },
{ "step": 3, "action": "click", "target": "button \"Sign in\"", "confidence": 0.73, "goal": 0.03, "latency_ms": 421 },
{ "step": 4, "action": "finish", "goal": 0.92, "latency_ms": 2030, "note": "done" }
],
"usage": { "calls": 4, "input_tokens": 3831, "output_tokens": 859, "jev_ms": 3915, "total_ms": 6573 }
}
```
Then:
```text
list_console_messages({ pageId: 1 })
```
The page id matters. chrome-devtools-mcp routes page-scoped tools by id, and the
id is invisible outside the process, so `reach` hands it back.
Console and network history follow the stock rules: buffered per navigation,
newest first. Messages from pages the walk passed through are in the earlier
navigation buckets, and the current page's messages are what the default call
shows.
### Inputs
| name | meaning |
|---|---|
| `goal` | The state to reach, in one sentence. Say what should be visible at the end. |
| `url` | Navigate here first. Omit to start from the current page. |
| `tab_url_prefix` | Act on the open tab whose URL starts with this instead of the selected tab. |
| `values` | Named strings Jev may type. Only the names reach Jev; the values go to the browser. |
| `max_steps` | Step budget, default 15. |
| `allow_irreversible` | Let Jev click through order, pay, send, delete, publish. Default false. |
| `settle_ms` | Network-quiet time after each action, default 500. |
| `timeout_ms` | Wall-clock budget, default 60000. |
### Statuses
| status | meaning |
|---|---|
| `done` | Jev rates the goal reached at 0.85 or above. |
| `likely_done` | The page looks done but Jev is unsure. Verify before moving on. |
| `needs_confirmation` | The next action looks irreversible. See `pending`. Re-run with `allow_irreversible` only if the user wants it. |
| `needs_login` | A sign-in wall and no `values`. Pass credentials. |
| `stuck` | Nothing changed for three actions. See `candidates` for the top elements. |
| `blocked` | Captcha, access denied, or an error page. |
| `max_steps`, `timeout` | Budget spent. |
No page text is ever returned. The follow-up devtools call is where you read the
page.
## How it works
```
reach(goal)
│ warm the stock server: internal list_pages launches Chrome and attaches
│ the console and network collectors, and names the selected tab
├─ loop
│ 1. snapshot: visible, enabled interactive elements in the main frame and
│ open shadow roots, numbered, in-viewport first
│ 2. one parallel Jev call: element, action, value binding, goal reached,
│ irreversible, login wall, blocked
│ 3. act through puppeteer on the same page objects
│ 4. settle on network quiet, diff url + text + inputs
└─ status, page_id, url, title, steps, usage
```
Above 254 candidates the list is split, each chunk shortlisted in parallel, and
the winners plus the in-viewport elements go to the main call.
Native `alert` dialogs are accepted and logged. `confirm` and `prompt` count as
irreversible: they are dismissed and the walk stops with `needs_confirmation`.
## Limits
- Navigation only. Jev never judges whether the bug is fixed.
- Jev never generates text. Anything typed comes from `values`.
- Irreversible detection is a model judgment plus a keyword list. It is a guard,
not a security boundary.
- Page text is sent to TypeSafe for each step. Fine for local dev pages; think
about it for client sites.
- Main frame and open shadow roots. Iframes, file upload, and drag are not
handled yet.
- `reach` has its own mutex and does not share the stock tool mutex. Do not run
it in parallel with other browser tools.
- chrome-devtools-mcp is pinned to an exact version because jev-reach relies on
its internals. Boot fails loudly if the pin is broken.
## Environment
| variable | meaning |
|---|---|
| `TYPESAFE_API_KEY` | The key. Or `~/.config/typesafe/key`, or `JEV_KEY_FILE`. |
| `JEV_MODEL` | Model id, default `jev-latest`. |
| `JEV_TIMEOUT_MS` | Per-request timeout, default 15000. |
| `JEV_REACH_LOG` | `1` prints one stderr line per step. |
## Development
```bash
npm install
npm run build
npm test # offline: helpers and server boot
JEV_REACH_E2E=1 npm run test:e2e # launches headless Chrome; scripted Jev
JEV_REACH_E2E=1 TYPESAFE_API_KEY=... npm run test:e2e # adds a live run
```
## Credits
The describe, decide, act loop with typed statuses is ported from the wave of
Jev browser projects, in particular
[Ying-Kai-Liao/jev-browser](https://github.com/Ying-Kai-Liao/jev-browser) and
[Browser Use's jev-ultrafast](https://github.com/browser-use/jev-ultrafast).
What is new here is running that loop inside chrome-devtools-mcp's own process,
so the devtools surface, the isolation model, and the recorded console and
network stay exactly as they are.
## License
MIT
TDQS
Scored across 30 tools
Most tools have distinct targets, but fill, fill_form, type_text, and press_key overlap significantly in the input/text-entry space. The tool descriptions help, especially fill_form's explicit preference note, but an agent could still easily misselect among them.
Names are consistently lowercase snake_case, but they mix verb-first forms like list_pages and take_screenshot with domain-prefixed forms like performance_start_trace and lighthouse_audit, plus bare verbs like fill, hover, and reach. The naming is readable but not predictable enough to infer related tools consistently.
At 30 tools, the surface is over the heavy threshold and likely larger than needed. The breadth is defensible for a DevTools-style browser automation server, but several tools could be consolidated, such as fill/fill_form and the list_network_requests/get_network_request pairs.
The set covers the full browser automation and debugging lifecycle: navigation, page management, element interaction, forms, uploads, dialogs, console/network inspection, screenshots/snapshots, and performance/memory auditing. There are no obvious dead ends or missing core operations for its apparent purpose.