@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 +15 -6
- package/bin/cli.js +44 -10
- package/extension/background.js +420 -14
- package/extension/manifest.json +2 -2
- package/extension/popup.html +4 -4
- package/index.js +25 -5
- package/package.json +17 -6
- package/tools.js +69 -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,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
|
-
//
|
|
258
|
+
// Fire JS click + React/Angular framework fallbacks.
|
|
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
|
+
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 '
|
|
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
|
|
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.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",
|
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.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 :
|
|
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.
|
|
4
|
-
"description": "Browser MCP — control your real Chrome from Claude Code.
|
|
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
|
-
"
|
|
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.',
|
|
@@ -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
|