@agent360/browser-mcp 1.20.1 → 1.23.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 +77 -15
- package/bin/cli.js +6 -1
- package/extension/background.js +307 -62
- package/extension/manifest.json +2 -2
- package/index.js +62 -3
- package/package.json +2 -2
- package/tools.js +29 -2
package/README.md
CHANGED
|
@@ -5,28 +5,63 @@
|
|
|
5
5
|
[](https://github.com/Agent360dk/browser-mcp)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://modelcontextprotocol.io)
|
|
8
|
+
[](https://chromewebstore.google.com/detail/agent360-browser-mcp/jdehgalffmffhfhmmhaokfbfnafnmgcl)
|
|
8
9
|
|
|
9
10
|
**Control your real Chrome from Claude Code — with your logins, cookies, and 2FA.**
|
|
10
11
|
|
|
11
12
|

|
|
12
13
|
|
|
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).
|
|
14
|
+
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). 34 tools total.
|
|
14
15
|
|
|
15
|
-
## Install
|
|
16
|
+
## Install — 2 steps (~60 seconds)
|
|
17
|
+
|
|
18
|
+
### Step 1: Configure the MCP server
|
|
16
19
|
|
|
17
20
|
```bash
|
|
18
21
|
npx @agent360/browser-mcp install
|
|
19
22
|
```
|
|
20
23
|
|
|
21
|
-
This copies the Chrome extension to `~/.browser-mcp/extension/` and
|
|
24
|
+
This copies the Chrome extension files to `~/.browser-mcp/extension/` and adds the MCP server to your Claude Code config. **You'll see the path to the extension folder printed in the terminal — copy it.**
|
|
25
|
+
|
|
26
|
+
### Step 2: Load the extension in Chrome
|
|
27
|
+
|
|
28
|
+
> Chrome won't let extensions install themselves from npm — you load it manually one time. To **update** later, re-run the install command and reload it (see [Keeping it updated](#keeping-it-updated)). Prefer the [Chrome Web Store](#chrome-web-store-one-click-install) install if you'd rather have the extension auto-update.
|
|
29
|
+
|
|
30
|
+
1. **Open Chrome** and type `chrome://extensions` in the address bar
|
|
31
|
+
2. **Toggle "Developer mode"** ON (top right corner)
|
|
32
|
+
3. **Click "Load unpacked"** (top left, next to "Pack extension")
|
|
33
|
+
4. **Navigate to `~/.browser-mcp/extension/`** and click "Select"
|
|
34
|
+
- On Mac: Press `Cmd+Shift+G` in the file picker, paste `~/.browser-mcp/extension/`, press Enter
|
|
35
|
+
- On Windows: Paste `%USERPROFILE%\.browser-mcp\extension\` in the address bar
|
|
36
|
+
- On Linux: Type `~/.browser-mcp/extension/` in the path field
|
|
37
|
+
5. **Restart Claude Code** so it picks up the new MCP server
|
|
38
|
+
|
|
39
|
+
That's it. The Browser MCP icon will appear in your toolbar, and 34 browser tools are now available in Claude Code.
|
|
40
|
+
|
|
41
|
+
### Alternative: Manual zip download (no npm)
|
|
42
|
+
|
|
43
|
+
If you don't want to use npm, download the extension directly:
|
|
44
|
+
|
|
45
|
+
1. [Download `browser-mcp-v1.23.0.zip`](https://github.com/Agent360dk/browser-mcp/releases/latest) from the latest GitHub release
|
|
46
|
+
2. Unzip the file (anywhere — e.g. `~/Downloads/browser-mcp-extension/`)
|
|
47
|
+
3. Follow Step 2 above, but select the unzipped folder instead of `~/.browser-mcp/extension/`
|
|
48
|
+
4. Configure Claude Code manually by adding this to your `~/.claude.json` (or run `npx @agent360/browser-mcp install --skip-extension`):
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"mcpServers": {
|
|
52
|
+
"browser-mcp": {
|
|
53
|
+
"command": "npx",
|
|
54
|
+
"args": ["@agent360/browser-mcp"]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
22
59
|
|
|
23
|
-
|
|
24
|
-
1. Open `chrome://extensions`
|
|
25
|
-
2. Enable **Developer mode** (top right)
|
|
26
|
-
3. Click **Load unpacked** → select `~/.browser-mcp/extension/`
|
|
27
|
-
4. Restart Claude Code
|
|
60
|
+
### Chrome Web Store (one-click install)
|
|
28
61
|
|
|
29
|
-
|
|
62
|
+
[**Install from Chrome Web Store →**](https://chromewebstore.google.com/detail/agent360-browser-mcp/jdehgalffmffhfhmmhaokfbfnafnmgcl)
|
|
63
|
+
|
|
64
|
+
No Developer mode needed. Then run `npx @agent360/browser-mcp install --skip-extension` to configure Claude Code.
|
|
30
65
|
|
|
31
66
|
## Why This Over Playwright MCP / BrowserMCP?
|
|
32
67
|
|
|
@@ -43,7 +78,7 @@ That's it. 29 browser tools are now available in Claude Code.
|
|
|
43
78
|
| **Custom dropdowns** | Angular Material, React Select support | Works (headless) | Limited |
|
|
44
79
|
| **Install** | `npx @agent360/browser-mcp install` | `npx @anthropic-ai/mcp-playwright` | Manual clone |
|
|
45
80
|
|
|
46
|
-
##
|
|
81
|
+
## 34 Tools
|
|
47
82
|
|
|
48
83
|
### Navigation & Content
|
|
49
84
|
| Tool | Description |
|
|
@@ -63,7 +98,10 @@ That's it. 29 browser tools are now available in Claude Code.
|
|
|
63
98
|
| `browser_wait` | Wait for element to appear |
|
|
64
99
|
| `browser_hover` | Hover for tooltips/dropdowns |
|
|
65
100
|
| `browser_select_option` | Native `<select>` + custom dropdowns (Angular Material, React Select) |
|
|
66
|
-
| `
|
|
101
|
+
| `browser_set_combobox` | Autocomplete/combobox: type query → wait for filtered listbox → click option (multi-value chip support). Use when `browser_select_option` fails on lazy-rendered options |
|
|
102
|
+
| `browser_set_date` | Robust date inputs: tries native value-set → masked typing → calendar-picker navigation (MUI/AntD/react-datepicker/Lexical). Use when `browser_fill` fails on date fields |
|
|
103
|
+
| `browser_dismiss_overlays` | Bulk-dismiss popups/modals/tooltips/banners via aria-label/text/×-char heuristics. `non_critical` mode preserves dialogs with form data |
|
|
104
|
+
| `browser_handle_dialog` | Accept/dismiss native alert/confirm/prompt dialogs |
|
|
67
105
|
|
|
68
106
|
### Tabs & Frames
|
|
69
107
|
| Tool | Description |
|
|
@@ -103,6 +141,7 @@ That's it. 29 browser tools are now available in Claude Code.
|
|
|
103
141
|
| `browser_set_local_storage` | Write localStorage values |
|
|
104
142
|
| `browser_console_logs` | Capture console.log/warn/error messages from page |
|
|
105
143
|
| `browser_upload_file` | Upload files to `<input type="file">` via Chrome Debugger API (no dialog) |
|
|
144
|
+
| `browser_drop_file` | Upload via drop-zones: finds hidden `<input type="file">` in target subtree/parent (up to 2 levels). Use when `browser_upload_file` fails because the zone has no visible input |
|
|
106
145
|
|
|
107
146
|
## Multi-Session Support
|
|
108
147
|
|
|
@@ -145,7 +184,7 @@ extension/
|
|
|
145
184
|
|
|
146
185
|
mcp-server/
|
|
147
186
|
index.js # MCP server (stdio) + WebSocket client
|
|
148
|
-
tools.js #
|
|
187
|
+
tools.js # 34 tool definitions
|
|
149
188
|
bin/cli.js # Install CLI
|
|
150
189
|
```
|
|
151
190
|
|
|
@@ -157,11 +196,19 @@ mcp-server/
|
|
|
157
196
|
5. Commands flow: Claude Code → MCP → Extension → Chrome APIs
|
|
158
197
|
6. Process auto-exits when Claude Code closes (stdin detection)
|
|
159
198
|
|
|
160
|
-
##
|
|
199
|
+
## Keeping it updated
|
|
200
|
+
|
|
201
|
+
Browser MCP has two parts, and they update independently — how the **extension** updates depends on how you installed it:
|
|
161
202
|
|
|
162
|
-
|
|
203
|
+
| Part | Install method | How it updates |
|
|
204
|
+
|------|----------------|----------------|
|
|
205
|
+
| **MCP server** | any | **Automatic.** Runs via `npx @agent360/browser-mcp@latest`, so every Claude Code session pulls the newest from npm. Nothing to do. |
|
|
206
|
+
| **Extension** | **Chrome Web Store** | **Automatic.** Chrome updates it in the background (usually within a few hours). Nothing to do. |
|
|
207
|
+
| **Extension** | **Unpacked** (`npx … install` or manual zip) | **Manual.** Chrome never auto-updates a load-unpacked extension. Re-run `npx @agent360/browser-mcp install`, then open `chrome://extensions` → Browser MCP → **↻ reload**. |
|
|
163
208
|
|
|
164
|
-
|
|
209
|
+
**Not sure which you have?** Open `chrome://extensions` and find Browser MCP. If it shows a **"Loaded from /path/…"** line, it's unpacked (manual updates). If there's no such line, it came from the Chrome Web Store (auto-updates).
|
|
210
|
+
|
|
211
|
+
**Want zero-maintenance updates?** Install the extension from the [Chrome Web Store](https://chromewebstore.google.com/detail/agent360-browser-mcp/jdehgalffmffhfhmmhaokfbfnafnmgcl), then run `npx @agent360/browser-mcp install --skip-extension` to wire up just the MCP server. After that, both parts stay current on their own.
|
|
165
212
|
|
|
166
213
|
## Troubleshooting
|
|
167
214
|
|
|
@@ -183,6 +230,21 @@ To update the extension: `npx @agent360/browser-mcp install` (re-copies files),
|
|
|
183
230
|
- Idle timeout: 4 hours without commands → auto-exit
|
|
184
231
|
- Manual cleanup: `lsof -i :9876-9885 | grep LISTEN`
|
|
185
232
|
|
|
233
|
+
## 💡 Help Shape Browser MCP
|
|
234
|
+
|
|
235
|
+
Browser MCP is built in the open and shaped by the people using it.
|
|
236
|
+
|
|
237
|
+
### Browse what others want / built
|
|
238
|
+
- 💡 **[Wishlist →](WISHLIST.md)** — features people are asking for
|
|
239
|
+
- 🎯 **[Use-cases →](USE_CASES.md)** — what others have built (LinkedIn ICP scraping, vendor research, daily ops, …)
|
|
240
|
+
|
|
241
|
+
### Contribute in 30 seconds
|
|
242
|
+
- 💡 [Wish for a feature](https://github.com/Agent360dk/browser-mcp/issues/new?template=wish.yml)
|
|
243
|
+
- 🎯 [Share a use-case](https://github.com/Agent360dk/browser-mcp/issues/new?template=use-case.yml)
|
|
244
|
+
- 🐛 [Report a bug](https://github.com/Agent360dk/browser-mcp/issues/new?template=bug.yml)
|
|
245
|
+
|
|
246
|
+
Or just **ask Claude** — it knows about the `browser_about` tool and will draft + submit on your behalf when you say things like *"I wish browser-mcp could …"* or *"share my browser-mcp use-case"*.
|
|
247
|
+
|
|
186
248
|
## License
|
|
187
249
|
|
|
188
250
|
MIT — [Agent360](https://agent360.dk)
|
package/bin/cli.js
CHANGED
|
@@ -85,7 +85,7 @@ function install() {
|
|
|
85
85
|
5. Navigate to and select this folder:
|
|
86
86
|
${extensionDir}
|
|
87
87
|
6. The extension "Agent360 Browser MCP" appears with a puzzle icon
|
|
88
|
-
7. Restart Claude Code —
|
|
88
|
+
7. Restart Claude Code — 34 browser tools are now available!
|
|
89
89
|
|
|
90
90
|
🔄 Auto-updates (fully automatic):
|
|
91
91
|
- MCP server: always fetches latest from npm (npx @latest)
|
|
@@ -93,6 +93,11 @@ function install() {
|
|
|
93
93
|
- Extension reload: auto-triggered via WebSocket
|
|
94
94
|
- You don't need to do anything — updates happen on every Claude Code session start
|
|
95
95
|
|
|
96
|
+
💡 Help shape Browser MCP:
|
|
97
|
+
- Public wishlist: https://github.com/Agent360dk/browser-mcp/blob/main/WISHLIST.md
|
|
98
|
+
- Use-case gallery: https://github.com/Agent360dk/browser-mcp/blob/main/USE_CASES.md
|
|
99
|
+
- Got an idea, bug, or cool thing you built? Just ask Claude — it can draft + submit for you.
|
|
100
|
+
|
|
96
101
|
📖 Docs: https://browsermcp.dev
|
|
97
102
|
`);
|
|
98
103
|
}
|
package/extension/background.js
CHANGED
|
@@ -53,10 +53,45 @@ function getSession(port) {
|
|
|
53
53
|
return sessions.get(port);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// LRU eviction cap: hver session må højst have N åbne tabs samtidigt.
|
|
57
|
+
// Når en ny tab tilføjes ud over cap'en, lukkes den ÆLDSTE tab i sessionen
|
|
58
|
+
// (insertion-order via Set) — bortset fra session.activeTabId (current tab).
|
|
59
|
+
// Begrundelse: Claude Code-flows kan åbne 20+ navigate(new_tab=true) per session
|
|
60
|
+
// over en længere conversation. Uden eviction akkumulerer disse i Chrome som
|
|
61
|
+
// orphan-tabs der spiser RAM + giver "extension localhost 19+" tab-noise.
|
|
62
|
+
const MAX_TABS_PER_SESSION = 10;
|
|
63
|
+
|
|
64
|
+
async function evictOldestTabs(session, justAddedTabId) {
|
|
65
|
+
// Drop dead tab-ids først (user manually closed dem)
|
|
66
|
+
for (const id of [...session.tabIds]) {
|
|
67
|
+
try {
|
|
68
|
+
await chrome.tabs.get(id);
|
|
69
|
+
} catch {
|
|
70
|
+
session.tabIds.delete(id);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Evict oldest indtil ≤ cap. Skip activeTabId og just-added tab.
|
|
74
|
+
const ordered = [...session.tabIds];
|
|
75
|
+
for (const oldId of ordered) {
|
|
76
|
+
if (session.tabIds.size <= MAX_TABS_PER_SESSION) break;
|
|
77
|
+
if (oldId === session.activeTabId) continue;
|
|
78
|
+
if (oldId === justAddedTabId) continue;
|
|
79
|
+
try {
|
|
80
|
+
await chrome.tabs.remove(oldId);
|
|
81
|
+
} catch {} // tab may already be closed
|
|
82
|
+
session.tabIds.delete(oldId);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
56
86
|
async function addTabToSession(port, tabId) {
|
|
57
87
|
const session = getSession(port);
|
|
58
88
|
session.tabIds.add(tabId);
|
|
59
89
|
|
|
90
|
+
// LRU eviction: når sessionen overstiger cap, luk de ældste tabs.
|
|
91
|
+
if (session.tabIds.size > MAX_TABS_PER_SESSION) {
|
|
92
|
+
await evictOldestTabs(session, tabId);
|
|
93
|
+
}
|
|
94
|
+
|
|
60
95
|
try {
|
|
61
96
|
if (session.groupId !== null) {
|
|
62
97
|
try {
|
|
@@ -176,17 +211,55 @@ async function getSessionTab(port, activate = false) {
|
|
|
176
211
|
// Track which tabs have debugger attached to avoid repeated attach/detach
|
|
177
212
|
const debuggerAttached = new Set();
|
|
178
213
|
|
|
214
|
+
// Verify Chrome's actual debugger-truth before trusting local cache.
|
|
215
|
+
// Fixes "ghost-attached" state where Set says attached but Chrome side is gone
|
|
216
|
+
// (happens on SW lifecycle events, user-canceled banners, anti-automation evictions).
|
|
217
|
+
async function verifyAttachedWithChrome(tabId) {
|
|
218
|
+
try {
|
|
219
|
+
const targets = await chrome.debugger.getTargets();
|
|
220
|
+
const t = targets.find(x => x.tabId === tabId);
|
|
221
|
+
return !!t?.attached;
|
|
222
|
+
} catch {
|
|
223
|
+
return false; // assume not-attached on API error
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
179
227
|
async function debuggerAttach(tabId) {
|
|
180
|
-
|
|
228
|
+
// First check local cache — fast path
|
|
229
|
+
if (debuggerAttached.has(tabId)) {
|
|
230
|
+
// Verify with Chrome before trusting cache (cheap, ~1ms)
|
|
231
|
+
if (await verifyAttachedWithChrome(tabId)) return;
|
|
232
|
+
// Cache was stale — Chrome doesn't actually have us attached
|
|
233
|
+
debuggerAttached.delete(tabId);
|
|
234
|
+
}
|
|
235
|
+
|
|
181
236
|
try {
|
|
182
237
|
await chrome.debugger.attach({ tabId }, '1.3');
|
|
183
|
-
|
|
238
|
+
// Verify attach actually took effect (Chrome can silently no-op after user-cancel)
|
|
239
|
+
if (await verifyAttachedWithChrome(tabId)) {
|
|
240
|
+
debuggerAttached.add(tabId);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Debugger detached (GHOST_ATTACH): chrome.debugger.attach returned success but Chrome state shows tab ${tabId} not attached. ` +
|
|
245
|
+
`This typically means the user clicked "Cancel" on Chrome's debugger banner. ` +
|
|
246
|
+
`Fix: reload Browser MCP extension (chrome://extensions/ → ↻) or restart Chrome.`
|
|
247
|
+
);
|
|
184
248
|
} catch (e) {
|
|
185
249
|
if (e.message?.includes('Already attached')) {
|
|
250
|
+
// Chrome side has session — sync local cache
|
|
186
251
|
debuggerAttached.add(tabId);
|
|
187
|
-
|
|
188
|
-
throw e;
|
|
252
|
+
return;
|
|
189
253
|
}
|
|
254
|
+
if (e.message?.includes('Cannot attach') || e.message?.includes('canceled') || e.message?.includes('GHOST_ATTACH') || e.message?.includes('Debugger detached')) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Debugger detached (BLOCKED_BY_USER): Cannot attach debugger to tab ${tabId}. ` +
|
|
257
|
+
`Chrome blocks debugger attach — user likely clicked "Cancel" on debugger banner earlier this session. ` +
|
|
258
|
+
`Fix: chrome://extensions/ → Browser MCP → reload (↻) icon. Or restart Chrome. ` +
|
|
259
|
+
`Original error: ${e.message}`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
throw e;
|
|
190
263
|
}
|
|
191
264
|
}
|
|
192
265
|
|
|
@@ -237,27 +310,39 @@ const RETRYABLE_CDP_METHODS = new Set([
|
|
|
237
310
|
// For side-effectful methods, only re-attaches and throws — caller must decide.
|
|
238
311
|
async function cdpSend(tabId, method, params = {}) {
|
|
239
312
|
await debuggerAttach(tabId);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
msg
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
313
|
+
let lastMsg = '';
|
|
314
|
+
// 4 total attempts (initial + 3 retries) for read-only methods; backoff 100/300/500ms.
|
|
315
|
+
// Handles aggressive auto-detach on anti-automation sites (Apple ASC, Salesforce, etc.)
|
|
316
|
+
// where Chrome re-detaches between attach and command execution.
|
|
317
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
318
|
+
try {
|
|
319
|
+
return await chrome.debugger.sendCommand({ tabId }, method, params);
|
|
320
|
+
} catch (e) {
|
|
321
|
+
const msg = e?.message || String(e);
|
|
322
|
+
const isDetachError =
|
|
323
|
+
msg.includes('not attached') ||
|
|
324
|
+
msg.includes('Detached') ||
|
|
325
|
+
msg.includes('detached') ||
|
|
326
|
+
msg.includes('Debugger is gone') ||
|
|
327
|
+
msg.includes('No tab with given id');
|
|
328
|
+
if (!isDetachError) throw e;
|
|
329
|
+
lastMsg = msg;
|
|
330
|
+
debuggerAttached.delete(tabId);
|
|
331
|
+
if (!RETRYABLE_CDP_METHODS.has(method)) {
|
|
332
|
+
// Side-effectful methods (Input.*) — re-attach for next caller but signal
|
|
333
|
+
// to handler so it can fall back to chrome.scripting (e.g., synthetic click).
|
|
334
|
+
try { await debuggerAttach(tabId); } catch {}
|
|
335
|
+
throw new Error(`Debugger detached during ${method} — not auto-retried (side-effect risk). Original: ${msg}`);
|
|
336
|
+
}
|
|
337
|
+
if (attempt < 3) {
|
|
338
|
+
await new Promise(r => setTimeout(r, 100 + attempt * 200));
|
|
339
|
+
try { await debuggerAttach(tabId); } catch (attachErr) {
|
|
340
|
+
throw new Error(`Re-attach failed during ${method}: ${attachErr.message}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
256
343
|
}
|
|
257
|
-
await new Promise(r => setTimeout(r, 100));
|
|
258
|
-
await debuggerAttach(tabId);
|
|
259
|
-
return await chrome.debugger.sendCommand({ tabId }, method, params);
|
|
260
344
|
}
|
|
345
|
+
throw new Error(`Debugger detached repeatedly during ${method} (4 attempts). Last: ${lastMsg}`);
|
|
261
346
|
}
|
|
262
347
|
|
|
263
348
|
// Clean up debugger + session refs when tabs close
|
|
@@ -307,25 +392,58 @@ async function debuggerType(tabId, text) {
|
|
|
307
392
|
async function debuggerClick(tabId, x, y) {
|
|
308
393
|
await debuggerAttach(tabId);
|
|
309
394
|
try {
|
|
395
|
+
// 0. Capture the DEEPEST target element under the point BEFORE dispatching.
|
|
396
|
+
// Web-components (Google Ads <button-panel>, Material Web) keep their real
|
|
397
|
+
// <button> inside an (open) shadow root, so we pierce shadow roots to reach
|
|
398
|
+
// it. We stash it on window so the framework fallback (step 3) can verify it
|
|
399
|
+
// is still connected — if the trusted click already navigated/re-rendered,
|
|
400
|
+
// the ref is detached and we must NOT re-fire (avoids mis-clicks on the new
|
|
401
|
+
// view / double-submits).
|
|
402
|
+
await cdpSend(tabId, 'Runtime.evaluate', {
|
|
403
|
+
expression: `(() => {
|
|
404
|
+
let el = document.elementFromPoint(${x}, ${y});
|
|
405
|
+
let host = el;
|
|
406
|
+
for (let i = 0; i < 20 && host && host.shadowRoot; i++) {
|
|
407
|
+
const inner = host.shadowRoot.elementFromPoint(${x}, ${y});
|
|
408
|
+
if (!inner || inner === host) break;
|
|
409
|
+
el = inner; host = inner;
|
|
410
|
+
}
|
|
411
|
+
window.__bmcpClickTarget = el || null;
|
|
412
|
+
})()`,
|
|
413
|
+
});
|
|
310
414
|
// 1. mouseMoved first (triggers hover state, required by some frameworks)
|
|
311
415
|
await cdpSend(tabId, 'Input.dispatchMouseEvent', {
|
|
312
416
|
type: 'mouseMoved', x, y,
|
|
313
417
|
});
|
|
314
418
|
await new Promise(r => setTimeout(r, 30));
|
|
315
|
-
// 2. mousePressed + mouseReleased (
|
|
419
|
+
// 2. mousePressed + mouseReleased. The `buttons` bitmask (1 while pressed,
|
|
420
|
+
// 0 on release) plus a small press→release gap are REQUIRED for Chrome to
|
|
421
|
+
// synthesize a *trusted* 'click' from the pair. Without them, web-components
|
|
422
|
+
// that gate on the trusted click event (Google Ads, Material Web) never fire.
|
|
316
423
|
await cdpSend(tabId, 'Input.dispatchMouseEvent', {
|
|
317
|
-
type: 'mousePressed', x, y, button: 'left', clickCount: 1,
|
|
424
|
+
type: 'mousePressed', x, y, button: 'left', buttons: 1, clickCount: 1,
|
|
318
425
|
});
|
|
426
|
+
await new Promise(r => setTimeout(r, 30));
|
|
319
427
|
await cdpSend(tabId, 'Input.dispatchMouseEvent', {
|
|
320
|
-
type: 'mouseReleased', x, y, button: 'left', clickCount: 1,
|
|
428
|
+
type: 'mouseReleased', x, y, button: 'left', buttons: 0, clickCount: 1,
|
|
321
429
|
});
|
|
322
|
-
// 3.
|
|
323
|
-
//
|
|
430
|
+
// 3. Framework fallback — only if the captured target is STILL connected (i.e.
|
|
431
|
+
// the trusted click in step 2 did not already handle it). Settle delay lets
|
|
432
|
+
// SPA re-renders (Google Ads) detach the element first. Fires a full pointer
|
|
433
|
+
// + mouse sequence on the shadow-pierced target, then React/Angular handlers.
|
|
434
|
+
await new Promise(r => setTimeout(r, 120));
|
|
324
435
|
await cdpSend(tabId, 'Runtime.evaluate', {
|
|
325
436
|
expression: `(() => {
|
|
326
|
-
const el =
|
|
327
|
-
|
|
328
|
-
el.
|
|
437
|
+
const el = window.__bmcpClickTarget;
|
|
438
|
+
try { delete window.__bmcpClickTarget; } catch (e) {}
|
|
439
|
+
if (!el || !el.isConnected) return; // already navigated/handled — don't double-fire
|
|
440
|
+
const opts = { bubbles: true, cancelable: true, composed: true, view: window, clientX: ${x}, clientY: ${y} };
|
|
441
|
+
try { el.dispatchEvent(new PointerEvent('pointerdown', opts)); } catch (e) {}
|
|
442
|
+
el.dispatchEvent(new MouseEvent('mousedown', opts));
|
|
443
|
+
try { el.dispatchEvent(new PointerEvent('pointerup', opts)); } catch (e) {}
|
|
444
|
+
el.dispatchEvent(new MouseEvent('mouseup', opts));
|
|
445
|
+
el.dispatchEvent(new MouseEvent('click', opts));
|
|
446
|
+
if (typeof el.click === 'function') el.click();
|
|
329
447
|
|
|
330
448
|
// React fiber fallback — find and call onClick handler directly
|
|
331
449
|
const fiberKey = Object.keys(el).find(k => k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance'));
|
|
@@ -337,16 +455,11 @@ async function debuggerClick(tabId, x, y) {
|
|
|
337
455
|
}
|
|
338
456
|
}
|
|
339
457
|
|
|
340
|
-
// Angular fallback —
|
|
458
|
+
// Angular Material fallback — ripple + internal handlers
|
|
341
459
|
const ngKey = Object.keys(el).find(k => k.startsWith('__ng'));
|
|
342
460
|
if (ngKey || el.getAttribute('ng-click') || el.getAttribute('(click)')) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
el.dispatchEvent(new PointerEvent('pointerup', {bubbles:true, cancelable:true}));
|
|
346
|
-
el.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, view:window}));
|
|
347
|
-
// Angular Material components (mat-button, mat-checkbox, etc.) use ripple + internal handlers
|
|
348
|
-
const matRipple = el.closest('[mat-button], [mat-raised-button], [mat-icon-button], [mat-fab], mat-checkbox, mat-slide-toggle, mat-radio-button');
|
|
349
|
-
if (matRipple) matRipple.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, view:window}));
|
|
461
|
+
const matRipple = el.closest && el.closest('[mat-button], [mat-raised-button], [mat-icon-button], [mat-fab], mat-checkbox, mat-slide-toggle, mat-radio-button');
|
|
462
|
+
if (matRipple) matRipple.dispatchEvent(new MouseEvent('click', opts));
|
|
350
463
|
}
|
|
351
464
|
})()`,
|
|
352
465
|
});
|
|
@@ -437,6 +550,46 @@ async function debuggerEval(tabId, expression) {
|
|
|
437
550
|
}
|
|
438
551
|
}
|
|
439
552
|
|
|
553
|
+
// Synthetic click via chrome.scripting — fallback when debugger detaches on
|
|
554
|
+
// anti-automation sites (Apple ASC, etc.). Loses isTrusted=true but works for
|
|
555
|
+
// the ~95% of sites that don't check it. Handles text= and :text() selectors.
|
|
556
|
+
async function scriptingClick(tabId, selector) {
|
|
557
|
+
try {
|
|
558
|
+
const [result] = await chrome.scripting.executeScript({
|
|
559
|
+
target: { tabId },
|
|
560
|
+
world: 'MAIN',
|
|
561
|
+
func: (sel) => {
|
|
562
|
+
let el;
|
|
563
|
+
if (sel.startsWith('text=')) {
|
|
564
|
+
const text = sel.slice(5).trim();
|
|
565
|
+
el = Array.from(document.querySelectorAll('button, a, [role="button"], [role="menuitem"], [role="tab"], [role="option"], input, label, span, div, p, li, td'))
|
|
566
|
+
.find(e => (e.textContent || '').trim() === text);
|
|
567
|
+
} else {
|
|
568
|
+
const m = sel.match(/^([\w-]+):text\(([^)]+)\)$/);
|
|
569
|
+
if (m) {
|
|
570
|
+
const needle = m[2].trim();
|
|
571
|
+
el = Array.from(document.querySelectorAll(m[1]))
|
|
572
|
+
.find(e => (e.textContent || '').trim().includes(needle));
|
|
573
|
+
} else {
|
|
574
|
+
el = document.querySelector(sel);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (!el) return { ok: false, reason: 'not_found' };
|
|
578
|
+
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
579
|
+
const opts = { bubbles: true, cancelable: true, view: window };
|
|
580
|
+
el.dispatchEvent(new MouseEvent('mousedown', opts));
|
|
581
|
+
el.dispatchEvent(new MouseEvent('mouseup', opts));
|
|
582
|
+
el.click();
|
|
583
|
+
return { ok: true, tag: el.tagName };
|
|
584
|
+
},
|
|
585
|
+
args: [selector],
|
|
586
|
+
});
|
|
587
|
+
return result?.result || { ok: false, reason: 'no_result' };
|
|
588
|
+
} catch (e) {
|
|
589
|
+
return { ok: false, reason: 'exception', error: e.message };
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
440
593
|
// Try executeScript first, fall back to debugger on CSP error
|
|
441
594
|
async function safeExecuteScript(tabId, func, args = [], world = 'MAIN') {
|
|
442
595
|
try {
|
|
@@ -1584,36 +1737,99 @@ async function dispatch(port, method, params) {
|
|
|
1584
1737
|
}
|
|
1585
1738
|
|
|
1586
1739
|
case 'execute_script': {
|
|
1740
|
+
// v1.22.2 (DIAGNOSTIC): Try scripting paths but log all errors so we can see WHY they fail
|
|
1587
1741
|
const tab = await getSessionTab(port);
|
|
1588
1742
|
if (tab.url.startsWith('chrome://')) throw new Error('Cannot execute scripts on chrome:// pages');
|
|
1743
|
+
|
|
1744
|
+
const diag = { tried: [] };
|
|
1745
|
+
|
|
1746
|
+
// Step 1: try ISOLATED world
|
|
1589
1747
|
try {
|
|
1590
1748
|
const [result] = await chrome.scripting.executeScript({
|
|
1591
1749
|
target: { tabId: tab.id },
|
|
1592
|
-
|
|
1593
|
-
|
|
1750
|
+
world: 'ISOLATED',
|
|
1751
|
+
args: [params.code],
|
|
1752
|
+
func: (codeStr) => {
|
|
1753
|
+
try {
|
|
1754
|
+
const fn = new Function('return (' + codeStr + ')');
|
|
1755
|
+
return { __ok: true, value: fn() };
|
|
1756
|
+
} catch (e) {
|
|
1757
|
+
return { __scriptingError: true, message: String(e?.message || e), name: e?.name, world: 'ISOLATED' };
|
|
1758
|
+
}
|
|
1759
|
+
},
|
|
1594
1760
|
});
|
|
1595
|
-
|
|
1761
|
+
const r = result?.result;
|
|
1762
|
+
diag.tried.push({ world: 'ISOLATED', result_keys: r ? Object.keys(r) : null, r_type: typeof r });
|
|
1763
|
+
if (r && typeof r === 'object' && r.__ok) {
|
|
1764
|
+
return { result: r.value, method: 'scripting-isolated' };
|
|
1765
|
+
}
|
|
1766
|
+
if (r && typeof r === 'object' && r.__scriptingError) {
|
|
1767
|
+
diag.isolated_error = r.message;
|
|
1768
|
+
}
|
|
1596
1769
|
} catch (e) {
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1770
|
+
diag.isolated_throw = String(e?.message || e);
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// Step 2: try MAIN world
|
|
1774
|
+
try {
|
|
1775
|
+
const [result] = await chrome.scripting.executeScript({
|
|
1776
|
+
target: { tabId: tab.id },
|
|
1777
|
+
world: 'MAIN',
|
|
1778
|
+
args: [params.code],
|
|
1779
|
+
func: (codeStr) => {
|
|
1780
|
+
try {
|
|
1781
|
+
const fn = new Function('return (' + codeStr + ')');
|
|
1782
|
+
return { __ok: true, value: fn() };
|
|
1783
|
+
} catch (e) {
|
|
1784
|
+
return { __scriptingError: true, message: String(e?.message || e), name: e?.name, world: 'MAIN' };
|
|
1785
|
+
}
|
|
1786
|
+
},
|
|
1787
|
+
});
|
|
1788
|
+
const r = result?.result;
|
|
1789
|
+
diag.tried.push({ world: 'MAIN', result_keys: r ? Object.keys(r) : null, r_type: typeof r });
|
|
1790
|
+
if (r && typeof r === 'object' && r.__ok) {
|
|
1791
|
+
return { result: r.value, method: 'scripting-main' };
|
|
1601
1792
|
}
|
|
1602
|
-
|
|
1793
|
+
if (r && typeof r === 'object' && r.__scriptingError) {
|
|
1794
|
+
diag.main_error = r.message;
|
|
1795
|
+
}
|
|
1796
|
+
} catch (e) {
|
|
1797
|
+
diag.main_throw = String(e?.message || e);
|
|
1603
1798
|
}
|
|
1799
|
+
|
|
1800
|
+
// Step 3: DIAG MODE — don't fallback to debugger, return scripting-state info instead
|
|
1801
|
+
return {
|
|
1802
|
+
result: '__SCRIPTING_FAILED__',
|
|
1803
|
+
method: 'scripting-failed',
|
|
1804
|
+
diag,
|
|
1805
|
+
note: 'Both ISOLATED + MAIN scripting paths returned null/error. See diag for details.',
|
|
1806
|
+
};
|
|
1604
1807
|
}
|
|
1605
1808
|
|
|
1606
1809
|
case 'click': {
|
|
1607
1810
|
const tab = await getSessionTab(port);
|
|
1608
1811
|
if (tab.url.startsWith('chrome://')) throw new Error('Cannot interact with chrome:// pages');
|
|
1609
1812
|
|
|
1610
|
-
//
|
|
1611
|
-
|
|
1612
|
-
|
|
1813
|
+
// Wrap full click flow (incl. resolveElement) so debugger failures in EITHER
|
|
1814
|
+
// resolveElement (text-selectors use debuggerEval) OR debuggerClick trigger
|
|
1815
|
+
// the scripting-fallback. v1.21.2: previously only debuggerClick was wrapped,
|
|
1816
|
+
// leaving text-selector clicks unrecoverable when debugger was user-blocked.
|
|
1817
|
+
try {
|
|
1818
|
+
const el = await resolveElement(tab.id, params.selector);
|
|
1819
|
+
if (!el) return { ok: false, error: 'Element not found: ' + params.selector };
|
|
1613
1820
|
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1821
|
+
// Primary path: debugger mouse events (isTrusted=true, works on React/Angular SPAs)
|
|
1822
|
+
await debuggerClick(tab.id, el.x, el.y);
|
|
1823
|
+
return { ok: true, method: el.method || 'debugger', tag: el.tag, text: el.text };
|
|
1824
|
+
} catch (e) {
|
|
1825
|
+
// Fallback: synthetic click via chrome.scripting for anti-automation sites
|
|
1826
|
+
// (Apple ASC etc.) OR user-blocked-debugger scenarios.
|
|
1827
|
+
if (/Debugger detached/.test(e?.message || '')) {
|
|
1828
|
+
const r = await scriptingClick(tab.id, params.selector);
|
|
1829
|
+
if (r.ok) return { ok: true, method: 'scripting-fallback', tag: r.tag };
|
|
1830
|
+
}
|
|
1831
|
+
throw e;
|
|
1832
|
+
}
|
|
1617
1833
|
}
|
|
1618
1834
|
|
|
1619
1835
|
case 'fill': {
|
|
@@ -1785,11 +2001,24 @@ async function dispatch(port, method, params) {
|
|
|
1785
2001
|
}
|
|
1786
2002
|
|
|
1787
2003
|
case 'press_key': {
|
|
1788
|
-
|
|
2004
|
+
// v1.22: activate tab so key-event lands in foreground (otherwise Chrome routes to active tab)
|
|
2005
|
+
const tab = await getSessionTab(port, true);
|
|
1789
2006
|
if (tab.url.startsWith('chrome://')) throw new Error('Cannot interact with chrome:// pages');
|
|
1790
2007
|
const key = params.key; // e.g. "Enter", "Tab", "Escape", "ArrowDown"
|
|
1791
2008
|
const modifiers = (params.ctrl ? 2 : 0) | (params.alt ? 1 : 0) | (params.shift ? 8 : 0) | (params.meta ? 4 : 0);
|
|
1792
2009
|
|
|
2010
|
+
// v1.22: Chrome requires windowsVirtualKeyCode for navigation/system keys to trigger
|
|
2011
|
+
// scroll/form-submit behavior. Without these, key-event is dispatched but page doesn't react.
|
|
2012
|
+
const VK_CODES = {
|
|
2013
|
+
'Backspace': 8, 'Tab': 9, 'Enter': 13, 'Shift': 16, 'Control': 17, 'Alt': 18,
|
|
2014
|
+
'Escape': 27, 'Space': 32, ' ': 32,
|
|
2015
|
+
'PageUp': 33, 'PageDown': 34, 'End': 35, 'Home': 36,
|
|
2016
|
+
'ArrowLeft': 37, 'ArrowUp': 38, 'ArrowRight': 39, 'ArrowDown': 40,
|
|
2017
|
+
'Delete': 46,
|
|
2018
|
+
};
|
|
2019
|
+
const vkCode = VK_CODES[key];
|
|
2020
|
+
const vkParams = vkCode ? { windowsVirtualKeyCode: vkCode, nativeVirtualKeyCode: vkCode } : {};
|
|
2021
|
+
|
|
1793
2022
|
await debuggerAttach(tab.id);
|
|
1794
2023
|
try {
|
|
1795
2024
|
await cdpSend(tab.id, 'Input.dispatchKeyEvent', {
|
|
@@ -1798,12 +2027,14 @@ async function dispatch(port, method, params) {
|
|
|
1798
2027
|
code: params.code || key,
|
|
1799
2028
|
modifiers,
|
|
1800
2029
|
text: key.length === 1 ? key : '',
|
|
2030
|
+
...vkParams,
|
|
1801
2031
|
});
|
|
1802
2032
|
await cdpSend(tab.id, 'Input.dispatchKeyEvent', {
|
|
1803
2033
|
type: 'keyUp',
|
|
1804
2034
|
key,
|
|
1805
2035
|
code: params.code || key,
|
|
1806
2036
|
modifiers,
|
|
2037
|
+
...vkParams,
|
|
1807
2038
|
});
|
|
1808
2039
|
} finally {
|
|
1809
2040
|
await debuggerDetach(tab.id);
|
|
@@ -1812,6 +2043,8 @@ async function dispatch(port, method, params) {
|
|
|
1812
2043
|
}
|
|
1813
2044
|
|
|
1814
2045
|
case 'scroll': {
|
|
2046
|
+
// v1.22: NO activate — CDP Input.dispatchMouseEvent goes via debugger directly to target,
|
|
2047
|
+
// doesn't need active tab. Re-activating on every scroll-call destabilizes debugger.
|
|
1815
2048
|
const tab = await getSessionTab(port);
|
|
1816
2049
|
if (tab.url.startsWith('chrome://')) throw new Error('Cannot interact with chrome:// pages');
|
|
1817
2050
|
// Scroll to element
|
|
@@ -1820,20 +2053,32 @@ async function dispatch(port, method, params) {
|
|
|
1820
2053
|
if (!el) return { ok: false, error: 'Element not found: ' + params.selector };
|
|
1821
2054
|
return { ok: true, scrolled_to: params.selector };
|
|
1822
2055
|
}
|
|
1823
|
-
// Scroll by pixels using CDP mouseWheel —
|
|
2056
|
+
// Scroll by pixels using CDP mouseWheel — split into smaller steps so IntersectionObservers fire.
|
|
2057
|
+
// FB/Twitter/IG only trigger lazy-load on continuous wheel events, not a single large delta.
|
|
1824
2058
|
const dx = params.x || 0;
|
|
1825
2059
|
const dy = params.y || 0;
|
|
1826
2060
|
try {
|
|
1827
2061
|
await debuggerAttach(tab.id);
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2062
|
+
const STEP_SIZE = 300; // pixels per wheel-event (matches a typical mouse-wheel notch)
|
|
2063
|
+
const totalSteps = Math.max(1, Math.ceil(Math.max(Math.abs(dx), Math.abs(dy)) / STEP_SIZE));
|
|
2064
|
+
const stepX = dx / totalSteps;
|
|
2065
|
+
const stepY = dy / totalSteps;
|
|
2066
|
+
for (let i = 0; i < totalSteps; i++) {
|
|
2067
|
+
await cdpSend(tab.id, 'Input.dispatchMouseEvent', {
|
|
2068
|
+
type: 'mouseWheel', x: 400, y: 300, deltaX: stepX, deltaY: stepY,
|
|
2069
|
+
});
|
|
2070
|
+
// Small delay between wheel-events so IntersectionObserver + lazy-load XHRs can fire
|
|
2071
|
+
if (i < totalSteps - 1) await new Promise(r => setTimeout(r, 80));
|
|
2072
|
+
}
|
|
2073
|
+
// After last wheel-event, give FB/Twitter/IG ~600ms to start lazy-load XHRs
|
|
2074
|
+
// before any subsequent commands run (caller often scrapes immediately after)
|
|
2075
|
+
await new Promise(r => setTimeout(r, 600));
|
|
2076
|
+
} catch (e) {
|
|
2077
|
+
// Fallback to window.scrollBy for simple pages (synthetic but works on non-anti-scrape sites)
|
|
1834
2078
|
await debuggerEval(tab.id, `window.scrollBy(${dx}, ${dy})`);
|
|
2079
|
+
return { ok: true, scrolled: { x: dx, y: dy }, method: 'fallback', fallback_reason: e.message };
|
|
1835
2080
|
}
|
|
1836
|
-
return { ok: true, scrolled: { x: dx, y: dy } };
|
|
2081
|
+
return { ok: true, scrolled: { x: dx, y: dy }, method: 'mouseWheel-stepped' };
|
|
1837
2082
|
}
|
|
1838
2083
|
|
|
1839
2084
|
case 'hover': {
|
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": "Control your real Chrome from Claude Code — navigate, click, fill, screenshot, solve CAPTCHAs.
|
|
4
|
+
"version": "1.23.0",
|
|
5
|
+
"description": "Control your real Chrome from Claude Code — navigate, click, fill, screenshot, solve CAPTCHAs. 34 tools, multi-session.",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"tabs",
|
|
8
8
|
"tabGroups",
|
package/index.js
CHANGED
|
@@ -14,9 +14,9 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
14
14
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
15
15
|
import { WebSocketServer } from 'ws';
|
|
16
16
|
import { execSync } from 'child_process';
|
|
17
|
-
import { dirname, join } from 'path';
|
|
17
|
+
import { dirname, join, resolve } from 'path';
|
|
18
18
|
import { fileURLToPath } from 'url';
|
|
19
|
-
import { readFileSync } from 'fs';
|
|
19
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
20
20
|
import { TOOLS, PROVIDER_PAGES } from './tools.js';
|
|
21
21
|
|
|
22
22
|
// Read version from package.json — single source of truth, never drifts
|
|
@@ -242,7 +242,10 @@ For image grid challenges: cells are 0-indexed, left-to-right, top-to-bottom. A
|
|
|
242
242
|
The MCP server auto-pulls the latest code from git on every new session startup.
|
|
243
243
|
If the extension files were updated, ask the user to reload it:
|
|
244
244
|
"The Browser MCP extension was updated. Please go to chrome://extensions, find 'Agent360 Browser MCP', and click the reload icon (🔄) to apply the update."
|
|
245
|
-
You cannot navigate to chrome:// pages — the user must do this manually
|
|
245
|
+
You cannot navigate to chrome:// pages — the user must do this manually.
|
|
246
|
+
|
|
247
|
+
## Sharing wishes / use-cases / bugs
|
|
248
|
+
Browser MCP is community-shaped. Whenever the user (a) says "I wish browser-mcp could…" or expresses a missing feature, (b) hits something broken or unexpected that looks like a bug in browser-mcp itself, or (c) describes something cool they built with it — call **browser_about** with the matching intent ("wish" / "use_case" / "bug") and a short title + body, then offer the returned submit_url to the user as a clickable link. Don't ask permission, just draft + offer the link. This is the user's path to contribute back, and they usually don't know it exists.`;
|
|
246
249
|
|
|
247
250
|
const mcpServer = new Server(
|
|
248
251
|
{ name: 'agent360-browser', version: PKG_VERSION },
|
|
@@ -294,6 +297,10 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
294
297
|
browser_drop_file: 'drop_file',
|
|
295
298
|
};
|
|
296
299
|
|
|
300
|
+
if (name === 'browser_about') {
|
|
301
|
+
return handleAbout(args);
|
|
302
|
+
}
|
|
303
|
+
|
|
297
304
|
if (name === 'browser_extract_token') {
|
|
298
305
|
return await handleExtractToken(args);
|
|
299
306
|
}
|
|
@@ -312,6 +319,19 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
312
319
|
const prefix = isJpeg ? /^data:image\/jpeg;base64,/ : /^data:image\/png;base64,/;
|
|
313
320
|
const mimeType = isJpeg ? 'image/jpeg' : 'image/png';
|
|
314
321
|
const base64 = result.image.replace(prefix, '');
|
|
322
|
+
|
|
323
|
+
if (args && args.path) {
|
|
324
|
+
const targetPath = resolve(process.cwd(), args.path);
|
|
325
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
326
|
+
writeFileSync(targetPath, Buffer.from(base64, 'base64'));
|
|
327
|
+
return {
|
|
328
|
+
content: [
|
|
329
|
+
{ type: 'text', text: `Screenshot successfully saved to: ${targetPath}` },
|
|
330
|
+
{ type: 'image', data: base64, mimeType }
|
|
331
|
+
]
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
315
335
|
return { content: [{ type: 'image', data: base64, mimeType }] };
|
|
316
336
|
}
|
|
317
337
|
|
|
@@ -337,6 +357,45 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
337
357
|
}
|
|
338
358
|
});
|
|
339
359
|
|
|
360
|
+
const REPO_URL = 'https://github.com/Agent360dk/browser-mcp';
|
|
361
|
+
const ISSUE_TEMPLATES = { wish: 'wish.yml', use_case: 'use-case.yml', bug: 'bug.yml' };
|
|
362
|
+
|
|
363
|
+
function handleAbout(args) {
|
|
364
|
+
const intent = args?.intent || 'info';
|
|
365
|
+
const title = args?.title || '';
|
|
366
|
+
const body = args?.body || '';
|
|
367
|
+
|
|
368
|
+
const submit_url = intent === 'info' || !ISSUE_TEMPLATES[intent]
|
|
369
|
+
? `${REPO_URL}/issues/new/choose`
|
|
370
|
+
: `${REPO_URL}/issues/new?template=${ISSUE_TEMPLATES[intent]}` +
|
|
371
|
+
(title ? `&title=${encodeURIComponent(title)}` : '') +
|
|
372
|
+
(body ? `&body=${encodeURIComponent(body)}` : '');
|
|
373
|
+
|
|
374
|
+
const instruction =
|
|
375
|
+
intent === 'wish'
|
|
376
|
+
? `Share this exact submission link with the user as a clickable link, with a short note like "Click to submit your wish — it'll open a pre-filled GitHub issue you can review before submitting": ${submit_url}`
|
|
377
|
+
: intent === 'use_case'
|
|
378
|
+
? `Share this exact submission link with the user as a clickable link, with a short note like "Click to share your use-case — pre-filled, you can edit before submitting": ${submit_url}`
|
|
379
|
+
: intent === 'bug'
|
|
380
|
+
? `Share this exact bug-report link with the user as a clickable link, with a short note like "Click to report — pre-filled, please add reproduction steps before submitting": ${submit_url}`
|
|
381
|
+
: `Browser MCP is community-shaped. Open wishlist: ${REPO_URL}/blob/main/WISHLIST.md · Use-cases: ${REPO_URL}/blob/main/USE_CASES.md · Submit anything: ${REPO_URL}/issues/new/choose`;
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
content: [{
|
|
385
|
+
type: 'text',
|
|
386
|
+
text: JSON.stringify({
|
|
387
|
+
name: 'Browser MCP by Agent360',
|
|
388
|
+
version: PKG_VERSION,
|
|
389
|
+
repo: REPO_URL,
|
|
390
|
+
wishlist: `${REPO_URL}/blob/main/WISHLIST.md`,
|
|
391
|
+
use_cases: `${REPO_URL}/blob/main/USE_CASES.md`,
|
|
392
|
+
submit_url,
|
|
393
|
+
instruction,
|
|
394
|
+
}, null, 2),
|
|
395
|
+
}],
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
340
399
|
async function handleExtractToken(args) {
|
|
341
400
|
const { provider, store_in_vault } = args;
|
|
342
401
|
const info = PROVIDER_PAGES[provider];
|
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.23.0",
|
|
4
|
+
"description": "Browser MCP — control your real Chrome from Claude Code. 34 tools, CAPTCHA solving, date pickers, autocomplete combobox, overlay dismissal, file upload, multi-session, human-in-the-loop.",
|
|
5
5
|
"mcpName": "io.github.Agent360dk/browser-mcp",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.js",
|
package/tools.js
CHANGED
|
@@ -29,8 +29,13 @@ export const TOOLS = [
|
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
name: 'browser_screenshot',
|
|
32
|
-
description: 'Take a screenshot of the visible area of the current tab. Returns base64 PNG.',
|
|
33
|
-
inputSchema: {
|
|
32
|
+
description: 'Take a screenshot of the visible area of the current tab. Returns base64 PNG, or saves to disk if path is provided.',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
path: { type: 'string', description: 'File path to save the screenshot to (e.g. /path/to/screenshot.png)' },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
34
39
|
},
|
|
35
40
|
{
|
|
36
41
|
name: 'browser_execute_script',
|
|
@@ -398,6 +403,28 @@ export const TOOLS = [
|
|
|
398
403
|
},
|
|
399
404
|
},
|
|
400
405
|
},
|
|
406
|
+
{
|
|
407
|
+
name: 'browser_about',
|
|
408
|
+
description: 'Returns Browser MCP info and pre-filled URLs the user can click to submit feature wishes, share use-cases, or report bugs. Call this PROACTIVELY whenever the user (a) mentions a feature they wish existed ("I wish browser-mcp could...", "it would be nice if..."), (b) says something is missing, broken, or unexpected, (c) asks how Browser MCP works or who maintains it, or (d) describes something cool they built with browser-mcp. Pass intent="wish" | "use_case" | "bug" | "info" plus an optional title and body, and offer the returned submit_url to the user. Browser MCP is community-shaped — this tool is how the user contributes back.',
|
|
409
|
+
inputSchema: {
|
|
410
|
+
type: 'object',
|
|
411
|
+
properties: {
|
|
412
|
+
intent: {
|
|
413
|
+
type: 'string',
|
|
414
|
+
enum: ['wish', 'use_case', 'bug', 'info'],
|
|
415
|
+
description: 'What the user wants to share. "wish" = feature request, "use_case" = share what they built, "bug" = something broken, "info" = general (default: "info").',
|
|
416
|
+
},
|
|
417
|
+
title: {
|
|
418
|
+
type: 'string',
|
|
419
|
+
description: 'Optional pre-filled issue title (e.g. "Support hCaptcha v3"). Will be URL-encoded into the submit link.',
|
|
420
|
+
},
|
|
421
|
+
body: {
|
|
422
|
+
type: 'string',
|
|
423
|
+
description: 'Optional pre-filled body / first-comment draft. Will be URL-encoded into the submit link. Keep it short; user can expand on GitHub.',
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
},
|
|
401
428
|
];
|
|
402
429
|
|
|
403
430
|
// Known provider token pages for browser_extract_token
|