@agent360/browser-mcp 1.13.0 → 1.16.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,35 @@ 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/Angular framework fallbacks.
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
+
265
+ // React fiber fallback — find and call onClick handler directly
266
+ const fiberKey = Object.keys(el).find(k => k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance'));
267
+ if (fiberKey) {
268
+ let fiber = el[fiberKey];
269
+ for (let i = 0; i < 10 && fiber; i++) {
270
+ if (fiber.memoizedProps?.onClick) { fiber.memoizedProps.onClick(new MouseEvent('click', {bubbles:true})); break; }
271
+ fiber = fiber.return;
272
+ }
273
+ }
274
+
275
+ // Angular fallback — trigger via Zone.js patched events + ngClick
276
+ const ngKey = Object.keys(el).find(k => k.startsWith('__ng'));
277
+ if (ngKey || el.getAttribute('ng-click') || el.getAttribute('(click)')) {
278
+ // Angular uses Zone.js which patches addEventListener — dispatch real events through it
279
+ el.dispatchEvent(new PointerEvent('pointerdown', {bubbles:true, cancelable:true}));
280
+ el.dispatchEvent(new PointerEvent('pointerup', {bubbles:true, cancelable:true}));
281
+ el.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, view:window}));
282
+ // Angular Material components (mat-button, mat-checkbox, etc.) use ripple + internal handlers
283
+ const matRipple = el.closest('[mat-button], [mat-raised-button], [mat-icon-button], [mat-fab], mat-checkbox, mat-slide-toggle, mat-radio-button');
284
+ if (matRipple) matRipple.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, view:window}));
285
+ }
286
+ })()`,
261
287
  });
262
288
  } finally {
263
289
  await debuggerDetach(tabId);
@@ -632,7 +658,7 @@ async function dispatch(port, method, params) {
632
658
  // Always reuse the active tab — navigate in place, don't create new tabs
633
659
  // Only create new tab if explicitly requested via new_tab param
634
660
  if (params.new_tab) {
635
- tab = await chrome.tabs.create({ url: params.url, active: true });
661
+ tab = await chrome.tabs.create({ url: params.url, active: false });
636
662
  await addTabToSession(port, tab.id);
637
663
  } else {
638
664
  await chrome.tabs.update(tab.id, { url: params.url });
@@ -658,9 +684,9 @@ async function dispatch(port, method, params) {
658
684
  // Check for CAPTCHA after navigation
659
685
  const captcha = await detectCaptcha(tab.id);
660
686
  const result = { title: updated.title, url: updated.url, tab_id: tab.id, session: session.label };
661
- if (captcha) {
662
- result.captcha_detected = captcha;
663
- result.hint = `CAPTCHA (${captcha}) detected. Use browser_ask_user to ask the user to solve it, then retry.`;
687
+ if (captcha && captcha.found) {
688
+ result.captcha_detected = captcha.types.join(', ');
689
+ result.hint = `CAPTCHA detected: ${captcha.types.join(', ')}. Use browser_solve_captcha to handle it.`;
664
690
  }
665
691
  return result;
666
692
  }
@@ -762,7 +788,10 @@ async function dispatch(port, method, params) {
762
788
  if (!el) return { ok: false, error: 'Element not found: ' + sel };
763
789
  el.scrollIntoView({ block: 'center', behavior: 'instant' });
764
790
  el.focus();
765
- el.value = val;
791
+ // Use nativeInputValueSetter to bypass React controlled input
792
+ const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
793
+ const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
794
+ if (setter) setter.call(el, val); else el.value = val;
766
795
  el.dispatchEvent(new Event('input', { bubbles: true }));
767
796
  el.dispatchEvent(new Event('change', { bubbles: true }));
768
797
  return { ok: true };
@@ -827,16 +856,25 @@ async function dispatch(port, method, params) {
827
856
  case 'scroll': {
828
857
  const tab = await getSessionTab(port);
829
858
  if (tab.url.startsWith('chrome://')) throw new Error('Cannot interact with chrome:// pages');
830
- // Scroll to element or by pixels
859
+ // Scroll to element
831
860
  if (params.selector) {
832
861
  const el = await resolveElement(tab.id, params.selector);
833
862
  if (!el) return { ok: false, error: 'Element not found: ' + params.selector };
834
863
  return { ok: true, scrolled_to: params.selector };
835
864
  }
836
- // Scroll by pixels
865
+ // Scroll by pixels using CDP mouseWheel — works in React containers with overflow:scroll
837
866
  const dx = params.x || 0;
838
867
  const dy = params.y || 0;
839
- await debuggerEval(tab.id, `window.scrollBy(${dx}, ${dy})`);
868
+ try {
869
+ await debuggerAttach(tab.id);
870
+ await chrome.debugger.sendCommand({ tabId: tab.id }, 'Input.dispatchMouseEvent', {
871
+ type: 'mouseWheel', x: 400, y: 300, deltaX: dx, deltaY: dy,
872
+ });
873
+ await debuggerDetach(tab.id);
874
+ } catch {
875
+ // Fallback to window.scrollBy for simple pages
876
+ await debuggerEval(tab.id, `window.scrollBy(${dx}, ${dy})`);
877
+ }
840
878
  return { ok: true, scrolled: { x: dx, y: dy } };
841
879
  }
842
880
 
@@ -1061,8 +1099,91 @@ async function dispatch(port, method, params) {
1061
1099
  return { value, method: 'debugger' };
1062
1100
  }
1063
1101
 
1064
- case 'ask_user': {
1102
+ case 'set_cookies': {
1103
+ const results = [];
1104
+ const cookieList = Array.isArray(params.cookies) ? params.cookies : [params];
1105
+ for (const c of cookieList) {
1106
+ try {
1107
+ const cookie = await chrome.cookies.set({
1108
+ url: c.url || `https://${c.domain}`,
1109
+ name: c.name,
1110
+ value: c.value,
1111
+ domain: c.domain,
1112
+ path: c.path || '/',
1113
+ secure: c.secure !== false,
1114
+ httpOnly: c.httpOnly || false,
1115
+ sameSite: c.sameSite || 'lax',
1116
+ });
1117
+ results.push({ ok: true, name: c.name });
1118
+ } catch (e) {
1119
+ results.push({ ok: false, name: c.name, error: e.message });
1120
+ }
1121
+ }
1122
+ return { results };
1123
+ }
1124
+
1125
+ case 'set_local_storage': {
1126
+ const tab = await getSessionTab(port);
1127
+ if (tab.url.startsWith('chrome://')) throw new Error('Cannot access chrome:// pages');
1128
+ const key = params.key;
1129
+ const val = params.value;
1130
+ const expr = `localStorage.setItem(${JSON.stringify(key)}, ${JSON.stringify(val)})`;
1131
+ try {
1132
+ const scriptResult = await safeExecuteScript(tab.id, (k, v) => { localStorage.setItem(k, v); return { ok: true }; }, [key, val]);
1133
+ if (!scriptResult.cspBlocked) return scriptResult.result;
1134
+ } catch {}
1135
+ await debuggerEval(tab.id, expr);
1136
+ return { ok: true, method: 'debugger' };
1137
+ }
1138
+
1139
+ case 'console_logs': {
1065
1140
  const tab = await getSessionTab(port);
1141
+ const count = params.count || 50;
1142
+ try {
1143
+ await debuggerAttach(tab.id);
1144
+ await chrome.debugger.sendCommand({ tabId: tab.id }, 'Runtime.enable');
1145
+ // Collect console messages for a brief period
1146
+ const logs = [];
1147
+ const handler = (source, method, eventParams) => {
1148
+ if (source.tabId === tab.id && method === 'Runtime.consoleAPICalled') {
1149
+ logs.push({
1150
+ type: eventParams.type,
1151
+ text: eventParams.args?.map(a => a.value || a.description || '').join(' '),
1152
+ timestamp: eventParams.timestamp,
1153
+ });
1154
+ }
1155
+ };
1156
+ chrome.debugger.onEvent.addListener(handler);
1157
+ // Also grab existing console via page JS
1158
+ const { result } = await chrome.debugger.sendCommand({ tabId: tab.id }, 'Runtime.evaluate', {
1159
+ expression: `(() => {
1160
+ if (!window.__mcpConsoleLogs) {
1161
+ window.__mcpConsoleLogs = [];
1162
+ const orig = { log: console.log, warn: console.warn, error: console.error, info: console.info };
1163
+ for (const [type, fn] of Object.entries(orig)) {
1164
+ console[type] = (...args) => {
1165
+ window.__mcpConsoleLogs.push({ type, text: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '), ts: Date.now() });
1166
+ if (window.__mcpConsoleLogs.length > 200) window.__mcpConsoleLogs.shift();
1167
+ fn.apply(console, args);
1168
+ };
1169
+ }
1170
+ }
1171
+ return JSON.stringify(window.__mcpConsoleLogs.slice(-${count}));
1172
+ })()`,
1173
+ returnByValue: true,
1174
+ });
1175
+ chrome.debugger.onEvent.removeListener(handler);
1176
+ await debuggerDetach(tab.id);
1177
+ const existing = JSON.parse(result.value || '[]');
1178
+ return { logs: [...existing, ...logs].slice(-count) };
1179
+ } catch (e) {
1180
+ try { await debuggerDetach(tab.id); } catch {}
1181
+ return { logs: [], error: e.message };
1182
+ }
1183
+ }
1184
+
1185
+ case 'ask_user': {
1186
+ const tab = await getSessionTab(port, true);
1066
1187
  const timeout = params.timeout || 120000;
1067
1188
  const fields = params.fields || [];
1068
1189
  const hasFields = fields.length > 0;
@@ -1262,11 +1383,296 @@ async function dispatch(port, method, params) {
1262
1383
  return { ok: true, remaining: session.tabIds.size };
1263
1384
  }
1264
1385
 
1386
+ case 'solve_captcha': {
1387
+ const tab = await getSessionTab(port);
1388
+ const action = params.action || 'detect';
1389
+
1390
+ // ── Detect CAPTCHA on page ──
1391
+ if (action === 'detect') {
1392
+ const detection = await detectCaptcha(tab.id);
1393
+ return detection;
1394
+ }
1395
+
1396
+ // ── Auto-click reCAPTCHA checkbox ──
1397
+ if (action === 'click_checkbox') {
1398
+ const result = await clickRecaptchaCheckbox(tab.id);
1399
+ // Wait for challenge or pass
1400
+ await new Promise(r => setTimeout(r, 2500));
1401
+ // Re-detect to see if it passed or image challenge appeared
1402
+ const after = await detectCaptcha(tab.id);
1403
+ return { ...result, after };
1404
+ }
1405
+
1406
+ // ── Click specific grid cells (AI vision guided) ──
1407
+ if (action === 'click_grid') {
1408
+ const cells = params.cells || [];
1409
+ if (!cells.length) return { error: 'No cells specified' };
1410
+ const result = await clickCaptchaGridCells(tab.id, cells);
1411
+ return result;
1412
+ }
1413
+
1414
+ // ── Human fallback ──
1415
+ if (action === 'ask_human') {
1416
+ 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."' };
1417
+ }
1418
+
1419
+ return { error: 'Unknown action: ' + action };
1420
+ }
1421
+
1422
+ case 'upload_file': {
1423
+ const tab = await getSessionTab(port);
1424
+ const selector = params.selector || 'input[type="file"]';
1425
+ try {
1426
+ await debuggerAttach(tab.id);
1427
+ // Find the file input element
1428
+ const { result: nodeResult } = await chrome.debugger.sendCommand({ tabId: tab.id }, 'Runtime.evaluate', {
1429
+ expression: `(() => {
1430
+ const el = document.querySelector(${JSON.stringify(selector)});
1431
+ if (!el) return JSON.stringify({ found: false, error: 'File input not found: ${selector}' });
1432
+ return JSON.stringify({ found: true, tag: el.tagName, type: el.type, accept: el.accept, multiple: el.multiple });
1433
+ })()`,
1434
+ returnByValue: true,
1435
+ });
1436
+ const info = JSON.parse(nodeResult.value);
1437
+ if (!info.found) {
1438
+ await debuggerDetach(tab.id);
1439
+ return info;
1440
+ }
1441
+
1442
+ // Get the DOM node ID for the file input
1443
+ const { result: docResult } = await chrome.debugger.sendCommand({ tabId: tab.id }, 'DOM.getDocument', {});
1444
+ const { nodeId } = await chrome.debugger.sendCommand({ tabId: tab.id }, 'DOM.querySelector', {
1445
+ nodeId: docResult.root.nodeId,
1446
+ selector: selector,
1447
+ });
1448
+
1449
+ if (!nodeId) {
1450
+ await debuggerDetach(tab.id);
1451
+ return { found: false, error: 'Could not get DOM node for file input' };
1452
+ }
1453
+
1454
+ // Set files on the input using CDP
1455
+ const files = Array.isArray(params.files) ? params.files : [params.files || params.file];
1456
+ await chrome.debugger.sendCommand({ tabId: tab.id }, 'DOM.setFileInputFiles', {
1457
+ nodeId: nodeId,
1458
+ files: files,
1459
+ });
1460
+
1461
+ await debuggerDetach(tab.id);
1462
+ return { ok: true, files: files, input: info };
1463
+ } catch (e) {
1464
+ try { await debuggerDetach(tab.id); } catch {}
1465
+ return { ok: false, error: e.message };
1466
+ }
1467
+ }
1468
+
1469
+ case 'reload_extension': {
1470
+ // MCP server signals that extension files were updated via npx
1471
+ // Reload after a short delay to allow response to be sent
1472
+ setTimeout(() => chrome.runtime.reload(), 500);
1473
+ return { ok: true, message: 'Extension reloading in 500ms' };
1474
+ }
1475
+
1265
1476
  default:
1266
1477
  throw new Error('Unknown method: ' + method);
1267
1478
  }
