@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 CHANGED
@@ -1,10 +1,16 @@
1
1
  # Browser MCP by Agent360
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@agent360/browser-mcp)](https://www.npmjs.com/package/@agent360/browser-mcp)
4
+ [![npm downloads](https://img.shields.io/npm/dw/@agent360/browser-mcp)](https://www.npmjs.com/package/@agent360/browser-mcp)
5
+ [![GitHub stars](https://img.shields.io/github/stars/Agent360dk/browser-mcp)](https://github.com/Agent360dk/browser-mcp)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![MCP](https://img.shields.io/badge/MCP-compatible-blue)](https://modelcontextprotocol.io)
8
+
3
9
  **Control your real Chrome from Claude Code — with your logins, cookies, and 2FA.**
4
10
 
5
11
  ![Browser MCP Demo](assets/demo.gif)
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 9 more).
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. 21 browser tools are now available in Claude Code.
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** | 12 built-in (Stripe, HubSpot, Slack...) | None | None |
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
- ## 21 Tools
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
- 📋 Next steps:
79
- 1. Open Chrome → chrome://extensions
80
- 2. Enable "Developer mode" (top right toggle)
81
- 3. Click "Load unpacked"
82
- 4. Select: ${extensionDir}
83
- 5. Restart Claude Code browser tools are now available
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
- 🔄 Auto-updates: The MCP server always uses the latest npm version.
86
- Extension updates: re-run "npx @agent360/browser-mcp install"
100
+ function autoUpdateExtension() {
101
+ const home = homedir();
102
+ const extensionDir = join(home, '.browser-mcp', 'extension');
103
+ const sourceExtension = join(pkgRoot, 'extension');
87
104
 
88
- 📖 Docs: https://github.com/Agent360dk/browser-mcp
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
  }
@@ -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
- // IMPORTANT: Also activates the tab so Chrome APIs (captureVisibleTab,
122
- // executeScript) operate on the correct tab, not whatever the user is viewing.
123
- async function getSessionTab(port, activate = true) {
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
- // React/Angular listen on 'click', not mouseup. Fire it via JS as backup.
258
+ // Fire JS click + React fiber lookup as backup for React SPAs.
259
259
  await chrome.debugger.sendCommand({ tabId }, 'Runtime.evaluate', {
260
- expression: `document.elementFromPoint(${x}, ${y})?.click()`,
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: true });
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
- el.value = val;
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 or by pixels
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
- await debuggerEval(tab.id, `window.scrollBy(${dx}, ${dy})`);
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 'ask_user': {
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
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Agent360 Browser MCP",
4
- "version": "1.13.0",
5
- "description": "Lader Claude Code styre din browserforbind tjenester, hent tokens, automatisér workflows. Multi-session support.",
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",
@@ -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">Tjekker...</span>
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">Ingen aktive sessions</div>
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">Ingen actions endnu</div>
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">Ryd log</button>
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
- - After navigate, check the response for captcha_detected field
183
- - If captcha_detected is set, use browser_ask_user: "A CAPTCHA was detected on this page. Please solve it in the browser, then click Done."
184
- - After user solves it, retry the navigation or continue with the page
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.12.0' },
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 : 30000;
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.13.0",
4
- "description": "Browser MCP — control your real Chrome from Claude Code. Multi-session, human-in-the-loop, 21 tools.",
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
- "browser-automation",
29
- "model-context-protocol"
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://github.com/Agent360dk/browser-mcp",
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