@predotdev/mcp 2.1.0 → 2.1.2
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 +7 -2
- package/package.json +1 -1
- package/src/setup.mjs +12 -4
- package/src/tools.mjs +20 -6
package/README.md
CHANGED
|
@@ -59,6 +59,10 @@ Then restart your agent and try:
|
|
|
59
59
|
|
|
60
60
|
The command is safe to run again at any time, and running it again updates to the latest version. `npx -y @predotdev/mcp check` shows the state of everything, and `uninstall` removes it from every agent.
|
|
61
61
|
|
|
62
|
+
### Coming from Claude in Chrome?
|
|
63
|
+
|
|
64
|
+
Run the setup command above, then type `/chrome` in Claude Code and turn off **Enabled by default** (or start a session with `claude --no-chrome`). Your Chrome profiles and logins carry over, because this server drives the Chrome you already use. The [switching guide](https://docs.pre.dev/browser-agents/switch-from-claude-in-chrome) maps each Claude in Chrome tool to its pre.dev tool and has a line you can paste into `CLAUDE.md`.
|
|
65
|
+
|
|
62
66
|
## Local or cloud?
|
|
63
67
|
|
|
64
68
|
Both editions of pre.dev Browser Agents are in this server, and your agent picks per task.
|
|
@@ -128,7 +132,7 @@ Once you are signed in to pre.dev (`setup` or `login` does it, or set `PREDEV_AP
|
|
|
128
132
|
- `chrome_act`: click or type into an element described in plain words, like *"the Create button in the dialog"*, in under a second with no snapshot needed. If it isn't sure, it lists the likely matches instead of guessing.
|
|
129
133
|
- `chrome_wait` with `condition`: wait until a plain-words statement about the page is true, like *"the export has finished"*.
|
|
130
134
|
|
|
131
|
-
**Pricing.** The MCP server is free and open source, and the Chrome tools run free on your Mac. Plain-words actions and the cloud tools use pre.dev credits, the same credits as the rest of pre.dev ([pricing](https://pre.dev/pricing)). Each action costs a small fraction of one credit, and the free trial's credits cover
|
|
135
|
+
**Pricing.** The MCP server is free and open source, and the Chrome tools run free on your Mac. Plain-words actions and the cloud tools use pre.dev credits, the same credits as the rest of pre.dev ([pricing](https://pre.dev/pricing)). Each action costs a small fraction of one credit, and the free trial's credits cover hundreds of them. They show up in your usage as `browser-agents-local`, and `check` shows your plan. If the free plan's allowance or your credits run out, your agent tells you and offers to open the [billing page](https://pre.dev/billing) in your Chrome; everything else keeps working. Every other tool runs entirely on your machine and never calls pre.dev.
|
|
132
136
|
|
|
133
137
|
## Setup for every agent
|
|
134
138
|
|
|
@@ -169,7 +173,7 @@ pi mcp add predev -- npx -y @predotdev/mcp
|
|
|
169
173
|
pi mcp list
|
|
170
174
|
```
|
|
171
175
|
|
|
172
|
-
`pi mcp list` should say `predev: connected`. Pi
|
|
176
|
+
`pi mcp list` should say `predev: connected`. `pi mcp add` needs Pi 0.99 or newer. On an older Pi, run `pi install npm:pi-mcp-adapter` and add the `predev` entry to `~/.pi/agent/mcp.json` by hand (`setup` writes it for you).
|
|
173
177
|
|
|
174
178
|
</details>
|
|
175
179
|
|
|
@@ -512,6 +516,7 @@ npx -y @predotdev/mcp uninstall # remove it from every agent and delete its fil
|
|
|
512
516
|
| You had the hosted pre.dev MCP or an older `chrome` install | `setup` replaced it; this server has all of its tools. |
|
|
513
517
|
| The agent can't start the server, or `npx`/`node` not found | Run `setup`: it registers full paths that work in desktop apps. |
|
|
514
518
|
| Codex says a tool call timed out | Set `tool_timeout_sec = 900` (see the Codex setup). |
|
|
519
|
+
| `codex exec` says a tool call `requires approval, but approval policy is never` | Non-interactive Codex can't ask you, so allow the tools for that run: `codex exec -c 'mcp_servers.predev.default_tools_approval_mode="approve"' "..."`. |
|
|
515
520
|
| Anything else | Run `npx -y @predotdev/mcp check`, and look at `~/.predev/mcp/predev-mcp.log`. |
|
|
516
521
|
|
|
517
522
|
## Platform support
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@predotdev/mcp",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "pre.dev in any coding agent: Browser Agents in the Chrome you already use and in the cloud, plus specs and plans. One command to set up. Free and open source.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/setup.mjs
CHANGED
|
@@ -481,10 +481,18 @@ export function createSetup(ctx) {
|
|
|
481
481
|
if (!found) continue;
|
|
482
482
|
try { if (agent.unregister()) ok(`Removed from ${agent.label}`); } catch (error) { skip(`${agent.label}: ${error.message}`); }
|
|
483
483
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
fs.
|
|
487
|
-
|
|
484
|
+
// Every daemon it runs (one per Chrome data dir), then everything it keeps in ~/.predev/mcp:
|
|
485
|
+
// the stable copy, the saved key, logs and the cached profile list (names and emails).
|
|
486
|
+
const subdirs = (() => { try { return fs.readdirSync(baseStateDir, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => path.join(baseStateDir, d.name)); } catch { return []; } })();
|
|
487
|
+
const alive = pid => { try { process.kill(pid, 0); return true; } catch { return false; } };
|
|
488
|
+
for (const dir of new Set([stateDir, baseStateDir, ...subdirs])) {
|
|
489
|
+
const state = (() => { try { return JSON.parse(fs.readFileSync(path.join(dir, 'daemon.json'), 'utf8')); } catch { return null; } })();
|
|
490
|
+
if (!state) continue;
|
|
491
|
+
await request(state, 'POST', '/shutdown', {}, 3000).catch(() => {});
|
|
492
|
+
// It exits just after answering; wait, so its last log line can't recreate the folder.
|
|
493
|
+
for (let i = 0; i < 30 && state.pid && alive(state.pid); i++) await sleep(100);
|
|
494
|
+
}
|
|
495
|
+
fs.rmSync(baseStateDir, { recursive: true, force: true });
|
|
488
496
|
ok('Stopped it and deleted its files and saved key');
|
|
489
497
|
console.log('\nDone. Restart your agents. To also turn off remote debugging, open chrome://inspect/#remote-debugging.');
|
|
490
498
|
}
|
package/src/tools.mjs
CHANGED
|
@@ -180,10 +180,15 @@ async function pageLine(info, session) {
|
|
|
180
180
|
return `Tab ${tabId(info)} · ${label(profileOf(info))} · ${(title || '(untitled)').slice(0, 100)}\n${shortUrl}`;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// Dev servers on this computer or the local network, which rarely speak https.
|
|
184
|
+
const LOCAL_HOST = /^(localhost|127(\.\d{1,3}){3}|0\.0\.0\.0|\[::1\]|10(\.\d{1,3}){3}|192\.168(\.\d{1,3}){2}|172\.(1[6-9]|2\d|3[01])(\.\d{1,3}){2}|[^\s/:?#@]+\.(localhost|test))(:\d+)?([/?#]|$)/i;
|
|
185
|
+
|
|
183
186
|
function normalizeUrl(url) {
|
|
184
187
|
const value = String(url || '').trim();
|
|
185
188
|
if (!value) throw new Error('Pass a url.');
|
|
186
|
-
|
|
189
|
+
// "https:", "about:", "chrome:"... is a scheme, but "localhost:3000/app" is a host and port.
|
|
190
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(value) && !/^[^\s/:?#@]+:\d+([/?#]|$)/.test(value)) return value;
|
|
191
|
+
return `${LOCAL_HOST.test(value) ? 'http' : 'https'}://${value}`;
|
|
187
192
|
}
|
|
188
193
|
|
|
189
194
|
const isLocalDev = url => {
|
|
@@ -274,9 +279,10 @@ function shortHref(href) {
|
|
|
274
279
|
return s.length > 70 ? s.slice(0, 69) + '…' : s;
|
|
275
280
|
} catch (e) { return ''; }
|
|
276
281
|
}
|
|
277
|
-
|
|
282
|
+
// peek: list the elements without replacing the refs the agent got from its last snapshot.
|
|
283
|
+
function snapshot(max, peek) {
|
|
278
284
|
const refs = new Map();
|
|
279
|
-
window.__agentRefs = refs;
|
|
285
|
+
if (!peek) window.__agentRefs = refs;
|
|
280
286
|
const found = new Set();
|
|
281
287
|
collect(document, found);
|
|
282
288
|
const all = [...found];
|
|
@@ -382,7 +388,7 @@ function point(q) {
|
|
|
382
388
|
while (hit?.shadowRoot) { const inner = hit.shadowRoot.elementFromPoint(x, y); if (!inner || inner === hit) break; hit = inner; }
|
|
383
389
|
if (hit && !contains(el, hit) && !contains(hit, el)) covered = describe(hit);
|
|
384
390
|
}
|
|
385
|
-
return { x, y, desc: describe(el), covered };
|
|
391
|
+
return { x, y, desc: describe(el), covered, select: el.tagName === 'SELECT' };
|
|
386
392
|
}
|
|
387
393
|
function focus(q, clear) {
|
|
388
394
|
const f = find(q);
|
|
@@ -548,11 +554,17 @@ async function matchElement(session, description, ctx) {
|
|
|
548
554
|
return { ref: answer.ref || null, desc: answer.text, p: Number(answer.confidence) || 0, list };
|
|
549
555
|
}
|
|
550
556
|
|
|
557
|
+
// What a plain-words wait checks: the page's text and its interactive elements, since an empty field
|
|
558
|
+
// or an icon button has no text of its own. Elements go first: pre.dev reads the first 12,000 characters.
|
|
551
559
|
async function pageState(session) {
|
|
552
|
-
|
|
560
|
+
const [head, text] = await evaluate(session, `(() => {
|
|
553
561
|
const dialog = [...document.querySelectorAll('[role=dialog],[aria-modal=true],dialog[open],[role=alert]')].map(d => d.innerText).join('\\n').slice(0, 1500);
|
|
554
|
-
return 'Title: ' + document.title + '\\nURL: ' + location.href + (dialog ? '\\nDialog/alert: ' + dialog : '')
|
|
562
|
+
return ['Title: ' + document.title + '\\nURL: ' + location.href + (dialog ? '\\nDialog/alert: ' + dialog : ''), document.body ? document.body.innerText : ''];
|
|
555
563
|
})()`, 10000);
|
|
564
|
+
const snap = await inPage(session, 'snapshot(150, true)', 10000).catch(() => '');
|
|
565
|
+
const elements = String(snap).split('\n').filter(line => /^\[e\d+\] /.test(line)).map(line => line.replace(/^\[e\d+\] /, '- ')).join('\n').slice(0, 3000);
|
|
566
|
+
const room = Math.min(8000, Math.max(2000, 11500 - head.length - elements.length));
|
|
567
|
+
return `${head}${elements ? `\nInteractive elements:\n${elements}` : ''}\n\nText:\n${text.slice(0, room)}`;
|
|
556
568
|
}
|
|
557
569
|
|
|
558
570
|
// ---------------------------------------------------------------- tools
|
|
@@ -681,6 +693,8 @@ const TOOLS = [
|
|
|
681
693
|
({ x, y } = hit);
|
|
682
694
|
what = hit.desc;
|
|
683
695
|
if (hit.covered) note = `\nNote: ${hit.covered} was on top of it at that point; the click went there.`;
|
|
696
|
+
// A click only opens a dropdown's list; arrow keys and option clicks don't reach it in every Chrome.
|
|
697
|
+
if (hit.select) note += `\nNote: this is a dropdown. To pick an option, call chrome_type with this element and the option's text.`;
|
|
684
698
|
}
|
|
685
699
|
await click(session, x, y, args.double ? 2 : 1);
|
|
686
700
|
await settle(session, before);
|