1268
1479
  }
1269
1480
 
1481
+ // ── CAPTCHA Detection & Solving Helpers ─────────────────────────────────────
1482
+
1483
+ async function detectCaptcha(tabId) {
1484
+ try {
1485
+ await debuggerAttach(tabId);
1486
+ const { result } = await chrome.debugger.sendCommand({ tabId }, 'Runtime.evaluate', {
1487
+ expression: `(() => {
1488
+ const res = { found: false, types: [] };
1489
+
1490
+ // reCAPTCHA v2 — checkbox iframe
1491
+ const recaptchaAnchor = document.querySelector('iframe[src*="recaptcha/api2/anchor"], iframe[src*="recaptcha/enterprise/anchor"]');
1492
+ if (recaptchaAnchor) {
1493
+ res.found = true;
1494
+ res.types.push('recaptcha_v2_checkbox');
1495
+ const container = document.querySelector('.g-recaptcha');
1496
+ if (container) res.sitekey = container.getAttribute('data-sitekey');
1497
+ }
1498
+
1499
+ // reCAPTCHA v2 — image challenge iframe
1500
+ const recaptchaChallenge = document.querySelector('iframe[src*="recaptcha/api2/bframe"], iframe[src*="recaptcha/enterprise/bframe"]');
1501
+ if (recaptchaChallenge) {
1502
+ res.found = true;
1503
+ if (!res.types.includes('recaptcha_v2_checkbox')) res.types.push('recaptcha_v2_image');
1504
+ res.types.push('recaptcha_v2_challenge_visible');
1505
+ // Get iframe dimensions for grid clicking
1506
+ const rect = recaptchaChallenge.getBoundingClientRect();
1507
+ res.challengeFrame = { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
1508
+ }
1509
+
1510
+ // reCAPTCHA v3 — invisible badge
1511
+ const recaptchaV3 = document.querySelector('.grecaptcha-badge');
1512
+ if (recaptchaV3 && !recaptchaAnchor) {
1513
+ res.found = true;
1514
+ res.types.push('recaptcha_v3_invisible');
1515
+ res.note = 'reCAPTCHA v3 is invisible and score-based. Real Chrome with Google login usually passes automatically. No action needed.';
1516
+ }
1517
+
1518
+ // hCaptcha
1519
+ const hcaptcha = document.querySelector('iframe[src*="hcaptcha.com"], .h-captcha');
1520
+ if (hcaptcha) {
1521
+ res.found = true;
1522
+ res.types.push('hcaptcha');
1523
+ const container = document.querySelector('.h-captcha');
1524
+ if (container) res.sitekey = container.getAttribute('data-sitekey');
1525
+ }
1526
+
1527
+ // Cloudflare Turnstile
1528
+ const turnstile = document.querySelector('iframe[src*="challenges.cloudflare.com"], .cf-turnstile');
1529
+ if (turnstile) {
1530
+ res.found = true;
1531
+ res.types.push('cloudflare_turnstile');
1532
+ const container = document.querySelector('.cf-turnstile');
1533
+ if (container) res.sitekey = container.getAttribute('data-sitekey');
1534
+ }
1535
+
1536
+ // Cloudflare challenge page (5-second interstitial)
1537
+ if (document.title.includes('Just a moment') || document.querySelector('#challenge-running')) {
1538
+ res.found = true;
1539
+ res.types.push('cloudflare_challenge_page');
1540
+ res.note = 'Cloudflare challenge page. Wait 5-10 seconds — real Chrome usually passes automatically.';
1541
+ }
1542
+
1543
+ // FunCaptcha / Arkose Labs
1544
+ const funcaptcha = document.querySelector('#FunCaptcha, iframe[src*="funcaptcha"], iframe[src*="arkoselabs"]');
1545
+ if (funcaptcha) {
1546
+ res.found = true;
1547
+ res.types.push('funcaptcha');
1548
+ }
1549
+
1550
+ if (!res.found) res.note = 'No CAPTCHA detected on this page.';
1551
+ res.pageUrl = window.location.href;
1552
+ return JSON.stringify(res);
1553
+ })()`,
1554
+ returnByValue: true,
1555
+ });
1556
+ await debuggerDetach(tabId);
1557
+ return JSON.parse(result.value);
1558
+ } catch (e) {
1559
+ try { await debuggerDetach(tabId); } catch {}
1560
+ return { found: false, error: e.message };
1561
+ }
1562
+ }
1563
+
1564
+ async function clickRecaptchaCheckbox(tabId) {
1565
+ try {
1566
+ await debuggerAttach(tabId);
1567
+ // Find the reCAPTCHA anchor iframe position
1568
+ const { result } = await chrome.debugger.sendCommand({ tabId }, 'Runtime.evaluate', {
1569
+ expression: `(() => {
1570
+ const iframe = document.querySelector('iframe[src*="recaptcha/api2/anchor"], iframe[src*="recaptcha/enterprise/anchor"]');
1571
+ if (!iframe) return JSON.stringify({ found: false });
1572
+ const rect = iframe.getBoundingClientRect();
1573
+ // Checkbox is roughly at 27,30 inside the iframe (standard reCAPTCHA layout)
1574
+ return JSON.stringify({ found: true, x: rect.x + 27, y: rect.y + 30 });
1575
+ })()`,
1576
+ returnByValue: true,
1577
+ });
1578
+ const pos = JSON.parse(result.value);
1579
+ if (!pos.found) {
1580
+ await debuggerDetach(tabId);
1581
+ return { clicked: false, reason: 'reCAPTCHA checkbox iframe not found' };
1582
+ }
1583
+
1584
+ // Click the checkbox using real mouse events
1585
+ await chrome.debugger.sendCommand({ tabId }, 'Input.dispatchMouseEvent', {
1586
+ type: 'mouseMoved', x: pos.x, y: pos.y,
1587
+ });
1588
+ await new Promise(r => setTimeout(r, 100 + Math.random() * 200));
1589
+ await chrome.debugger.sendCommand({ tabId }, 'Input.dispatchMouseEvent', {
1590
+ type: 'mousePressed', x: pos.x, y: pos.y, button: 'left', clickCount: 1,
1591
+ });
1592
+ await chrome.debugger.sendCommand({ tabId }, 'Input.dispatchMouseEvent', {
1593
+ type: 'mouseReleased', x: pos.x, y: pos.y, button: 'left', clickCount: 1,
1594
+ });
1595
+ await debuggerDetach(tabId);
1596
+ return { clicked: true, position: pos, note: 'Checkbox clicked. Wait 2-3 seconds then re-detect to check if passed or image challenge appeared.' };
1597
+ } catch (e) {
1598
+ try { await debuggerDetach(tabId); } catch {}
1599
+ return { clicked: false, error: e.message };
1600
+ }
1601
+ }
1602
+
1603
+ async function clickCaptchaGridCells(tabId, cells) {
1604
+ try {
1605
+ await debuggerAttach(tabId);
1606
+ // Find the challenge iframe position and dimensions
1607
+ const { result } = await chrome.debugger.sendCommand({ tabId }, 'Runtime.evaluate', {
1608
+ expression: `(() => {
1609
+ const iframe = document.querySelector('iframe[src*="recaptcha/api2/bframe"], iframe[src*="recaptcha/enterprise/bframe"]');
1610
+ if (!iframe) return JSON.stringify({ found: false });
1611
+ const rect = iframe.getBoundingClientRect();
1612
+ return JSON.stringify({ found: true, x: rect.x, y: rect.y, width: rect.width, height: rect.height });
1613
+ })()`,
1614
+ returnByValue: true,
1615
+ });
1616
+ const frame = JSON.parse(result.value);
1617
+ if (!frame.found) {
1618
+ await debuggerDetach(tabId);
1619
+ return { clicked: false, reason: 'Challenge iframe not found. Take a screenshot to verify CAPTCHA state.' };
1620
+ }
1621
+
1622
+ // Determine grid size — reCAPTCHA uses 3x3 or 4x4 grids
1623
+ // The image grid starts ~100px from top of iframe, and is roughly square
1624
+ const gridTop = frame.y + 100;
1625
+ const gridLeft = frame.x + 14;
1626
+ const gridSize = frame.width - 28; // padding on each side
1627
+ const cols = cells.some(c => c >= 9) ? 4 : 3;
1628
+ const rows = cols;
1629
+ const cellSize = gridSize / cols;
1630
+
1631
+ const maxCell = cols * rows - 1;
1632
+ const validCells = cells.filter(c => c >= 0 && c <= maxCell);
1633
+ if (!validCells.length) {
1634
+ await debuggerDetach(tabId);
1635
+ return { clicked: false, error: `All cell indices out of bounds. Grid is ${cols}x${rows}, valid range: 0-${maxCell}` };
1636
+ }
1637
+
1638
+ const clicked = [];
1639
+ for (const cell of validCells) {
1640
+ const row = Math.floor(cell / cols);
1641
+ const col = cell % cols;
1642
+ const x = Math.round(gridLeft + col * cellSize + cellSize / 2);
1643
+ const y = Math.round(gridTop + row * cellSize + cellSize / 2);
1644
+
1645
+ // Human-like click with small random offset
1646
+ const ox = x + Math.round((Math.random() - 0.5) * cellSize * 0.3);
1647
+ const oy = y + Math.round((Math.random() - 0.5) * cellSize * 0.3);
1648
+
1649
+ await chrome.debugger.sendCommand({ tabId }, 'Input.dispatchMouseEvent', {
1650
+ type: 'mouseMoved', x: ox, y: oy,
1651
+ });
1652
+ await new Promise(r => setTimeout(r, 150 + Math.random() * 300));
1653
+ await chrome.debugger.sendCommand({ tabId }, 'Input.dispatchMouseEvent', {
1654
+ type: 'mousePressed', x: ox, y: oy, button: 'left', clickCount: 1,
1655
+ });
1656
+ await chrome.debugger.sendCommand({ tabId }, 'Input.dispatchMouseEvent', {
1657
+ type: 'mouseReleased', x: ox, y: oy, button: 'left', clickCount: 1,
1658
+ });
1659
+ await new Promise(r => setTimeout(r, 200 + Math.random() * 400));
1660
+ clicked.push({ cell, row, col, x: ox, y: oy });
1661
+ }
1662
+
1663
+ await debuggerDetach(tabId);
1664
+ return {
1665
+ clicked: true,
1666
+ cells: clicked,
1667
+ grid: `${cols}x${rows}`,
1668
+ note: 'Cells clicked. Take a screenshot to verify, then click the "Verify" / "Skip" button if needed.',
1669
+ };
1670
+ } catch (e) {
1671
+ try { await debuggerDetach(tabId); } catch {}
1672
+ return { clicked: false, error: e.message };
1673
+ }
1674
+ }
1675
+
1270
1676
  // ── Start ───────────────────────────────────────────────────────────────────
