@agent360/browser-mcp 1.13.0 → 1.15.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 +15 -6
- package/bin/cli.js +44 -10
- package/extension/background.js +357 -11
- package/extension/manifest.json +2 -2
- package/extension/popup.html +4 -4
- package/index.js +24 -5
- package/package.json +17 -6
- package/tools.js +56 -0
package/README.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# Browser MCP by Agent360
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@agent360/browser-mcp)
|
|
4
|
+
[](https://www.npmjs.com/package/@agent360/browser-mcp)
|
|
5
|
+
[](https://github.com/Agent360dk/browser-mcp)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://modelcontextprotocol.io)
|
|
8
|
+
|
|
3
9
|
**Control your real Chrome from Claude Code — with your logins, cookies, and 2FA.**
|
|
4
10
|
|
|
5
11
|

|
|
6
12
|
|
|
7
|
-
The only browser MCP with **multi-session support** (10 concurrent AI sessions), **human-in-the-loop** (2FA, CAPTCHA, credentials), and **built-in provider integrations** (Stripe, HubSpot, Slack, and
|
|
13
|
+
The only browser MCP with **multi-session support** (10 concurrent AI sessions), **human-in-the-loop** (2FA, CAPTCHA, credentials), and **built-in provider integrations** (Stripe, HubSpot, Slack, and 6 more). 25 tools total.
|
|
8
14
|
|
|
9
15
|
## Install
|
|
10
16
|
|
|
@@ -20,7 +26,7 @@ Then load the extension in Chrome:
|
|
|
20
26
|
3. Click **Load unpacked** → select `~/.browser-mcp/extension/`
|
|
21
27
|
4. Restart Claude Code
|
|
22
28
|
|
|
23
|
-
That's it.
|
|
29
|
+
That's it. 25 browser tools are now available in Claude Code.
|
|
24
30
|
|
|
25
31
|
## Why This Over Playwright MCP / BrowserMCP?
|
|
26
32
|
|
|
@@ -30,14 +36,14 @@ That's it. 21 browser tools are now available in Claude Code.
|
|
|
30
36
|
| **Logins/cookies** | Already authenticated | Must log in every time | Already authenticated |
|
|
31
37
|
| **Multi-session** | 10 concurrent sessions with color-coded tab groups | Single session | Single session |
|
|
32
38
|
| **Human-in-the-loop** | `browser_ask_user` — 2FA, CAPTCHA, credential input | None | None |
|
|
33
|
-
| **Provider integrations** |
|
|
39
|
+
| **Provider integrations** | 9 built-in (Stripe, HubSpot, Slack...) | None | None |
|
|
34
40
|
| **CORS bypass** | `browser_fetch` from extension background | N/A | Limited |
|
|
35
41
|
| **Network monitoring** | `browser_wait_for_network` via CDP | Built-in | None |
|
|
36
42
|
| **CSP-strict sites** | Chrome Debugger API throughout | Works (headless) | Limited |
|
|
37
43
|
| **Custom dropdowns** | Angular Material, React Select support | Works (headless) | Limited |
|
|
38
44
|
| **Install** | `npx @agent360/browser-mcp install` | `npx @anthropic-ai/mcp-playwright` | Manual clone |
|
|
39
45
|
|
|
40
|
-
##
|
|
46
|
+
## 25 Tools
|
|
41
47
|
|
|
42
48
|
### Navigation & Content
|
|
43
49
|
| Tool | Description |
|
|
@@ -78,6 +84,11 @@ That's it. 21 browser tools are now available in Claude Code.
|
|
|
78
84
|
| `browser_wait_for_network` | Wait for specific API call to complete |
|
|
79
85
|
| `browser_extract_token` | Navigate to provider dashboard + extract API token |
|
|
80
86
|
|
|
87
|
+
### CAPTCHA Solving
|
|
88
|
+
| Tool | Description |
|
|
89
|
+
|------|-------------|
|
|
90
|
+
| `browser_solve_captcha` | Detect and solve CAPTCHAs. Auto-detects reCAPTCHA v2/v3, hCaptcha, Turnstile, FunCaptcha. Actions: `detect`, `click_checkbox` (auto-click, ~80% pass with Google login), `click_grid` (AI vision guided), `ask_human` (fallback) |
|
|
91
|
+
|
|
81
92
|
### Human-in-the-Loop
|
|
82
93
|
| Tool | Description |
|
|
83
94
|
|------|-------------|
|
|
@@ -112,8 +123,6 @@ Claude Session 3 ←(stdio)→ MCP :9878 ←(WS)→
|
|
|
112
123
|
| Mailchimp | `...-us1` | admin.mailchimp.com |
|
|
113
124
|
| Google | OAuth Client | console.cloud.google.com |
|
|
114
125
|
| LinkedIn | Client ID/Secret | linkedin.com/developers |
|
|
115
|
-
| Facebook | `EAA...` | developers.facebook.com |
|
|
116
|
-
| Instagram | Via Facebook | developers.facebook.com |
|
|
117
126
|
|
|
118
127
|
## Architecture
|
|
119
128
|
|
package/bin/cli.js
CHANGED
|
@@ -21,6 +21,8 @@ if (command === 'install') {
|
|
|
21
21
|
install();
|
|
22
22
|
} else if (!command) {
|
|
23
23
|
// No subcommand = start MCP server (Claude Code calls this)
|
|
24
|
+
// Auto-update extension files if installed via npx
|
|
25
|
+
autoUpdateExtension();
|
|
24
26
|
await import('../index.js');
|
|
25
27
|
} else {
|
|
26
28
|
console.log(`
|
|
@@ -75,16 +77,48 @@ function install() {
|
|
|
75
77
|
|
|
76
78
|
// 3. Print next steps
|
|
77
79
|
console.log(`
|
|
78
|
-
📋
|
|
79
|
-
1. Open Chrome
|
|
80
|
-
2.
|
|
81
|
-
3.
|
|
82
|
-
4.
|
|
83
|
-
5.
|
|
80
|
+
📋 First-time setup (one time only):
|
|
81
|
+
1. Open Chrome
|
|
82
|
+
2. Go to chrome://extensions (type it in the address bar)
|
|
83
|
+
3. Enable "Developer mode" (toggle in top right corner)
|
|
84
|
+
4. Click "Load unpacked" button (top left)
|
|
85
|
+
5. Navigate to and select this folder:
|
|
86
|
+
${extensionDir}
|
|
87
|
+
6. The extension "Agent360 Browser MCP" appears with a puzzle icon
|
|
88
|
+
7. Restart Claude Code — 28 browser tools are now available!
|
|
89
|
+
|
|
90
|
+
🔄 Auto-updates (fully automatic):
|
|
91
|
+
- MCP server: always fetches latest from npm (npx @latest)
|
|
92
|
+
- Extension files: auto-copied when npm version is newer
|
|
93
|
+
- Extension reload: auto-triggered via WebSocket
|
|
94
|
+
- You don't need to do anything — updates happen on every Claude Code session start
|
|
95
|
+
|
|
96
|
+
📖 Docs: https://browsermcp.dev
|
|
97
|
+
`);
|
|
98
|
+
}
|
|
84
99
|
|
|
85
|
-
|
|
86
|
-
|
|
100
|
+
function autoUpdateExtension() {
|
|
101
|
+
const home = homedir();
|
|
102
|
+
const extensionDir = join(home, '.browser-mcp', 'extension');
|
|
103
|
+
const sourceExtension = join(pkgRoot, 'extension');
|
|
87
104
|
|
|
88
|
-
|
|
89
|
-
|
|
105
|
+
if (!existsSync(extensionDir) || !existsSync(sourceExtension)) return;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
// Compare manifest versions
|
|
109
|
+
const installedManifest = join(extensionDir, 'manifest.json');
|
|
110
|
+
const sourceManifest = join(sourceExtension, 'manifest.json');
|
|
111
|
+
if (!existsSync(installedManifest)) return;
|
|
112
|
+
|
|
113
|
+
const installed = JSON.parse(readFileSync(installedManifest, 'utf8'));
|
|
114
|
+
const source = JSON.parse(readFileSync(sourceManifest, 'utf8'));
|
|
115
|
+
|
|
116
|
+
if (installed.version !== source.version) {
|
|
117
|
+
cpSync(sourceExtension, extensionDir, { recursive: true });
|
|
118
|
+
process.stderr.write(`[MCP] Extension auto-updated: ${installed.version} → ${source.version}\n`);
|
|
119
|
+
process.stderr.write('[MCP] Extension will auto-reload when connected\n');
|
|
120
|
+
// Signal to index.js that extension needs reload
|
|
121
|
+
process.env.BROWSER_MCP_EXTENSION_UPDATED = '1';
|
|
122
|
+
}
|
|
123
|
+
} catch {}
|
|
90
124
|
}
|
package/extension/background.js
CHANGED
|
@@ -118,9 +118,9 @@ function persistSessions() {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
// Get the active tab for this session (last navigated), or create one.
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
async function getSessionTab(port, activate =
|
|
121
|
+
// activate=false (default): runs in background — no focus stealing.
|
|
122
|
+
// activate=true: only for commands that NEED visible tab (screenshot, ask_user, navigate, execute_script).
|
|
123
|
+
async function getSessionTab(port, activate = false) {
|
|
124
124
|
const session = getSession(port);
|
|
125
125
|
let target = null;
|
|
126
126
|
|
|
@@ -255,9 +255,22 @@ async function debuggerClick(tabId, x, y) {
|
|
|
255
255
|
type: 'mouseReleased', x, y, button: 'left', clickCount: 1,
|
|
256
256
|
});
|
|
257
257
|
// 3. CDP doesn't synthesize 'click' event from mousePressed/mouseReleased.
|
|
258
|
-
//
|
|
258
|
+
// Fire JS click + React fiber lookup as backup for React SPAs.
|
|
259
259
|
await chrome.debugger.sendCommand({ tabId }, 'Runtime.evaluate', {
|
|
260
|
-
expression: `
|
|
260
|
+
expression: `(() => {
|
|
261
|
+
const el = document.elementFromPoint(${x}, ${y});
|
|
262
|
+
if (!el) return;
|
|
263
|
+
el.click();
|
|
264
|
+
// React fiber fallback — find and call onClick handler directly
|
|
265
|
+
const fiberKey = Object.keys(el).find(k => k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance'));
|
|
266
|
+
if (fiberKey) {
|
|
267
|
+
let fiber = el[fiberKey];
|
|
268
|
+
for (let i = 0; i < 10 && fiber; i++) {
|
|
269
|
+
if (fiber.memoizedProps?.onClick) { fiber.memoizedProps.onClick(new MouseEvent('click', {bubbles:true})); break; }
|
|
270
|
+
fiber = fiber.return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
})()`,
|
|
261
274
|
});
|
|
262
275
|
} finally {
|
|
263
276
|
await debuggerDetach(tabId);
|
|
@@ -632,7 +645,7 @@ async function dispatch(port, method, params) {
|
|
|
632
645
|
// Always reuse the active tab — navigate in place, don't create new tabs
|
|
633
646
|
// Only create new tab if explicitly requested via new_tab param
|
|
634
647
|
if (params.new_tab) {
|
|
635
|
-
tab = await chrome.tabs.create({ url: params.url, active:
|
|
648
|
+
tab = await chrome.tabs.create({ url: params.url, active: false });
|
|
636
649
|
await addTabToSession(port, tab.id);
|
|
637
650
|
} else {
|
|
638
651
|
await chrome.tabs.update(tab.id, { url: params.url });
|
|
@@ -762,7 +775,10 @@ async function dispatch(port, method, params) {
|
|
|
762
775
|
if (!el) return { ok: false, error: 'Element not found: ' + sel };
|
|
763
776
|
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
764
777
|
el.focus();
|
|
765
|
-
|
|
778
|
+
// Use nativeInputValueSetter to bypass React controlled input
|
|
779
|
+
const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
780
|
+
const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
|
|
781
|
+
if (setter) setter.call(el, val); else el.value = val;
|
|
766
782
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
767
783
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
768
784
|
return { ok: true };
|
|
@@ -827,16 +843,25 @@ async function dispatch(port, method, params) {
|
|
|
827
843
|
case 'scroll': {
|
|
828
844
|
const tab = await getSessionTab(port);
|
|
829
845
|
if (tab.url.startsWith('chrome://')) throw new Error('Cannot interact with chrome:// pages');
|
|
830
|
-
// Scroll to element
|
|
846
|
+
// Scroll to element
|
|
831
847
|
if (params.selector) {
|
|
832
848
|
const el = await resolveElement(tab.id, params.selector);
|
|
833
849
|
if (!el) return { ok: false, error: 'Element not found: ' + params.selector };
|
|
834
850
|
return { ok: true, scrolled_to: params.selector };
|
|
835
851
|
}
|
|
836
|
-
// Scroll by pixels
|
|
852
|
+
// Scroll by pixels using CDP mouseWheel — works in React containers with overflow:scroll
|
|
837
853
|
const dx = params.x || 0;
|
|
838
854
|
const dy = params.y || 0;
|
|
839
|
-
|
|
855
|
+
try {
|
|
856
|
+
await debuggerAttach(tab.id);
|
|
857
|
+
await chrome.debugger.sendCommand({ tabId: tab.id }, 'Input.dispatchMouseEvent', {
|
|
858
|
+
type: 'mouseWheel', x: 400, y: 300, deltaX: dx, deltaY: dy,
|
|
859
|
+
});
|
|
860
|
+
await debuggerDetach(tab.id);
|
|
861
|
+
} catch {
|
|
862
|
+
// Fallback to window.scrollBy for simple pages
|
|
863
|
+
await debuggerEval(tab.id, `window.scrollBy(${dx}, ${dy})`);
|
|
864
|
+
}
|
|
840
865
|
return { ok: true, scrolled: { x: dx, y: dy } };
|
|
841
866
|
}
|
|
842
867
|
|
|
@@ -1061,8 +1086,91 @@ async function dispatch(port, method, params) {
|
|
|
1061
1086
|
return { value, method: 'debugger' };
|
|
1062
1087
|
}
|
|
1063
1088
|
|
|
1064
|
-
case '
|
|
1089
|
+
case 'set_cookies': {
|
|
1090
|
+
const results = [];
|
|
1091
|
+
const cookieList = Array.isArray(params.cookies) ? params.cookies : [params];
|
|
1092
|
+
for (const c of cookieList) {
|
|
1093
|
+
try {
|
|
1094
|
+
const cookie = await chrome.cookies.set({
|
|
1095
|
+
url: c.url || `https://${c.domain}`,
|
|
1096
|
+
name: c.name,
|
|
1097
|
+
value: c.value,
|
|
1098
|
+
domain: c.domain,
|
|
1099
|
+
path: c.path || '/',
|
|
1100
|
+
secure: c.secure !== false,
|
|
1101
|
+
httpOnly: c.httpOnly || false,
|
|
1102
|
+
sameSite: c.sameSite || 'lax',
|
|
1103
|
+
});
|
|
1104
|
+
results.push({ ok: true, name: c.name });
|
|
1105
|
+
} catch (e) {
|
|
1106
|
+
results.push({ ok: false, name: c.name, error: e.message });
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return { results };
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
case 'set_local_storage': {
|
|
1113
|
+
const tab = await getSessionTab(port);
|
|
1114
|
+
if (tab.url.startsWith('chrome://')) throw new Error('Cannot access chrome:// pages');
|
|
1115
|
+
const key = params.key;
|
|
1116
|
+
const val = params.value;
|
|
1117
|
+
const expr = `localStorage.setItem(${JSON.stringify(key)}, ${JSON.stringify(val)})`;
|
|
1118
|
+
try {
|
|
1119
|
+
const scriptResult = await safeExecuteScript(tab.id, (k, v) => { localStorage.setItem(k, v); return { ok: true }; }, [key, val]);
|
|
1120
|
+
if (!scriptResult.cspBlocked) return scriptResult.result;
|
|
1121
|
+
} catch {}
|
|
1122
|
+
await debuggerEval(tab.id, expr);
|
|
1123
|
+
return { ok: true, method: 'debugger' };
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
case 'console_logs': {
|
|
1065
1127
|
const tab = await getSessionTab(port);
|
|
1128
|
+
const count = params.count || 50;
|
|
1129
|
+
try {
|
|
1130
|
+
await debuggerAttach(tab.id);
|
|
1131
|
+
await chrome.debugger.sendCommand({ tabId: tab.id }, 'Runtime.enable');
|
|
1132
|
+
// Collect console messages for a brief period
|
|
1133
|
+
const logs = [];
|
|
1134
|
+
const handler = (source, method, eventParams) => {
|
|
1135
|
+
if (source.tabId === tab.id && method === 'Runtime.consoleAPICalled') {
|
|
1136
|
+
logs.push({
|
|
1137
|
+
type: eventParams.type,
|
|
1138
|
+
text: eventParams.args?.map(a => a.value || a.description || '').join(' '),
|
|
1139
|
+
timestamp: eventParams.timestamp,
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
chrome.debugger.onEvent.addListener(handler);
|
|
1144
|
+
// Also grab existing console via page JS
|
|
1145
|
+
const { result } = await chrome.debugger.sendCommand({ tabId: tab.id }, 'Runtime.evaluate', {
|
|
1146
|
+
expression: `(() => {
|
|
1147
|
+
if (!window.__mcpConsoleLogs) {
|
|
1148
|
+
window.__mcpConsoleLogs = [];
|
|
1149
|
+
const orig = { log: console.log, warn: console.warn, error: console.error, info: console.info };
|
|
1150
|
+
for (const [type, fn] of Object.entries(orig)) {
|
|
1151
|
+
console[type] = (...args) => {
|
|
1152
|
+
window.__mcpConsoleLogs.push({ type, text: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '), ts: Date.now() });
|
|
1153
|
+
if (window.__mcpConsoleLogs.length > 200) window.__mcpConsoleLogs.shift();
|
|
1154
|
+
fn.apply(console, args);
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return JSON.stringify(window.__mcpConsoleLogs.slice(-${count}));
|
|
1159
|
+
})()`,
|
|
1160
|
+
returnByValue: true,
|
|
1161
|
+
});
|
|
1162
|
+
chrome.debugger.onEvent.removeListener(handler);
|
|
1163
|
+
await debuggerDetach(tab.id);
|
|
1164
|
+
const existing = JSON.parse(result.value || '[]');
|
|
1165
|
+
return { logs: [...existing, ...logs].slice(-count) };
|
|
1166
|
+
} catch (e) {
|
|
1167
|
+
try { await debuggerDetach(tab.id); } catch {}
|
|
1168
|
+
return { logs: [], error: e.message };
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
case 'ask_user': {
|
|
1173
|
+
const tab = await getSessionTab(port, true);
|
|
1066
1174
|
const timeout = params.timeout || 120000;
|
|
1067
1175
|
const fields = params.fields || [];
|
|
1068
1176
|
const hasFields = fields.length > 0;
|
|
@@ -1262,11 +1370,249 @@ async function dispatch(port, method, params) {
|
|
|
1262
1370
|
return { ok: true, remaining: session.tabIds.size };
|
|
1263
1371
|
}
|
|
1264
1372
|
|
|
1373
|
+
case 'solve_captcha': {
|
|
1374
|
+
const tab = await getSessionTab(port);
|
|
1375
|
+
const action = params.action || 'detect';
|
|
1376
|
+
|
|
1377
|
+
// ── Detect CAPTCHA on page ──
|
|
1378
|
+
if (action === 'detect') {
|
|
1379
|
+
const detection = await detectCaptcha(tab.id);
|
|
1380
|
+
return detection;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// ── Auto-click reCAPTCHA checkbox ──
|
|
1384
|
+
if (action === 'click_checkbox') {
|
|
1385
|
+
const result = await clickRecaptchaCheckbox(tab.id);
|
|
1386
|
+
// Wait for challenge or pass
|
|
1387
|
+
await new Promise(r => setTimeout(r, 2500));
|
|
1388
|
+
// Re-detect to see if it passed or image challenge appeared
|
|
1389
|
+
const after = await detectCaptcha(tab.id);
|
|
1390
|
+
return { ...result, after };
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// ── Click specific grid cells (AI vision guided) ──
|
|
1394
|
+
if (action === 'click_grid') {
|
|
1395
|
+
const cells = params.cells || [];
|
|
1396
|
+
if (!cells.length) return { error: 'No cells specified' };
|
|
1397
|
+
const result = await clickCaptchaGridCells(tab.id, cells);
|
|
1398
|
+
return result;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// ── Human fallback ──
|
|
1402
|
+
if (action === 'ask_human') {
|
|
1403
|
+
return { method: 'human', instructions: 'Call browser_ask_user with message: "A CAPTCHA needs to be solved. Please solve it in the browser and click Done when finished."' };
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
return { error: 'Unknown action: ' + action };
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
case 'reload_extension': {
|
|
1410
|
+
// MCP server signals that extension files were updated via npx
|
|
1411
|
+
// Reload after a short delay to allow response to be sent
|
|
1412
|
+
setTimeout(() => chrome.runtime.reload(), 500);
|
|
1413
|
+
return { ok: true, message: 'Extension reloading in 500ms' };
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1265
1416
|
default:
|
|
1266
1417
|
throw new Error('Unknown method: ' + method);
|
|
1267
1418
|
}
|
|
1268
1419
|
}
|
|
1269
1420
|
|
|
1421
|
+
// ── CAPTCHA Detection & Solving Helpers ─────────────────────────────────────
|
|
1422
|
+
|
|
1423
|
+
async function detectCaptcha(tabId) {
|
|
1424
|
+
try {
|
|
1425
|
+
await debuggerAttach(tabId);
|
|
1426
|
+
const { result } = await chrome.debugger.sendCommand({ tabId }, 'Runtime.evaluate', {
|
|
1427
|
+
expression: `(() => {
|
|
1428
|
+
const res = { found: false, types: [] };
|
|
1429
|
+
|
|
1430
|
+
// reCAPTCHA v2 — checkbox iframe
|
|
1431
|
+
const recaptchaAnchor = document.querySelector('iframe[src*="recaptcha/api2/anchor"], iframe[src*="recaptcha/enterprise/anchor"]');
|
|
1432
|
+
if (recaptchaAnchor) {
|
|
1433
|
+
res.found = true;
|
|
1434
|
+
res.types.push('recaptcha_v2_checkbox');
|
|
1435
|
+
const container = document.querySelector('.g-recaptcha');
|
|
1436
|
+
if (container) res.sitekey = container.getAttribute('data-sitekey');
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// reCAPTCHA v2 — image challenge iframe
|
|
1440
|
+
const recaptchaChallenge = document.querySelector('iframe[src*="recaptcha/api2/bframe"], iframe[src*="recaptcha/enterprise/bframe"]');
|
|
1441
|
+
if (recaptchaChallenge) {
|
|
1442
|
+
res.found = true;
|
|
1443
|
+
if (!res.types.includes('recaptcha_v2_checkbox')) res.types.push('recaptcha_v2_image');
|
|
1444
|
+
res.types.push('recaptcha_v2_challenge_visible');
|
|
1445
|
+
// Get iframe dimensions for grid clicking
|
|
1446
|
+
const rect = recaptchaChallenge.getBoundingClientRect();
|
|
1447
|
+
res.challengeFrame = { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// reCAPTCHA v3 — invisible badge
|
|
1451
|
+
const recaptchaV3 = document.querySelector('.grecaptcha-badge');
|
|
1452
|
+
if (recaptchaV3 && !recaptchaAnchor) {
|
|
1453
|
+
res.found = true;
|
|
1454
|
+
res.types.push('recaptcha_v3_invisible');
|
|
1455
|
+
res.note = 'reCAPTCHA v3 is invisible and score-based. Real Chrome with Google login usually passes automatically. No action needed.';
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// hCaptcha
|
|
1459
|
+
const hcaptcha = document.querySelector('iframe[src*="hcaptcha.com"], .h-captcha');
|
|
1460
|
+
if (hcaptcha) {
|
|
1461
|
+
res.found = true;
|
|
1462
|
+
res.types.push('hcaptcha');
|
|
1463
|
+
const container = document.querySelector('.h-captcha');
|
|
1464
|
+
if (container) res.sitekey = container.getAttribute('data-sitekey');
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// Cloudflare Turnstile
|
|
1468
|
+
const turnstile = document.querySelector('iframe[src*="challenges.cloudflare.com"], .cf-turnstile');
|
|
1469
|
+
if (turnstile) {
|
|
1470
|
+
res.found = true;
|
|
1471
|
+
res.types.push('cloudflare_turnstile');
|
|
1472
|
+
const container = document.querySelector('.cf-turnstile');
|
|
1473
|
+
if (container) res.sitekey = container.getAttribute('data-sitekey');
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// Cloudflare challenge page (5-second interstitial)
|
|
1477
|
+
if (document.title.includes('Just a moment') || document.querySelector('#challenge-running')) {
|
|
1478
|
+
res.found = true;
|
|
1479
|
+
res.types.push('cloudflare_challenge_page');
|
|
1480
|
+
res.note = 'Cloudflare challenge page. Wait 5-10 seconds — real Chrome usually passes automatically.';
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// FunCaptcha / Arkose Labs
|
|
1484
|
+
const funcaptcha = document.querySelector('#FunCaptcha, iframe[src*="funcaptcha"], iframe[src*="arkoselabs"]');
|
|
1485
|
+
if (funcaptcha) {
|
|
1486
|
+
res.found = true;
|
|
1487
|
+
res.types.push('funcaptcha');
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
if (!res.found) res.note = 'No CAPTCHA detected on this page.';
|
|
1491
|
+
res.pageUrl = window.location.href;
|
|
1492
|
+
return JSON.stringify(res);
|
|
1493
|
+
})()`,
|
|
1494
|
+
returnByValue: true,
|
|
1495
|
+
});
|
|
1496
|
+
await debuggerDetach(tabId);
|
|
1497
|
+
return JSON.parse(result.value);
|
|
1498
|
+
} catch (e) {
|
|
1499
|
+
try { await debuggerDetach(tabId); } catch {}
|
|
1500
|
+
return { found: false, error: e.message };
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
async function clickRecaptchaCheckbox(tabId) {
|
|
1505
|
+
try {
|
|
1506
|
+
await debuggerAttach(tabId);
|
|
1507
|
+
// Find the reCAPTCHA anchor iframe position
|
|
1508
|
+
const { result } = await chrome.debugger.sendCommand({ tabId }, 'Runtime.evaluate', {
|
|
1509
|
+
expression: `(() => {
|
|
1510
|
+
const iframe = document.querySelector('iframe[src*="recaptcha/api2/anchor"], iframe[src*="recaptcha/enterprise/anchor"]');
|
|
1511
|
+
if (!iframe) return JSON.stringify({ found: false });
|
|
1512
|
+
const rect = iframe.getBoundingClientRect();
|
|
1513
|
+
// Checkbox is roughly at 27,30 inside the iframe (standard reCAPTCHA layout)
|
|
1514
|
+
return JSON.stringify({ found: true, x: rect.x + 27, y: rect.y + 30 });
|
|
1515
|
+
})()`,
|
|
1516
|
+
returnByValue: true,
|
|
1517
|
+
});
|
|
1518
|
+
const pos = JSON.parse(result.value);
|
|
1519
|
+
if (!pos.found) {
|
|
1520
|
+
await debuggerDetach(tabId);
|
|
1521
|
+
return { clicked: false, reason: 'reCAPTCHA checkbox iframe not found' };
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// Click the checkbox using real mouse events
|
|
1525
|
+
await chrome.debugger.sendCommand({ tabId }, 'Input.dispatchMouseEvent', {
|
|
1526
|
+
type: 'mouseMoved', x: pos.x, y: pos.y,
|
|
1527
|
+
});
|
|
1528
|
+
await new Promise(r => setTimeout(r, 100 + Math.random() * 200));
|
|
1529
|
+
await chrome.debugger.sendCommand({ tabId }, 'Input.dispatchMouseEvent', {
|
|
1530
|
+
type: 'mousePressed', x: pos.x, y: pos.y, button: 'left', clickCount: 1,
|
|
1531
|
+
});
|
|
1532
|
+
await chrome.debugger.sendCommand({ tabId }, 'Input.dispatchMouseEvent', {
|
|
1533
|
+
type: 'mouseReleased', x: pos.x, y: pos.y, button: 'left', clickCount: 1,
|
|
1534
|
+
});
|
|
1535
|
+
await debuggerDetach(tabId);
|
|
1536
|
+
return { clicked: true, position: pos, note: 'Checkbox clicked. Wait 2-3 seconds then re-detect to check if passed or image challenge appeared.' };
|
|
1537
|
+
} catch (e) {
|
|
1538
|
+
try { await debuggerDetach(tabId); } catch {}
|
|
1539
|
+
return { clicked: false, error: e.message };
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
async function clickCaptchaGridCells(tabId, cells) {
|
|
1544
|
+
try {
|
|
1545
|
+
await debuggerAttach(tabId);
|
|
1546
|
+
// Find the challenge iframe position and dimensions
|
|
1547
|
+
const { result } = await chrome.debugger.sendCommand({ tabId }, 'Runtime.evaluate', {
|
|
1548
|
+
expression: `(() => {
|
|
1549
|
+
const iframe = document.querySelector('iframe[src*="recaptcha/api2/bframe"], iframe[src*="recaptcha/enterprise/bframe"]');
|
|
1550
|
+
if (!iframe) return JSON.stringify({ found: false });
|
|
1551
|
+
const rect = iframe.getBoundingClientRect();
|
|
1552
|
+
return JSON.stringify({ found: true, x: rect.x, y: rect.y, width: rect.width, height: rect.height });
|
|
1553
|
+
})()`,
|
|
1554
|
+
returnByValue: true,
|
|
1555
|
+
});
|
|
1556
|
+
const frame = JSON.parse(result.value);
|
|
1557
|
+
if (!frame.found) {
|
|
1558
|
+
await debuggerDetach(tabId);
|
|
1559
|
+
return { clicked: false, reason: 'Challenge iframe not found. Take a screenshot to verify CAPTCHA state.' };
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// Determine grid size — reCAPTCHA uses 3x3 or 4x4 grids
|
|
1563
|
+
// The image grid starts ~100px from top of iframe, and is roughly square
|
|
1564
|
+
const gridTop = frame.y + 100;
|
|
1565
|
+
const gridLeft = frame.x + 14;
|
|
1566
|
+
const gridSize = frame.width - 28; // padding on each side
|
|
1567
|
+
const cols = cells.some(c => c >= 9) ? 4 : 3;
|
|
1568
|
+
const rows = cols;
|
|
1569
|
+
const cellSize = gridSize / cols;
|
|
1570
|
+
|
|
1571
|
+
const maxCell = cols * rows - 1;
|
|
1572
|
+
const validCells = cells.filter(c => c >= 0 && c <= maxCell);
|
|
1573
|
+
if (!validCells.length) {
|
|
1574
|
+
await debuggerDetach(tabId);
|
|
1575
|
+
return { clicked: false, error: `All cell indices out of bounds. Grid is ${cols}x${rows}, valid range: 0-${maxCell}` };
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
const clicked = [];
|
|
1579
|
+
for (const cell of validCells) {
|
|
1580
|
+
const row = Math.floor(cell / cols);
|
|
1581
|
+
const col = cell % cols;
|
|
1582
|
+
const x = Math.round(gridLeft + col * cellSize + cellSize / 2);
|
|
1583
|
+
const y = Math.round(gridTop + row * cellSize + cellSize / 2);
|
|
1584
|
+
|
|
1585
|
+
// Human-like click with small random offset
|
|
1586
|
+
const ox = x + Math.round((Math.random() - 0.5) * cellSize * 0.3);
|
|
1587
|
+
const oy = y + Math.round((Math.random() - 0.5) * cellSize * 0.3);
|
|
1588
|
+
|
|
1589
|
+
await chrome.debugger.sendCommand({ tabId }, 'Input.dispatchMouseEvent', {
|
|
1590
|
+
type: 'mouseMoved', x: ox, y: oy,
|
|
1591
|
+
});
|
|
1592
|
+
await new Promise(r => setTimeout(r, 150 + Math.random() * 300));
|
|
1593
|
+
await chrome.debugger.sendCommand({ tabId }, 'Input.dispatchMouseEvent', {
|
|
1594
|
+
type: 'mousePressed', x: ox, y: oy, button: 'left', clickCount: 1,
|
|
1595
|
+
});
|
|
1596
|
+
await chrome.debugger.sendCommand({ tabId }, 'Input.dispatchMouseEvent', {
|
|
1597
|
+
type: 'mouseReleased', x: ox, y: oy, button: 'left', clickCount: 1,
|
|
1598
|
+
});
|
|
1599
|
+
await new Promise(r => setTimeout(r, 200 + Math.random() * 400));
|
|
1600
|
+
clicked.push({ cell, row, col, x: ox, y: oy });
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
await debuggerDetach(tabId);
|
|
1604
|
+
return {
|
|
1605
|
+
clicked: true,
|
|
1606
|
+
cells: clicked,
|
|
1607
|
+
grid: `${cols}x${rows}`,
|
|
1608
|
+
note: 'Cells clicked. Take a screenshot to verify, then click the "Verify" / "Skip" button if needed.',
|
|
1609
|
+
};
|
|
1610
|
+
} catch (e) {
|
|
1611
|
+
try { await debuggerDetach(tabId); } catch {}
|
|
1612
|
+
return { clicked: false, error: e.message };
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1270
1616
|
// ── Start ───────────────────────────────────────────────────────────────────
|
|
1271
1617
|
ensureOffscreen().catch(console.error);
|
|
1272
1618
|
|
package/extension/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Agent360 Browser MCP",
|
|
4
|
-
"version": "1.
|
|
5
|
-
"description": "
|
|
4
|
+
"version": "1.15.0",
|
|
5
|
+
"description": "Control your real Chrome from Claude Code — navigate, click, fill, screenshot, solve CAPTCHAs. 24 tools, multi-session, human-in-the-loop.",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"tabs",
|
|
8
8
|
"tabGroups",
|
package/extension/popup.html
CHANGED
|
@@ -44,21 +44,21 @@
|
|
|
44
44
|
|
|
45
45
|
<div class="status">
|
|
46
46
|
<div class="dot off" id="dot"></div>
|
|
47
|
-
<span class="label" id="label">
|
|
47
|
+
<span class="label" id="label">Checking...</span>
|
|
48
48
|
</div>
|
|
49
49
|
|
|
50
50
|
<div class="section-title">Sessions</div>
|
|
51
51
|
<div id="sessions" class="sessions">
|
|
52
|
-
<div class="empty">
|
|
52
|
+
<div class="empty">No active sessions</div>
|
|
53
53
|
</div>
|
|
54
54
|
|
|
55
55
|
<div class="section-title">Action Log</div>
|
|
56
56
|
<div class="log" id="log">
|
|
57
|
-
<div class="empty">
|
|
57
|
+
<div class="empty">No actions yet</div>
|
|
58
58
|
</div>
|
|
59
59
|
|
|
60
60
|
<button id="reconnect">Reconnect</button>
|
|
61
|
-
<button id="clearLog" style="margin-top:4px">
|
|
61
|
+
<button id="clearLog" style="margin-top:4px">Clear log</button>
|
|
62
62
|
|
|
63
63
|
<script src="popup.js"></script>
|
|
64
64
|
</body>
|
package/index.js
CHANGED
|
@@ -78,6 +78,15 @@ function createWSS(port = BASE_PORT) {
|
|
|
78
78
|
extensionSocket = ws;
|
|
79
79
|
process.stderr.write(`[MCP] Chrome extension connected on port ${port}\n`);
|
|
80
80
|
|
|
81
|
+
// If extension was auto-updated, trigger reload
|
|
82
|
+
if (process.env.BROWSER_MCP_EXTENSION_UPDATED === '1') {
|
|
83
|
+
process.env.BROWSER_MCP_EXTENSION_UPDATED = '';
|
|
84
|
+
process.stderr.write('[MCP] Extension files updated — triggering auto-reload\n');
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
sendToExtension('reload_extension', {}, 5000).catch(() => {});
|
|
87
|
+
}, 1000);
|
|
88
|
+
}
|
|
89
|
+
|
|
81
90
|
ws.on('message', (data) => {
|
|
82
91
|
let msg;
|
|
83
92
|
try { msg = JSON.parse(data.toString()); } catch { return; }
|
|
@@ -179,9 +188,14 @@ const INSTRUCTIONS = `You control the user's real Chrome browser via this MCP se
|
|
|
179
188
|
- browser_press_key("a", ctrl=true) — select all
|
|
180
189
|
|
|
181
190
|
## CAPTCHA handling
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
191
|
+
Use browser_solve_captcha to detect and solve CAPTCHAs automatically:
|
|
192
|
+
1. Call browser_solve_captcha() — detects CAPTCHA type on page
|
|
193
|
+
2. If reCAPTCHA v2 checkbox found → call browser_solve_captcha(action="click_checkbox") — auto-clicks, passes ~80% with logged-in Google
|
|
194
|
+
3. If image challenge appears → call browser_screenshot, analyze the grid visually, then call browser_solve_captcha(action="click_grid", cells=[2,5,7]) with the correct cell indices
|
|
195
|
+
4. If all else fails → call browser_solve_captcha(action="ask_human") to show overlay to user
|
|
196
|
+
5. After solving, retry the action that was blocked
|
|
197
|
+
|
|
198
|
+
For image grid challenges: cells are 0-indexed, left-to-right, top-to-bottom. A 3x3 grid has cells 0-8. A 4x4 grid has cells 0-15.
|
|
185
199
|
|
|
186
200
|
## OAuth popups
|
|
187
201
|
- OAuth popups (Google, Microsoft, GitHub, Slack, HubSpot) are automatically intercepted and added to your session's tab group
|
|
@@ -205,7 +219,7 @@ If the extension files were updated, ask the user to reload it:
|
|
|
205
219
|
You cannot navigate to chrome:// pages — the user must do this manually.`;
|
|
206
220
|
|
|
207
221
|
const mcpServer = new Server(
|
|
208
|
-
{ name: 'agent360-browser', version: '1.
|
|
222
|
+
{ name: 'agent360-browser', version: '1.15.0' },
|
|
209
223
|
{ capabilities: { tools: {} } },
|
|
210
224
|
{ instructions: INSTRUCTIONS },
|
|
211
225
|
);
|
|
@@ -243,6 +257,10 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
243
257
|
browser_get_new_tab: 'get_new_tab',
|
|
244
258
|
browser_switch_tab: 'switch_tab',
|
|
245
259
|
browser_close_tab: 'close_tab',
|
|
260
|
+
browser_set_cookies: 'set_cookies',
|
|
261
|
+
browser_set_local_storage: 'set_local_storage',
|
|
262
|
+
browser_console_logs: 'console_logs',
|
|
263
|
+
browser_solve_captcha: 'solve_captcha',
|
|
246
264
|
};
|
|
247
265
|
|
|
248
266
|
if (name === 'browser_extract_token') {
|
|
@@ -254,7 +272,8 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
254
272
|
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
|
|
255
273
|
}
|
|
256
274
|
|
|
257
|
-
const timeout = method === 'ask_user' ? (args?.timeout || 120000) + 5000 :
|
|
275
|
+
const timeout = method === 'ask_user' ? (args?.timeout || 120000) + 5000 :
|
|
276
|
+
method === 'solve_captcha' ? 60000 : 30000;
|
|
258
277
|
const result = await sendToExtension(method, args || {}, timeout);
|
|
259
278
|
|
|
260
279
|
if (name === 'browser_screenshot' && result?.image) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent360/browser-mcp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Browser MCP — control your real Chrome from Claude Code.
|
|
3
|
+
"version": "1.15.0",
|
|
4
|
+
"description": "Browser MCP — control your real Chrome from Claude Code. 28 tools, CAPTCHA solving, multi-session, human-in-the-loop.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
@@ -19,18 +19,29 @@
|
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
21
21
|
"mcp",
|
|
22
|
+
"mcp-server",
|
|
23
|
+
"model-context-protocol",
|
|
22
24
|
"browser",
|
|
25
|
+
"browser-mcp",
|
|
26
|
+
"browser-automation",
|
|
23
27
|
"chrome",
|
|
28
|
+
"chrome-extension",
|
|
24
29
|
"claude",
|
|
25
30
|
"claude-code",
|
|
26
|
-
"automation",
|
|
27
31
|
"ai-agent",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
32
|
+
"captcha",
|
|
33
|
+
"captcha-solver",
|
|
34
|
+
"human-in-the-loop",
|
|
35
|
+
"web-automation",
|
|
36
|
+
"playwright-alternative",
|
|
37
|
+
"selenium-alternative"
|
|
30
38
|
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
31
42
|
"author": "Agent360 <hello@agent360.dk>",
|
|
32
43
|
"license": "MIT",
|
|
33
|
-
"homepage": "https://
|
|
44
|
+
"homepage": "https://browsermcp.dev",
|
|
34
45
|
"repository": {
|
|
35
46
|
"type": "git",
|
|
36
47
|
"url": "https://github.com/Agent360dk/browser-mcp.git"
|
package/tools.js
CHANGED
|
@@ -194,6 +194,47 @@ export const TOOLS = [
|
|
|
194
194
|
},
|
|
195
195
|
},
|
|
196
196
|
},
|
|
197
|
+
{
|
|
198
|
+
name: 'browser_set_cookies',
|
|
199
|
+
description: 'Set one or more cookies for a domain.',
|
|
200
|
+
inputSchema: {
|
|
201
|
+
type: 'object',
|
|
202
|
+
properties: {
|
|
203
|
+
name: { type: 'string', description: 'Cookie name' },
|
|
204
|
+
value: { type: 'string', description: 'Cookie value' },
|
|
205
|
+
domain: { type: 'string', description: 'Cookie domain (e.g. ".example.com")' },
|
|
206
|
+
url: { type: 'string', description: 'URL for the cookie (alternative to domain)' },
|
|
207
|
+
path: { type: 'string', description: 'Cookie path (default: /)' },
|
|
208
|
+
secure: { type: 'boolean', description: 'Secure flag (default: true)' },
|
|
209
|
+
httpOnly: { type: 'boolean', description: 'HttpOnly flag (default: false)' },
|
|
210
|
+
sameSite: { type: 'string', enum: ['no_restriction', 'lax', 'strict'], description: 'SameSite attribute (default: lax)' },
|
|
211
|
+
cookies: { type: 'array', description: 'Array of cookie objects to set multiple at once', items: { type: 'object' } },
|
|
212
|
+
},
|
|
213
|
+
required: ['name', 'value'],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'browser_set_local_storage',
|
|
218
|
+
description: 'Set a localStorage key-value pair on the current page.',
|
|
219
|
+
inputSchema: {
|
|
220
|
+
type: 'object',
|
|
221
|
+
properties: {
|
|
222
|
+
key: { type: 'string', description: 'localStorage key to set' },
|
|
223
|
+
value: { type: 'string', description: 'Value to store (string)' },
|
|
224
|
+
},
|
|
225
|
+
required: ['key', 'value'],
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: 'browser_console_logs',
|
|
230
|
+
description: 'Get recent console.log/warn/error messages from the page. Installs a lightweight interceptor on first call. Returns the last N console messages.',
|
|
231
|
+
inputSchema: {
|
|
232
|
+
type: 'object',
|
|
233
|
+
properties: {
|
|
234
|
+
count: { type: 'number', description: 'Number of recent messages to return (default: 50)' },
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
197
238
|
{
|
|
198
239
|
name: 'browser_ask_user',
|
|
199
240
|
description: 'Show an overlay dialog asking the user to perform an action or provide information (credentials, 2FA, CAPTCHA, OAuth consent). Can include input fields for the user to fill in. Returns user responses.',
|
|
@@ -276,6 +317,21 @@ export const TOOLS = [
|
|
|
276
317
|
required: ['provider'],
|
|
277
318
|
},
|
|
278
319
|
},
|
|
320
|
+
{
|
|
321
|
+
name: 'browser_solve_captcha',
|
|
322
|
+
description: 'Detect and solve CAPTCHAs on the current page. Auto-detects reCAPTCHA v2/v3, hCaptcha, Cloudflare Turnstile, and FunCaptcha. Tries auto-click first (works ~80% with logged-in Google), then returns a screenshot for AI vision analysis, then falls back to asking the user. Returns detection info and solving status.',
|
|
323
|
+
inputSchema: {
|
|
324
|
+
type: 'object',
|
|
325
|
+
properties: {
|
|
326
|
+
action: { type: 'string', enum: ['detect', 'click_checkbox', 'click_grid', 'ask_human'], description: 'Action to take. "detect" scans for CAPTCHAs. "click_checkbox" clicks the reCAPTCHA checkbox. "click_grid" clicks specific grid cells (pass cells param). "ask_human" shows overlay to user. Default: "detect"' },
|
|
327
|
+
cells: {
|
|
328
|
+
type: 'array',
|
|
329
|
+
items: { type: 'number' },
|
|
330
|
+
description: 'Grid cell indices to click (0-indexed, left-to-right, top-to-bottom) for image challenges. E.g. [2, 5, 7] to click cells 3, 6, 8.',
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
279
335
|
];
|
|
280
336
|
|
|
281
337
|
// Known provider token pages for browser_extract_token
|