@agent360/browser-mcp 1.21.0 → 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 +227 -44
- 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
|
-
|
|
252
|
+
return;
|
|
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
|
+
);
|
|
189
261
|
}
|
|
262
|
+
throw e;
|
|
190
263
|
}
|
|
191
264
|
}
|
|
192
265
|
|
|
@@ -319,25 +392,58 @@ async function debuggerType(tabId, text) {
|
|
|
319
392
|
async function debuggerClick(tabId, x, y) {
|
|
320
393
|
await debuggerAttach(tabId);
|
|
321
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
|
+
});
|
|
322
414
|
// 1. mouseMoved first (triggers hover state, required by some frameworks)
|
|
323
415
|
await cdpSend(tabId, 'Input.dispatchMouseEvent', {
|
|
324
416
|
type: 'mouseMoved', x, y,
|
|
325
417
|
});
|
|
326
418
|
await new Promise(r => setTimeout(r, 30));
|
|
327
|
-
// 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.
|
|
328
423
|
await cdpSend(tabId, 'Input.dispatchMouseEvent', {
|
|
329
|
-
type: 'mousePressed', x, y, button: 'left', clickCount: 1,
|
|
424
|
+
type: 'mousePressed', x, y, button: 'left', buttons: 1, clickCount: 1,
|
|
330
425
|
});
|
|
426
|
+
await new Promise(r => setTimeout(r, 30));
|
|
331
427
|
await cdpSend(tabId, 'Input.dispatchMouseEvent', {
|
|
332
|
-
type: 'mouseReleased', x, y, button: 'left', clickCount: 1,
|
|
428
|
+
type: 'mouseReleased', x, y, button: 'left', buttons: 0, clickCount: 1,
|
|
333
429
|
});
|
|
334
|
-
// 3.
|
|
335
|
-
//
|
|
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));
|
|
336
435
|
await cdpSend(tabId, 'Runtime.evaluate', {
|
|
337
436
|
expression: `(() => {
|
|
338
|
-
const el =
|
|
339
|
-
|
|
340
|
-
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();
|
|
341
447
|
|
|
342
448
|
// React fiber fallback — find and call onClick handler directly
|
|
343
449
|
const fiberKey = Object.keys(el).find(k => k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance'));
|
|
@@ -349,16 +455,11 @@ async function debuggerClick(tabId, x, y) {
|
|
|
349
455
|
}
|
|
350
456
|
}
|
|
351
457
|
|
|
352
|
-
// Angular fallback —
|
|
458
|
+
// Angular Material fallback — ripple + internal handlers
|
|
353
459
|
const ngKey = Object.keys(el).find(k => k.startsWith('__ng'));
|
|
354
460
|
if (ngKey || el.getAttribute('ng-click') || el.getAttribute('(click)')) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
el.dispatchEvent(new PointerEvent('pointerup', {bubbles:true, cancelable:true}));
|
|
358
|
-
el.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, view:window}));
|
|
359
|
-
// Angular Material components (mat-button, mat-checkbox, etc.) use ripple + internal handlers
|
|
360
|
-
const matRipple = el.closest('[mat-button], [mat-raised-button], [mat-icon-button], [mat-fab], mat-checkbox, mat-slide-toggle, mat-radio-button');
|
|
361
|
-
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));
|
|
362
463
|
}
|
|
363
464
|
})()`,
|
|
364
465
|
});
|
|
@@ -1636,43 +1737,96 @@ async function dispatch(port, method, params) {
|
|
|
1636
1737
|
}
|
|
1637
1738
|
|
|
1638
1739
|
case 'execute_script': {
|
|
1740
|
+
// v1.22.2 (DIAGNOSTIC): Try scripting paths but log all errors so we can see WHY they fail
|
|
1639
1741
|
const tab = await getSessionTab(port);
|
|
1640
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
|
|
1641
1747
|
try {
|
|
1642
1748
|
const [result] = await chrome.scripting.executeScript({
|
|
1643
1749
|
target: { tabId: tab.id },
|
|
1644
|
-
|
|
1645
|
-
|
|
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
|
+
},
|
|
1646
1760
|
});
|
|
1647
|
-
|
|
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
|
+
}
|
|
1648
1769
|
} catch (e) {
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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' };
|
|
1653
1792
|
}
|
|
1654
|
-
|
|
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);
|
|
1655
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
|
+
};
|
|
1656
1807
|
}
|
|
1657
1808
|
|
|
1658
1809
|
case 'click': {
|
|
1659
1810
|
const tab = await getSessionTab(port);
|
|
1660
1811
|
if (tab.url.startsWith('chrome://')) throw new Error('Cannot interact with chrome:// pages');
|
|
1661
1812
|
|
|
1662
|
-
//
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
// Primary path: debugger mouse events (isTrusted=true, works on React/Angular SPAs)
|
|
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.
|
|
1667
1817
|
try {
|
|
1818
|
+
const el = await resolveElement(tab.id, params.selector);
|
|
1819
|
+
if (!el) return { ok: false, error: 'Element not found: ' + params.selector };
|
|
1820
|
+
|
|
1821
|
+
// Primary path: debugger mouse events (isTrusted=true, works on React/Angular SPAs)
|
|
1668
1822
|
await debuggerClick(tab.id, el.x, el.y);
|
|
1669
1823
|
return { ok: true, method: el.method || 'debugger', tag: el.tag, text: el.text };
|
|
1670
1824
|
} catch (e) {
|
|
1671
1825
|
// Fallback: synthetic click via chrome.scripting for anti-automation sites
|
|
1672
|
-
// (Apple ASC etc.)
|
|
1826
|
+
// (Apple ASC etc.) OR user-blocked-debugger scenarios.
|
|
1673
1827
|
if (/Debugger detached/.test(e?.message || '')) {
|
|
1674
1828
|
const r = await scriptingClick(tab.id, params.selector);
|
|
1675
|
-
if (r.ok) return { ok: true, method: 'scripting-fallback', tag: r.tag
|
|
1829
|
+
if (r.ok) return { ok: true, method: 'scripting-fallback', tag: r.tag };
|
|
1676
1830
|
}
|
|
1677
1831
|
throw e;
|
|
1678
1832
|
}
|
|
@@ -1847,11 +2001,24 @@ async function dispatch(port, method, params) {
|
|
|
1847
2001
|
}
|
|
1848
2002
|
|
|
1849
2003
|
case 'press_key': {
|
|
1850
|
-
|
|
2004
|
+
// v1.22: activate tab so key-event lands in foreground (otherwise Chrome routes to active tab)
|
|
2005
|
+
const tab = await getSessionTab(port, true);
|
|
1851
2006
|
if (tab.url.startsWith('chrome://')) throw new Error('Cannot interact with chrome:// pages');
|
|
1852
2007
|
const key = params.key; // e.g. "Enter", "Tab", "Escape", "ArrowDown"
|
|
1853
2008
|
const modifiers = (params.ctrl ? 2 : 0) | (params.alt ? 1 : 0) | (params.shift ? 8 : 0) | (params.meta ? 4 : 0);
|
|
1854
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
|
+
|
|
1855
2022
|
await debuggerAttach(tab.id);
|
|
1856
2023
|
try {
|
|
1857
2024
|
await cdpSend(tab.id, 'Input.dispatchKeyEvent', {
|
|
@@ -1860,12 +2027,14 @@ async function dispatch(port, method, params) {
|
|
|
1860
2027
|
code: params.code || key,
|
|
1861
2028
|
modifiers,
|
|
1862
2029
|
text: key.length === 1 ? key : '',
|
|
2030
|
+
...vkParams,
|
|
1863
2031
|
});
|
|
1864
2032
|
await cdpSend(tab.id, 'Input.dispatchKeyEvent', {
|
|
1865
2033
|
type: 'keyUp',
|
|
1866
2034
|
key,
|
|
1867
2035
|
code: params.code || key,
|
|
1868
2036
|
modifiers,
|
|
2037
|
+
...vkParams,
|
|
1869
2038
|
});
|
|
1870
2039
|
} finally {
|
|
1871
2040
|
await debuggerDetach(tab.id);
|
|
@@ -1874,6 +2043,8 @@ async function dispatch(port, method, params) {
|
|
|
1874
2043
|
}
|
|
1875
2044
|
|
|
1876
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.
|
|
1877
2048
|
const tab = await getSessionTab(port);
|
|
1878
2049
|
if (tab.url.startsWith('chrome://')) throw new Error('Cannot interact with chrome:// pages');
|
|
1879
2050
|
// Scroll to element
|
|
@@ -1882,20 +2053,32 @@ async function dispatch(port, method, params) {
|
|
|
1882
2053
|
if (!el) return { ok: false, error: 'Element not found: ' + params.selector };
|
|
1883
2054
|
return { ok: true, scrolled_to: params.selector };
|
|
1884
2055
|
}
|
|
1885
|
-
// 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.
|
|
1886
2058
|
const dx = params.x || 0;
|
|
1887
2059
|
const dy = params.y || 0;
|
|
1888
2060
|
try {
|
|
1889
2061
|
await debuggerAttach(tab.id);
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
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)
|
|
1896
2078
|
await debuggerEval(tab.id, `window.scrollBy(${dx}, ${dy})`);
|
|
2079
|
+
return { ok: true, scrolled: { x: dx, y: dy }, method: 'fallback', fallback_reason: e.message };
|
|
1897
2080
|
}
|
|
1898
|
-
return { ok: true, scrolled: { x: dx, y: dy } };
|
|
2081
|
+
return { ok: true, scrolled: { x: dx, y: dy }, method: 'mouseWheel-stepped' };
|
|
1899
2082
|
}
|
|
1900
2083
|
|
|
1901
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
|