iframer-toolkit 0.10.8 → 0.11.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 +68 -65
- package/dist/cli.js +2620 -1769
- 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 +44 -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.1",
|
|
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,25 @@ 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
|
+
- Private values are separated from the API, not a reason to stop. `ingest` swaps anything token-, email- or phone-like for a placeholder (`{id}` in paths, `{key}` for keys) and lists what it replaced. Endpoints that had one carry `privateValues` in api.json: pass that value per call.
|
|
186
|
+
- Identity: api.json `identity` (and the knowledge file) says which auth headers the site sends and where the page keeps each value, e.g. `localStorage["session -> user.token"]`. `__ifk.call` attaches them on its own, cookies included. If a call returns 401 with a `hint`, use the site once so it sends its token, then retry.
|
|
187
|
+
|
|
160
188
|
### Handle captchas
|
|
161
189
|
|
|
162
190
|
Captcha handling depends on the browser mode:
|
|
@@ -171,16 +199,13 @@ Captcha handling depends on the browser mode:
|
|
|
171
199
|
execute: steps: [{ type: "screenshot" }] // verify captcha is gone, then continue
|
|
172
200
|
```
|
|
173
201
|
|
|
174
|
-
**
|
|
175
|
-
-
|
|
202
|
+
**Headless, or nobody at the screen**:
|
|
203
|
+
- If you hit a captcha in headless, iframer auto-escalates to binary-headful.
|
|
204
|
+
- 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
205
|
```
|
|
177
206
|
execute: steps: [{ type: "solve-captcha" }, { type: "screenshot" }]
|
|
178
207
|
```
|
|
179
208
|
|
|
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
209
|
### Handle OTP / 2FA codes
|
|
185
210
|
|
|
186
211
|
If the login step encounters a code field and no TOTP secret is stored:
|
|
@@ -201,7 +226,7 @@ When the browser crashes, freezes, or you get connection errors:
|
|
|
201
226
|
```
|
|
202
227
|
session: { action: "restart" }
|
|
203
228
|
```
|
|
204
|
-
This kills ALL running browser instances
|
|
229
|
+
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
230
|
|
|
206
231
|
**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
232
|
|
|
@@ -232,7 +257,7 @@ knowledge: { action: "clear", domain: "example.com" }
|
|
|
232
257
|
|
|
233
258
|
If an `execute` call takes a very long time (30s+), it's probably stuck on one of:
|
|
234
259
|
|
|
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.
|
|
260
|
+
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
261
|
|
|
237
262
|
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
263
|
|
|
@@ -245,7 +270,7 @@ If an `execute` call takes a very long time (30s+), it's probably stuck on one o
|
|
|
245
270
|
|
|
246
271
|
2. If captcha visible:
|
|
247
272
|
- binary-headful → ask user to solve it in the visible window
|
|
248
|
-
-
|
|
273
|
+
- nobody at the screen → execute: steps: [{ type: "solve-captcha" }]
|
|
249
274
|
|
|
250
275
|
3. If the page shows an error or unexpected state:
|
|
251
276
|
- Take a snapshot to get element refs
|
|
@@ -256,11 +281,9 @@ If an `execute` call takes a very long time (30s+), it's probably stuck on one o
|
|
|
256
281
|
|
|
257
282
|
**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
283
|
|
|
259
|
-
**Timeouts:**
|
|
284
|
+
**Timeouts:** Heavy SPAs (Slack, Figma) can take 30+ seconds to load. If an execute call times out:
|
|
260
285
|
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
|
|
286
|
+
2. Take a screenshot to see where the browser is — the session is likely still alive
|
|
264
287
|
|
|
265
288
|
## Multi-step flows (email → code, OAuth, etc.)
|
|
266
289
|
|
|
@@ -295,7 +318,6 @@ Instead:
|
|
|
295
318
|
**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
319
|
|
|
297
320
|
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
321
|
- If doing a complex multi-step pipeline → break it into smaller execute calls
|
|
300
322
|
- If just trying to capture APIs → you probably already have enough from previous runs. Check `knowledge get <domain>` before retrying.
|
|
301
323
|
|
|
@@ -305,7 +327,7 @@ If the MCP disconnects repeatedly on the same operation, the site is probably to
|
|
|
305
327
|
|---------|-----------------|
|
|
306
328
|
| Guessing CSS selectors like `button[data-qa='submit']` | Use `snapshot` → read refs → use `@e` refs |
|
|
307
329
|
| 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`
|
|
330
|
+
| Re-storing credentials when login fails | Retry with stronger mode (`binary-headful`) |
|
|
309
331
|
| Launching browser without checking knowledge cache | Always call `knowledge get <domain>` first |
|
|
310
332
|
| Using `browse` for a page that needs interaction | Use `execute` with a pipeline |
|
|
311
333
|
| Running a huge pipeline without snapshots | Split into navigate+snapshot, then interact based on refs |
|
|
@@ -374,16 +396,13 @@ job to close it, not theirs.
|
|
|
374
396
|
| Simple data extraction, no login | Omit mode (auto-select, starts headless) |
|
|
375
397
|
| Site with login | Omit mode (auto-escalation if headless is blocked) |
|
|
376
398
|
| Known bot-detection (Cloudflare, etc.) | `binary-headful` |
|
|
377
|
-
| Heavy SPA (Slack, Figma, Linear, etc.) | `binary-headful`
|
|
399
|
+
| Heavy SPA (Slack, Figma, Linear, etc.) | `binary-headful` |
|
|
378
400
|
| Interactive session (browsing, clicking around) | `binary-headful` — most stable for long flows |
|
|
379
|
-
|
|
|
380
|
-
| Need to watch the browser remotely / headless server | `docker-headful` |
|
|
401
|
+
| Nobody available to solve a captcha | Any mode, with a best-effort `solve-captcha` step (needs `ANTHROPIC_API_KEY`) |
|
|
381
402
|
| Reverse-engineering after login | Same mode that login succeeded in |
|
|
382
403
|
| "Use my browser" / user's own logins / their open tab | `extension` + `tabId` from the `tabs` tool |
|
|
383
404
|
|
|
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.
|
|
405
|
+
**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
406
|
|
|
388
407
|
## Response handling
|
|
389
408
|
|