1271
1677
  ensureOffscreen().catch(console.error);
1272
1678
 
@@ -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.16.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.16.0' },
209
223
  { capabilities: { tools: {} } },
210
224
  { instructions: INSTRUCTIONS },
211
225
  );
@@ -243,6 +257,11 @@ 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_upload_file: 'upload_file',
261
+ browser_set_cookies: 'set_cookies',
262
+ browser_set_local_storage: 'set_local_storage',
263
+ browser_console_logs: 'console_logs',
264
+ browser_solve_captcha: 'solve_captcha',
246
265
  };
247
266
 
248
267
  if (name === 'browser_extract_token') {
@@ -254,7 +273,8 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
254
273
  return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
255
274
  }
256
275
 
257
- const timeout = method === 'ask_user' ? (args?.timeout || 120000) + 5000 : 30000;
276
+ const timeout = method === 'ask_user' ? (args?.timeout || 120000) + 5000 :
277
+ method === 'solve_captcha' ? 60000 : 30000;
258
278
  const result = await sendToExtension(method, args || {}, timeout);
259
279
 
260
280
  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.16.0",
4
+ "description": "Browser MCP — control your real Chrome from Claude Code. 29 tools, CAPTCHA solving, file upload, 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.',
@@ -264,6 +305,19 @@ export const TOOLS = [
264
305
  required: ['tab_id'],
265
306
  },
