iframer-toolkit 0.10.8 → 0.11.0
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 +66 -65
- package/dist/cli.js +2355 -1767
- package/dist/local-server.cjs +767 -1200
- package/dist/mcp-server.cjs +26 -110
- package/extension/README.md +10 -8
- package/extension/background.js +135 -84
- package/extension/manifest.json +1 -1
- package/extension/native-host.cjs +105 -29
- package/extension/popup.js +13 -4
- package/package.json +3 -6
- package/skills/iframer.md +43 -25
|
@@ -12,9 +12,16 @@
|
|
|
12
12
|
// stdlib) and must read the same secret locations as src/lib/auth/crypto.ts.
|
|
13
13
|
|
|
14
14
|
const fs = require("fs");
|
|
15
|
+
const http = require("http");
|
|
15
16
|
const os = require("os");
|
|
16
17
|
const path = require("path");
|
|
17
18
|
|
|
19
|
+
// Same window the local server allocates its port from
|
|
20
|
+
// (src/mcp/local-server.ts, extension/background.js).
|
|
21
|
+
const PORT_START = 3022;
|
|
22
|
+
const PORT_END = 3042;
|
|
23
|
+
const PROBE_TIMEOUT_MS = 1500;
|
|
24
|
+
|
|
18
25
|
function readToken() {
|
|
19
26
|
if (process.env.IFRAMER_SECRET) return process.env.IFRAMER_SECRET;
|
|
20
27
|
const dataDir = process.env.IFRAMER_DATA_DIR || path.join(os.homedir(), ".iframer");
|
|
@@ -33,34 +40,107 @@ function readToken() {
|
|
|
33
40
|
return "";
|
|
34
41
|
}
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
// server writes on startup (src/lib/browser/registry.ts) and checks the pid is
|
|
38
|
-
// actually alive — so the extension can connect straight to the right port
|
|
39
|
-
// instead of blind-scanning 21 ports (each dead dial logs a console error).
|
|
40
|
-
function readServerInfo() {
|
|
43
|
+
function readRegisteredPort() {
|
|
41
44
|
const dataDir = process.env.IFRAMER_DATA_DIR || path.join(os.homedir(), ".iframer");
|
|
42
45
|
try {
|
|
43
46
|
const info = JSON.parse(fs.readFileSync(path.join(dataDir, "server.json"), "utf8"));
|
|
44
|
-
|
|
45
|
-
if (info && typeof info.pid === "number") {
|
|
46
|
-
try {
|
|
47
|
-
process.kill(info.pid, 0);
|
|
48
|
-
alive = true;
|
|
49
|
-
} catch {
|
|
50
|
-
/* pid gone — stale registry */
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return { port: info && typeof info.port === "number" ? info.port : null, alive };
|
|
47
|
+
return info && typeof info.port === "number" ? info.port : null;
|
|
54
48
|
} catch {
|
|
55
|
-
return
|
|
49
|
+
return null;
|
|
56
50
|
}
|
|
57
51
|
}
|
|
58
52
|
|
|
59
|
-
function
|
|
53
|
+
function probe(port) {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
let settled = false;
|
|
56
|
+
const finish = (ok) => {
|
|
57
|
+
if (settled) return;
|
|
58
|
+
settled = true;
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
resolve(ok);
|
|
61
|
+
};
|
|
62
|
+
const req = http.get({ host: "127.0.0.1", port, path: "/health", agent: false }, (res) => {
|
|
63
|
+
let body = "";
|
|
64
|
+
res.setEncoding("utf8");
|
|
65
|
+
res.on("data", (d) => {
|
|
66
|
+
if (body.length < 4096) body += d;
|
|
67
|
+
});
|
|
68
|
+
res.on("end", () => {
|
|
69
|
+
try {
|
|
70
|
+
finish(JSON.parse(body).ok === true);
|
|
71
|
+
} catch {
|
|
72
|
+
finish(false);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
res.on("error", () => finish(false));
|
|
76
|
+
});
|
|
77
|
+
req.on("error", () => finish(false));
|
|
78
|
+
const timer = setTimeout(() => {
|
|
79
|
+
req.destroy();
|
|
80
|
+
finish(false);
|
|
81
|
+
}, PROBE_TIMEOUT_MS);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Where is the iframer server right now? Only a server that actually answers
|
|
86
|
+
// counts. A pid check can't decide this: on Windows a reused pid looks alive,
|
|
87
|
+
// and a server running as another user looks dead. Probing from here also lets
|
|
88
|
+
// the extension find the port without dialing dead ones (each dead dial logs a
|
|
89
|
+
// console error in the extension).
|
|
90
|
+
async function locateServer() {
|
|
91
|
+
const registered = readRegisteredPort();
|
|
92
|
+
if (registered && (await probe(registered))) return { port: registered, alive: true };
|
|
93
|
+
|
|
94
|
+
const ports = [];
|
|
95
|
+
for (let p = PORT_START; p <= PORT_END; p++) if (p !== registered) ports.push(p);
|
|
96
|
+
// All at once: Windows takes about 2s to refuse a closed port, one by one
|
|
97
|
+
// would take most of a minute.
|
|
98
|
+
const found = await new Promise((resolve) => {
|
|
99
|
+
let left = ports.length;
|
|
100
|
+
for (const p of ports) {
|
|
101
|
+
probe(p).then((ok) => {
|
|
102
|
+
if (ok) resolve(p);
|
|
103
|
+
else if (--left === 0) resolve(null);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return found ? { port: found, alive: true } : { port: registered, alive: false };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function send(msg, done) {
|
|
60
111
|
const payload = Buffer.from(JSON.stringify(msg), "utf8");
|
|
61
112
|
const header = Buffer.alloc(4);
|
|
62
113
|
header.writeUInt32LE(payload.length, 0);
|
|
63
|
-
process.stdout.write(Buffer.concat([header, payload]));
|
|
114
|
+
process.stdout.write(Buffer.concat([header, payload]), done);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Chrome may close stdin while a reply is still being worked out, so exit only
|
|
118
|
+
// once stdin has ended AND every reply has been written.
|
|
119
|
+
let pending = 0;
|
|
120
|
+
let stdinEnded = false;
|
|
121
|
+
function exitIfDone() {
|
|
122
|
+
if (stdinEnded && pending === 0) process.exit(0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function reply(msg) {
|
|
126
|
+
let out;
|
|
127
|
+
try {
|
|
128
|
+
if (msg && msg.cmd === "get-token") {
|
|
129
|
+
const token = readToken();
|
|
130
|
+
out = token ? { ok: true, token } : { ok: false, error: "no secret file found — run any iframer command once to create it" };
|
|
131
|
+
} else if (msg && msg.cmd === "get-info") {
|
|
132
|
+
// Token + live server location in one call.
|
|
133
|
+
out = { ok: true, token: readToken() || null, ...(await locateServer()) };
|
|
134
|
+
} else {
|
|
135
|
+
out = { ok: false, error: "unknown command" };
|
|
136
|
+
}
|
|
137
|
+
} catch (e) {
|
|
138
|
+
out = { ok: false, error: String((e && e.message) || e) };
|
|
139
|
+
}
|
|
140
|
+
send(out, () => {
|
|
141
|
+
pending--;
|
|
142
|
+
exitIfDone();
|
|
143
|
+
});
|
|
64
144
|
}
|
|
65
145
|
|
|
66
146
|
let buf = Buffer.alloc(0);
|
|
@@ -75,17 +155,13 @@ process.stdin.on("data", (chunk) => {
|
|
|
75
155
|
try {
|
|
76
156
|
msg = JSON.parse(body.toString("utf8"));
|
|
77
157
|
} catch {
|
|
78
|
-
/*
|
|
79
|
-
}
|
|
80
|
-
if (msg && msg.cmd === "get-token") {
|
|
81
|
-
const token = readToken();
|
|
82
|
-
send(token ? { ok: true, token } : { ok: false, error: "no secret file found — run any iframer command once to create it" });
|
|
83
|
-
} else if (msg && msg.cmd === "get-info") {
|
|
84
|
-
// Token + live server location in one call.
|
|
85
|
-
send({ ok: true, token: readToken() || null, ...readServerInfo() });
|
|
86
|
-
} else {
|
|
87
|
-
send({ ok: false, error: "unknown command" });
|
|
158
|
+
/* answered as an unknown command */
|
|
88
159
|
}
|
|
160
|
+
pending++;
|
|
161
|
+
reply(msg);
|
|
89
162
|
}
|
|
90
163
|
});
|
|
91
|
-
process.stdin.on("end", () =>
|
|
164
|
+
process.stdin.on("end", () => {
|
|
165
|
+
stdinEnded = true;
|
|
166
|
+
exitIfDone();
|
|
167
|
+
});
|
package/extension/popup.js
CHANGED
|
@@ -15,10 +15,10 @@ function renderStatus(status) {
|
|
|
15
15
|
const connected = status && status.connected;
|
|
16
16
|
$("dot").className = "dot" + (connected ? " on" : "");
|
|
17
17
|
const reasonText = {
|
|
18
|
-
"no-token": "No token
|
|
19
|
-
"server-not-found": "iframer server not found.
|
|
18
|
+
"no-token": "No token yet. Run `iframer install extension chrome` in a terminal and this pairs by itself, or paste the output of `iframer extension token` below.",
|
|
19
|
+
"server-not-found": "iframer server not found. It starts with your Claude/iframer session. Will connect by itself.",
|
|
20
20
|
"server-not-running": "iframer server not running — it starts with your Claude/iframer session. Will connect by itself.",
|
|
21
|
-
"bad-token": "Token rejected.
|
|
21
|
+
"bad-token": "Token rejected by the server. Paste the output of `iframer extension token` below.",
|
|
22
22
|
disconnected: "Disconnected — retrying…",
|
|
23
23
|
};
|
|
24
24
|
if (connected) {
|
|
@@ -86,7 +86,7 @@ $("autopair").addEventListener("click", async () => {
|
|
|
86
86
|
const res = await send("auto-pair");
|
|
87
87
|
if (res.ok) {
|
|
88
88
|
if (res.token) $("token").value = res.token;
|
|
89
|
-
$("pairmsg").textContent = "✓ Paired.";
|
|
89
|
+
$("pairmsg").textContent = res.status && res.status.connected ? "✓ Paired." : "Token saved. See the status above.";
|
|
90
90
|
renderStatus(res.status);
|
|
91
91
|
} else {
|
|
92
92
|
$("pairmsg").textContent = res.error || "Auto-pair failed — paste the token manually below.";
|
|
@@ -105,4 +105,13 @@ $("save").addEventListener("click", async () => {
|
|
|
105
105
|
$("save").textContent = "Save & connect";
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
// Keep the popup live: pairing usually finishes while it's open.
|
|
109
|
+
chrome.storage.onChanged.addListener((changes, area) => {
|
|
110
|
+
if (area !== "local") return;
|
|
111
|
+
if (changes.status) renderStatus(changes.status.newValue);
|
|
112
|
+
if (changes.token && changes.token.newValue && document.activeElement !== $("token")) {
|
|
113
|
+
$("token").value = changes.token.newValue;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
108
117
|
init();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iframer-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Browser access toolkit for AI agents — CLI and MCP server for headless/headful browsing, login flows, and pipeline execution.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"iframer",
|
|
@@ -39,9 +39,6 @@
|
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"start": "bun run index.ts",
|
|
42
|
-
"start:docker": "docker compose up --build -d",
|
|
43
|
-
"stop:docker": "docker compose down",
|
|
44
|
-
"logs:docker": "docker compose logs -f api",
|
|
45
42
|
"test:smoke": "node tests/smoke.js",
|
|
46
43
|
"mcp:install": "node bin/cli.js install-mcp",
|
|
47
44
|
"mcp:remove": "node bin/cli.js remove-mcp",
|
|
@@ -50,13 +47,13 @@
|
|
|
50
47
|
"test": "bun test tests/unit",
|
|
51
48
|
"test:extension": "node tests/integration/extension-bridge.mjs",
|
|
52
49
|
"prepublishOnly": "bun run build",
|
|
53
|
-
"test:cdp-relay": "bun tests/integration/cdp-relay.mjs"
|
|
50
|
+
"test:cdp-relay": "bun tests/integration/cdp-relay.mjs",
|
|
51
|
+
"test:kit": "bun tests/integration/kit-bridge.mjs"
|
|
54
52
|
},
|
|
55
53
|
"dependencies": {
|
|
56
54
|
"@anthropic-ai/sdk": "^0.80.0",
|
|
57
55
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
58
56
|
"better-sqlite3": "^12.8.0",
|
|
59
|
-
"fingerprint-generator": "^2.1.81",
|
|
60
57
|
"patchright": "^1.58.2",
|
|
61
58
|
"playwright-core": "^1.58.0",
|
|
62
59
|
"ws": "^8.18.0",
|
package/skills/iframer.md
CHANGED
|
@@ -10,10 +10,12 @@ You are an expert at using the iframer browser automation toolkit. The user want
|
|
|
10
10
|
- `iframer.credentials` — list or store login credentials (NEVER re-store as recovery)
|
|
11
11
|
- `iframer.execute` — run a browser pipeline (navigate, login, screenshot, snapshot, etc.)
|
|
12
12
|
- `iframer.browse` — fast headless page fetch (no pipeline needed)
|
|
13
|
-
- `iframer.session` — stop (save state)
|
|
13
|
+
- `iframer.session` — stop (save state, close the browser), clear (wipe session data), restart (recover from a freeze or crash), capture-start / capture-stop (record XHR/fetch), get-cookies, get-auth
|
|
14
14
|
- `iframer.status` — system health, modes, stored credentials
|
|
15
15
|
- `iframer.reverse-engineer` — capture API calls a site makes
|
|
16
16
|
- `iframer.tabs` — list/open tabs in the user's REAL Chrome (needs the extension)
|
|
17
|
+
- `iframer.remember` — per-domain element anchors, targeted as `@a:<name>` in any selector
|
|
18
|
+
- `iframer.clipboard` — read/write the machine's clipboard (same one the user's Chrome uses)
|
|
17
19
|
|
|
18
20
|
## Core rules
|
|
19
21
|
|
|
@@ -39,7 +41,8 @@ The `execute` tool takes a `steps` array. Each step is an object with a `type` f
|
|
|
39
41
|
| `snapshot` | — | Get interactive elements as structured list with @e refs. **Do this BEFORE interacting.** Fields no human can reach (honeypots pushed offscreen/clipped/transparent by an ancestor) are omitted; bait-looking ones (`website`, `fax`, tabindex=-1) are marked `[honeypot?]` — leave those empty unless the task clearly needs them. |
|
|
40
42
|
| `find` | `role?`, `name?`, `text?`, `placeholder?`, `label?` | Find a specific element, returns a ref |
|
|
41
43
|
| `screenshot` | `annotate?` | Take a screenshot. `annotate: true` overlays numbered badges. |
|
|
42
|
-
| `click` | `selector` | Click element (use @e refs from snapshot/find). Text selectors (`text=`, `:has-text()`, `:text-is()
|
|
44
|
+
| `click` | `selector` | Click element (use @e refs from snapshot/find). Text selectors (`text=`, `:has-text()`, `:text-is()`, `>>`) are rejected: `find` by text, then click the ref. |
|
|
45
|
+
| `right-click` | `selector` or `x`, `y` | Right-click an element or coordinates |
|
|
43
46
|
| `fill` | `selector`, `value`, `force?` | Fill input (use @e refs). Refuses honeypot fields (unreachable by a human) with an explanatory error — skip the field. `force: true` bypasses, only when you are certain it is real. |
|
|
44
47
|
| `human-click` | `selector` or `x`, `y` | Human-like click with random offset |
|
|
45
48
|
| `human-type` | `selector`, `value`, `clear?` | Human-like typing with variable delays. Appends by default; `clear: true` replaces the existing text. |
|
|
@@ -49,13 +52,19 @@ The `execute` tool takes a `steps` array. Each step is an object with a `type` f
|
|
|
49
52
|
| `evaluate` | `expression` | Run JS, return result |
|
|
50
53
|
| `extract` | `expression` | Run JS, include result in response |
|
|
51
54
|
| `keyboard` | `key`, `meta?`, `ctrl?` | Press a key (Enter, Escape, Tab, etc.). Cmd/Ctrl+A selects all everywhere; other Cmd/Ctrl shortcuts are refused in extension mode. |
|
|
52
|
-
| `
|
|
55
|
+
| `select` | `selector`, `value` | Pick an option in a native `<select>`, with real change events. Never set `.value` via `evaluate`. |
|
|
56
|
+
| `type-code` | `value`, `selector?` | Type a verification code digit by digit. Default target: `input[type="tel"]`. |
|
|
57
|
+
| `upload` | `selector`, `files` | Set absolute local file paths on an `<input type=file>` |
|
|
58
|
+
| `paste` | `selector?` | Paste the OS clipboard into a field, or the focused element if omitted. More reliable than a ⌘V `keyboard` step. |
|
|
59
|
+
| `download` | `url`, `path?` | Fetch a file with the browser's cookies and save it server-side, no Save-As dialog. Default path: `~/.iframer/downloads/<name>`. |
|
|
60
|
+
| `solve-captcha` | — | Best-effort reCAPTCHA or hCaptcha solve with vision AI. Works in any mode; needs `ANTHROPIC_API_KEY`. Fails the run with `captcha-unsolvable` if it can't solve it. |
|
|
61
|
+
| `recaptcha` | `action`, `tiles?` | Manual captcha control: `info`, `click`, `answer`, `select`, `verify`, `solve`. The full workflow arrives in the result when a captcha blocks a run. |
|
|
53
62
|
|
|
54
63
|
### Pipeline options
|
|
55
64
|
|
|
56
65
|
| Option | Default | Description |
|
|
57
66
|
|--------|---------|-------------|
|
|
58
|
-
| `mode` | auto | `headless`, `binary-headful
|
|
67
|
+
| `mode` | auto | `headless`, `binary-headful`. Omit for auto-select + auto-escalation. |
|
|
59
68
|
| `captureApi` | false | Record all XHR/fetch requests. Use for reverse-engineering, and when a form submit fails with only a vague on-page message: every HTTP >= 400 response is reported per step, so you read the server's real answer (e.g. `POST /api/apply → 422: {"error":"..."}`) instead of guessing. |
|
|
60
69
|
| `staleTimeoutMs` | 20000 | Per-step stale-state timeout |
|
|
61
70
|
| `continueOnError` | false | Don't abort pipeline on step failure |
|
|
@@ -157,6 +166,24 @@ execute: steps: [{ type: "click", selector: "@e3" }, { type: "screenshot" }]
|
|
|
157
166
|
|
|
158
167
|
The more interactions (scroll, click tabs, navigate sub-pages), the more API calls get captured. Results feed into the knowledge cache automatically.
|
|
159
168
|
|
|
169
|
+
### Reverse-engineer inside a browser iframer doesn't control (T3 Code, Playwright MCP, ...)
|
|
170
|
+
|
|
171
|
+
When you already drive a browser through your own tools (a T3 Code preview tab, Playwright MCP, Chrome DevTools MCP), don't launch iframer's browser: load the in-page kit into that tab. It records the page's own calls on the user's real session and exports shapes, never values. Use the CLI for this, not the MCP tools.
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
1. iframer kit --for t3 # or --for playwright-mcp / --for any. Prints the exact tool calls.
|
|
175
|
+
2. Follow the printed install steps (upload the kit file, stash it, top-level eval). Never paste the kit's source.
|
|
176
|
+
3. Drive the site with your normal tools. After EVERY full page load, re-run the printed eval step (step 4).
|
|
177
|
+
4. window.__ifk.dump(), then: iframer ingest --latest --typed
|
|
178
|
+
-> ./<host>-api/api.json, client.js, client.d.ts, plus the knowledge cache. The capture file is deleted.
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- Replays run IN the page: `window.__ifk.call('GET','/path')`, or load the generated `client.js` (`iframer kit --for t3 --client ./<host>-api/client.js` prints the steps). HttpOnly cookies never leave the browser, so curl replays fail.
|
|
182
|
+
- Writes need `{approved:true}`: ask the user first. Replays stop for good on 403, 429 or a captcha page. Do not retry or work around it.
|
|
183
|
+
- Sites that detect tampering: `iframer kit --mode passive` (no patching; URLs and form posts only).
|
|
184
|
+
- Report coverage as the kit states it. It is never complete: workers, redirect hops and cross-origin iframes are invisible.
|
|
185
|
+
- `ingest` refuses (exit 2) if anything value-like slipped into the capture. Don't edit the file to force it through; tell the user what was found.
|
|
186
|
+
|
|
160
187
|
### Handle captchas
|
|
161
188
|
|
|
162
189
|
Captcha handling depends on the browser mode:
|
|
@@ -171,16 +198,13 @@ Captcha handling depends on the browser mode:
|
|
|
171
198
|
execute: steps: [{ type: "screenshot" }] // verify captcha is gone, then continue
|
|
172
199
|
```
|
|
173
200
|
|
|
174
|
-
**
|
|
175
|
-
-
|
|
201
|
+
**Headless, or nobody at the screen**:
|
|
202
|
+
- If you hit a captcha in headless, iframer auto-escalates to binary-headful.
|
|
203
|
+
- If nobody can solve it by hand, try the `solve-captcha` step. It's best effort: it uses vision AI, needs `ANTHROPIC_API_KEY`, and fails on many challenges.
|
|
176
204
|
```
|
|
177
205
|
execute: steps: [{ type: "solve-captcha" }, { type: "screenshot" }]
|
|
178
206
|
```
|
|
179
207
|
|
|
180
|
-
**Headless**:
|
|
181
|
-
- Captchas can't be solved in headless. If you hit one, iframer auto-escalates to binary-headful.
|
|
182
|
-
- If it still fails, switch to docker-headful and use `solve-captcha`.
|
|
183
|
-
|
|
184
208
|
### Handle OTP / 2FA codes
|
|
185
209
|
|
|
186
210
|
If the login step encounters a code field and no TOTP secret is stored:
|
|
@@ -201,7 +225,7 @@ When the browser crashes, freezes, or you get connection errors:
|
|
|
201
225
|
```
|
|
202
226
|
session: { action: "restart" }
|
|
203
227
|
```
|
|
204
|
-
This kills ALL running browser instances
|
|
228
|
+
This kills ALL running browser instances and resets state. Credentials and knowledge cache are NOT affected. The next `execute` call launches a fresh browser automatically.
|
|
205
229
|
|
|
206
230
|
**The execute tool also auto-recovers:** if it detects a *connection-level* crash (connection closed, ECONNREFUSED/ECONNRESET/EPIPE, socket hang up), it restarts the browser server and retries once before reporting failure. Timeouts are deliberately NOT auto-retried — a slow-but-healthy pipeline that hit the client timeout must not be re-run, or non-idempotent steps (form submits, purchases) would double-execute. If auto-recovery fails, or a step timed out, call `session restart` manually and continue from where you left off (don't blindly re-run the whole pipeline).
|
|
207
231
|
|
|
@@ -232,7 +256,7 @@ knowledge: { action: "clear", domain: "example.com" }
|
|
|
232
256
|
|
|
233
257
|
If an `execute` call takes a very long time (30s+), it's probably stuck on one of:
|
|
234
258
|
|
|
235
|
-
1. **Captcha blocking progress** — the login/navigate step can't proceed because a captcha appeared. In binary-headful mode, tell the user to solve it in the visible window.
|
|
259
|
+
1. **Captcha blocking progress** — the login/navigate step can't proceed because a captcha appeared. In binary-headful mode, tell the user to solve it in the visible window. If nobody can, add a `solve-captcha` step.
|
|
236
260
|
|
|
237
261
|
2. **Waiting for a page transition that never happens** — the site didn't redirect after form submission. Take a screenshot to see the current state, then decide what to do (wrong button clicked, error message on page, etc.).
|
|
238
262
|
|
|
@@ -245,7 +269,7 @@ If an `execute` call takes a very long time (30s+), it's probably stuck on one o
|
|
|
245
269
|
|
|
246
270
|
2. If captcha visible:
|
|
247
271
|
- binary-headful → ask user to solve it in the visible window
|
|
248
|
-
-
|
|
272
|
+
- nobody at the screen → execute: steps: [{ type: "solve-captcha" }]
|
|
249
273
|
|
|
250
274
|
3. If the page shows an error or unexpected state:
|
|
251
275
|
- Take a snapshot to get element refs
|
|
@@ -256,11 +280,9 @@ If an `execute` call takes a very long time (30s+), it's probably stuck on one o
|
|
|
256
280
|
|
|
257
281
|
**CRITICAL: Don't restart from scratch.** The browser session persists between execute calls (same daemon, same page context). If you were mid-login and hit a captcha, you don't need to navigate back to the login page — you're still there. Just solve the captcha and continue.
|
|
258
282
|
|
|
259
|
-
**Timeouts:**
|
|
283
|
+
**Timeouts:** Heavy SPAs (Slack, Figma) can take 30+ seconds to load. If an execute call times out:
|
|
260
284
|
1. Use `staleTimeoutMs: 60000` or higher for heavy sites
|
|
261
|
-
2.
|
|
262
|
-
3. Take a screenshot to see where the browser is — the session is likely still alive
|
|
263
|
-
4. If the container crashed, `docker compose up -d` restarts it; stored sessions/credentials survive since they're in the host's SQLite
|
|
285
|
+
2. Take a screenshot to see where the browser is — the session is likely still alive
|
|
264
286
|
|
|
265
287
|
## Multi-step flows (email → code, OAuth, etc.)
|
|
266
288
|
|
|
@@ -295,7 +317,6 @@ Instead:
|
|
|
295
317
|
**Your progress is NOT lost.** Credentials, sessions, and knowledge cache are in the SQLite database on disk — they survive MCP server restarts. You don't need to re-login or re-store anything.
|
|
296
318
|
|
|
297
319
|
If the MCP disconnects repeatedly on the same operation, the site is probably too heavy for the current mode. Switch to a lighter approach:
|
|
298
|
-
- If using `docker-headful` → switch to `binary-headful` (less overhead)
|
|
299
320
|
- If doing a complex multi-step pipeline → break it into smaller execute calls
|
|
300
321
|
- If just trying to capture APIs → you probably already have enough from previous runs. Check `knowledge get <domain>` before retrying.
|
|
301
322
|
|
|
@@ -305,7 +326,7 @@ If the MCP disconnects repeatedly on the same operation, the site is probably to
|
|
|
305
326
|
|---------|-----------------|
|
|
306
327
|
| Guessing CSS selectors like `button[data-qa='submit']` | Use `snapshot` → read refs → use `@e` refs |
|
|
307
328
|
| Passing `usernameSelector`/`passwordSelector` to login step | Omit them — auto-detect handles it |
|
|
308
|
-
| Re-storing credentials when login fails | Retry with stronger mode (`binary-headful`
|
|
329
|
+
| Re-storing credentials when login fails | Retry with stronger mode (`binary-headful`) |
|
|
309
330
|
| Launching browser without checking knowledge cache | Always call `knowledge get <domain>` first |
|
|
310
331
|
| Using `browse` for a page that needs interaction | Use `execute` with a pipeline |
|
|
311
332
|
| Running a huge pipeline without snapshots | Split into navigate+snapshot, then interact based on refs |
|
|
@@ -374,16 +395,13 @@ job to close it, not theirs.
|
|
|
374
395
|
| Simple data extraction, no login | Omit mode (auto-select, starts headless) |
|
|
375
396
|
| Site with login | Omit mode (auto-escalation if headless is blocked) |
|
|
376
397
|
| Known bot-detection (Cloudflare, etc.) | `binary-headful` |
|
|
377
|
-
| Heavy SPA (Slack, Figma, Linear, etc.) | `binary-headful`
|
|
398
|
+
| Heavy SPA (Slack, Figma, Linear, etc.) | `binary-headful` |
|
|
378
399
|
| Interactive session (browsing, clicking around) | `binary-headful` — most stable for long flows |
|
|
379
|
-
|
|
|
380
|
-
| Need to watch the browser remotely / headless server | `docker-headful` |
|
|
400
|
+
| Nobody available to solve a captcha | Any mode, with a best-effort `solve-captcha` step (needs `ANTHROPIC_API_KEY`) |
|
|
381
401
|
| Reverse-engineering after login | Same mode that login succeeded in |
|
|
382
402
|
| "Use my browser" / user's own logins / their open tab | `extension` + `tabId` from the `tabs` tool |
|
|
383
403
|
|
|
384
|
-
**Prefer `binary-headful`
|
|
385
|
-
|
|
386
|
-
Use `docker-headful` only when: (a) automated captcha solving is needed, (b) the user asks for it explicitly, or (c) it's a remote/headless server with no display.
|
|
404
|
+
**Prefer `binary-headful` for sites that block headless.** It runs Chrome directly on the user's machine, is stable for long sessions, and the user can solve captchas manually.
|
|
387
405
|
|
|
388
406
|
## Response handling
|
|
389
407
|
|