266
307
  },
308
+ {
309
+ name: 'browser_upload_file',
310
+ description: 'Upload a file to a <input type="file"> element on the page. Uses Chrome Debugger API to set files programmatically — no dialog needed.',
311
+ inputSchema: {
312
+ type: 'object',
313
+ properties: {
314
+ selector: { type: 'string', description: 'CSS selector for the file input (default: input[type="file"])' },
315
+ files: { type: 'array', items: { type: 'string' }, description: 'Array of absolute file paths to upload. E.g. ["/Users/me/photo.jpg"]' },
316
+ file: { type: 'string', description: 'Single file path (alternative to files array)' },
317
+ },
318
+ required: ['files'],
319
+ },
320
+ },
267
321
  {
268
322
  name: 'browser_extract_token',
269
323
  description: 'Navigate to a provider\'s API settings page and extract the API token. Optionally store it in Agent360 vault.',
@@ -276,6 +330,21 @@ export const TOOLS = [
276
330
  required: ['provider'],
277
331
  },
278
332
  },
333
+ {
334
+ name: 'browser_solve_captcha',
335
+ 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.',
336
+ inputSchema: {
337
+ type: 'object',
338
+ properties: {
339
+ 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"' },
340
+ cells: {
341
+ type: 'array',
342
+ items: { type: 'number' },
343
+ 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.',
344
+ },
345
+ },
346
+ },
347
+ },
279
348
  ];
280
349
 
281
350
  // Known provider token pages for browser_extract_token