@agent360/browser-mcp 1.16.0 → 1.19.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 +14 -4
- package/extension/background.js +998 -40
- package/extension/manifest.json +2 -2
- package/extension/offscreen.js +15 -1
- package/index.js +49 -9
- package/package.json +2 -1
- package/tools.js +56 -3
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.19.0",
|
|
5
|
+
"description": "Control your real Chrome from Claude Code — navigate, click, fill, set dates, dismiss overlays, autocomplete, upload, screenshot, solve CAPTCHAs. 33 tools, multi-session, human-in-the-loop.",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"tabs",
|
|
8
8
|
"tabGroups",
|
package/extension/offscreen.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const BASE_PORT = 9876;
|
|
12
|
-
const MAX_PORT =
|
|
12
|
+
const MAX_PORT = 9895;
|
|
13
13
|
const connections = new Map(); // port → WebSocket
|
|
14
14
|
|
|
15
15
|
function scanPorts() {
|
|
@@ -92,6 +92,20 @@ function updateStatus() {
|
|
|
92
92
|
}).catch(() => {});
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// Listen for terminate signals from background.js (sent when last tab in a session closes)
|
|
96
|
+
chrome.runtime.onMessage.addListener((msg) => {
|
|
97
|
+
if (msg.type !== 'terminate_mcp_session' || typeof msg.port !== 'number') return;
|
|
98
|
+
const ws = connections.get(msg.port);
|
|
99
|
+
if (!ws) return;
|
|
100
|
+
try {
|
|
101
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
102
|
+
ws.send(JSON.stringify({ type: 'terminate' }));
|
|
103
|
+
}
|
|
104
|
+
} catch {}
|
|
105
|
+
try { ws.close(); } catch {}
|
|
106
|
+
// ws.onclose handler removes from connections + notifies background
|
|
107
|
+
});
|
|
108
|
+
|
|
95
109
|
// Initial scan + frequent rescan for new servers
|
|
96
110
|
scanPorts();
|
|
97
111
|
setInterval(scanPorts, 2000);
|
package/index.js
CHANGED
|
@@ -14,10 +14,16 @@ 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 } from 'path';
|
|
17
|
+
import { dirname, join } from 'path';
|
|
18
18
|
import { fileURLToPath } from 'url';
|
|
19
|
+
import { readFileSync } from 'fs';
|
|
19
20
|
import { TOOLS, PROVIDER_PAGES } from './tools.js';
|
|
20
21
|
|
|
22
|
+
// Read version from package.json — single source of truth, never drifts
|
|
23
|
+
const PKG_VERSION = JSON.parse(
|
|
24
|
+
readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'package.json'), 'utf8')
|
|
25
|
+
).version;
|
|
26
|
+
|
|
21
27
|
// ── Auto-update on startup ─────────────────────────────────────────────────
|
|
22
28
|
|
|
23
29
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -47,7 +53,7 @@ try {
|
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
const BASE_PORT = 9876;
|
|
50
|
-
const MAX_PORT =
|
|
56
|
+
const MAX_PORT = 9895; // 20 ports instead of 10 — zombies die within 5s via parent check
|
|
51
57
|
let extensionSocket = null;
|
|
52
58
|
let activePort = null;
|
|
53
59
|
let wss = null; // Track WSS for graceful shutdown
|
|
@@ -90,6 +96,12 @@ function createWSS(port = BASE_PORT) {
|
|
|
90
96
|
ws.on('message', (data) => {
|
|
91
97
|
let msg;
|
|
92
98
|
try { msg = JSON.parse(data.toString()); } catch { return; }
|
|
99
|
+
|
|
100
|
+
if (msg.type === 'terminate') {
|
|
101
|
+
process.stderr.write('[MCP] Terminate signal received from extension (last tab closed) — exiting\n');
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
|
|
93
105
|
const { id, result, error } = msg;
|
|
94
106
|
const p = pending.get(id);
|
|
95
107
|
if (!p) return;
|
|
@@ -128,12 +140,16 @@ createWSS();
|
|
|
128
140
|
|
|
129
141
|
// ── Send command to extension ───────────────────────────────────────────────
|
|
130
142
|
|
|
131
|
-
function sendToExtension(method, params = {}, timeoutMs = 30000) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries = 5) {
|
|
144
|
+
// Retry if extension is temporarily disconnected (reconnects every 2s)
|
|
145
|
+
if (!extensionSocket || extensionSocket.readyState !== 1) {
|
|
146
|
+
if (_retries > 0) {
|
|
147
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
148
|
+
return sendToExtension(method, params, timeoutMs, _retries - 1);
|
|
136
149
|
}
|
|
150
|
+
throw new Error('Chrome extension not connected after 5 retries. Open Chrome and ensure Agent360 Browser MCP extension is installed.');
|
|
151
|
+
}
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
137
153
|
const id = ++cmdId;
|
|
138
154
|
const timer = setTimeout(() => {
|
|
139
155
|
pending.delete(id);
|
|
@@ -206,11 +222,18 @@ For image grid challenges: cells are 0-indexed, left-to-right, top-to-bottom. A
|
|
|
206
222
|
- If a standard selector fails, the extension recursively searches shadow roots
|
|
207
223
|
- Text-based selectors ("text=Submit") also traverse shadow DOM
|
|
208
224
|
|
|
225
|
+
## Hard inputs — use the specialised tools first
|
|
226
|
+
- **Date inputs** → use browser_set_date (NOT browser_fill). Handles native date inputs, masked text inputs (MM/DD/YYYY etc.), AND calendar pickers (MUI, react-datepicker, AntD, Lexical/Meta). 3-path fallback with read-back verification.
|
|
227
|
+
- **Autocomplete / combobox** (Languages on Meta Ads, country selects, async dropdowns) → use browser_set_combobox (NOT browser_select_option). Types partial query, waits for filtered listbox, clicks option. Supports multi-value chips.
|
|
228
|
+
- **Drag-drop file zones without visible file input** → use browser_drop_file (NOT browser_upload_file). Finds hidden input in subtree/parent.
|
|
229
|
+
- **Annoying popups blocking the flow** (cookie banners, "Don't show again", Advantage+ tooltips, draft-confirm prompts) → call browser_dismiss_overlays before each major step. It only clicks safe close affordances by default; preserves forms with editable text fields.
|
|
230
|
+
|
|
209
231
|
## When things fail
|
|
210
232
|
- Element not found → try text-based selector instead of CSS
|
|
211
233
|
- Screenshot fails → debugger fallback is automatic
|
|
212
234
|
- Click doesn't work on SPA → debugger mouse events are used automatically
|
|
213
235
|
- CAPTCHA blocks page → use browser_ask_user, let human solve it
|
|
236
|
+
- browser_fill seemingly succeeds but value reverts → switch to browser_set_date or browser_set_combobox (most reverts are React-controlled validators)
|
|
214
237
|
|
|
215
238
|
## Extension updates
|
|
216
239
|
The MCP server auto-pulls the latest code from git on every new session startup.
|
|
@@ -219,7 +242,7 @@ If the extension files were updated, ask the user to reload it:
|
|
|
219
242
|
You cannot navigate to chrome:// pages — the user must do this manually.`;
|
|
220
243
|
|
|
221
244
|
const mcpServer = new Server(
|
|
222
|
-
{ name: 'agent360-browser', version:
|
|
245
|
+
{ name: 'agent360-browser', version: PKG_VERSION },
|
|
223
246
|
{ capabilities: { tools: {} } },
|
|
224
247
|
{ instructions: INSTRUCTIONS },
|
|
225
248
|
);
|
|
@@ -262,6 +285,10 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
262
285
|
browser_set_local_storage: 'set_local_storage',
|
|
263
286
|
browser_console_logs: 'console_logs',
|
|
264
287
|
browser_solve_captcha: 'solve_captcha',
|
|
288
|
+
browser_set_date: 'set_date',
|
|
289
|
+
browser_dismiss_overlays: 'dismiss_overlays',
|
|
290
|
+
browser_set_combobox: 'set_combobox',
|
|
291
|
+
browser_drop_file: 'drop_file',
|
|
265
292
|
};
|
|
266
293
|
|
|
267
294
|
if (name === 'browser_extract_token') {
|
|
@@ -345,7 +372,20 @@ process.on('exit', () => {
|
|
|
345
372
|
if (extensionSocket) try { extensionSocket.close(); } catch {}
|
|
346
373
|
});
|
|
347
374
|
|
|
348
|
-
// Detect Claude Code exit
|
|
375
|
+
// Detect Claude Code exit — check if parent process is still alive
|
|
376
|
+
// stdin.on('end') doesn't work because MCP SDK's StdioServerTransport owns stdin
|
|
377
|
+
const parentPid = process.ppid;
|
|
378
|
+
const parentCheck = setInterval(() => {
|
|
379
|
+
try {
|
|
380
|
+
process.kill(parentPid, 0); // signal 0 = check if process exists
|
|
381
|
+
} catch {
|
|
382
|
+
process.stderr.write(`[MCP] Parent process ${parentPid} died — shutting down\n`);
|
|
383
|
+
clearInterval(parentCheck);
|
|
384
|
+
process.exit(0);
|
|
385
|
+
}
|
|
386
|
+
}, 5000); // check every 5 seconds
|
|
387
|
+
|
|
388
|
+
// Also listen for stdin close as backup
|
|
349
389
|
process.stdin.on('end', () => {
|
|
350
390
|
process.stderr.write('[MCP] stdin closed — shutting down\n');
|
|
351
391
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent360/browser-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "Browser MCP — control your real Chrome from Claude Code. 29 tools, CAPTCHA solving, file upload, multi-session, human-in-the-loop.",
|
|
5
|
+
"mcpName": "io.github.Agent360dk/browser-mcp",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "index.js",
|
|
7
8
|
"bin": {
|
package/tools.js
CHANGED
|
@@ -56,7 +56,7 @@ export const TOOLS = [
|
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
name: 'browser_fill',
|
|
59
|
-
description: 'Fill a form input field with a value. Supports CSS selectors AND text-based selectors. Auto-scrolls and focuses the element. Works on CSP-strict sites via Chrome Debugger API.',
|
|
59
|
+
description: 'Fill a form input field with a value. Supports CSS selectors AND text-based selectors. Auto-scrolls and focuses the element. Works on CSP-strict sites via Chrome Debugger API. For date inputs use browser_set_date, for autocomplete/combobox use browser_set_combobox.',
|
|
60
60
|
inputSchema: {
|
|
61
61
|
type: 'object',
|
|
62
62
|
properties: {
|
|
@@ -120,7 +120,7 @@ export const TOOLS = [
|
|
|
120
120
|
},
|
|
121
121
|
{
|
|
122
122
|
name: 'browser_select_option',
|
|
123
|
-
description: 'Select an option from a dropdown menu. Works with native <select> elements AND custom dropdowns (Angular Material, React Select, etc.). For custom dropdowns: clicks the trigger, waits for options, then clicks the matching option by text.',
|
|
123
|
+
description: 'Select an option from a dropdown menu. Works with native <select> elements AND custom dropdowns (Angular Material, React Select, etc.). For custom dropdowns: clicks the trigger, waits for options, then clicks the matching option by text. For autocomplete (typing filters options) use browser_set_combobox instead.',
|
|
124
124
|
inputSchema: {
|
|
125
125
|
type: 'object',
|
|
126
126
|
properties: {
|
|
@@ -131,6 +131,59 @@ export const TOOLS = [
|
|
|
131
131
|
required: ['selector', 'option'],
|
|
132
132
|
},
|
|
133
133
|
},
|
|
134
|
+
{
|
|
135
|
+
name: 'browser_dismiss_overlays',
|
|
136
|
+
description: 'Dismiss visible popups, modals, tooltips, banners, and "Are you sure?"-style overlays in one call. Heuristic-based: finds close affordance via aria-label, text content (Skip/Cancel/Ikke nu/Don\'t show/Got it/Close), or × character button. Use when a flow is interrupted by unexpected dialogs (cookie banners, onboarding tooltips, draft-confirm prompts on Meta Ads, etc.). Returns list of what was dismissed.',
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
scope: { type: 'string', enum: ['non_critical', 'aggressive'], description: 'non_critical (default): skip dialogs containing editable form inputs (preserves user data). aggressive: dismiss everything.' },
|
|
141
|
+
max_passes: { type: 'number', description: 'Number of dismissal passes (some overlays reveal others when closed). Default: 3' },
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'browser_set_combobox',
|
|
147
|
+
description: 'Set value(s) on an autocomplete/combobox input. Handles the click → type query → wait for filtered listbox → click option flow as one MCP call. Supports multi-select (e.g., Languages on Meta Ads). Use when browser_select_option fails because options render lazily after typing.',
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
selector: { type: 'string', description: 'CSS selector for the combobox/autocomplete input' },
|
|
152
|
+
value: { type: 'string', description: 'Single value to select (use this OR values)' },
|
|
153
|
+
values: { type: 'array', items: { type: 'string' }, description: 'Array of values for multi-select. E.g. ["Danish", "English", "Swedish"]' },
|
|
154
|
+
multi: { type: 'boolean', description: 'True if combobox accepts multiple values (chips). Default: auto-detected from presence of values array' },
|
|
155
|
+
query_chars: { type: 'number', description: 'How many characters to type as filter query (default: 4 or full value length, whichever is smaller)' },
|
|
156
|
+
wait_ms: { type: 'number', description: 'Max ms to wait for options listbox to appear after typing (default: 3000)' },
|
|
157
|
+
},
|
|
158
|
+
required: ['selector'],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'browser_drop_file',
|
|
163
|
+
description: 'Upload a file by finding a hidden <input type="file"> within a drag-drop zone\'s subtree (or parent up to 2 levels). Use when browser_upload_file fails because the dropzone has no visible file input. Returns clear error if no input is found anywhere — pure drop-zones without backing inputs require manual handling.',
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
selector: { type: 'string', description: 'CSS selector for the drop-zone target element (e.g. ".upload-area")' },
|
|
168
|
+
file: { type: 'string', description: 'Single absolute file path' },
|
|
169
|
+
files: { type: 'array', items: { type: 'string' }, description: 'Array of absolute file paths' },
|
|
170
|
+
},
|
|
171
|
+
required: ['selector'],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'browser_set_date',
|
|
176
|
+
description: 'Robustly set a date input — handles native <input type="date">, masked text inputs (e.g. MM/DD/YYYY), and calendar pickers (MUI, react-datepicker, AntD, Lexical/Meta). Tries native value-set, format-aware typing via Input.insertText, and ARIA-based picker navigation in sequence with read-back verification. Use instead of browser_fill when fill fails or for any input that opens a calendar widget.',
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
properties: {
|
|
180
|
+
selector: { type: 'string', description: 'CSS selector for the date input element' },
|
|
181
|
+
date: { type: 'string', description: 'ISO date string (YYYY-MM-DD), e.g. "2026-05-15"' },
|
|
182
|
+
skip_picker: { type: 'boolean', description: 'If true, only try native + masked paths and skip calendar-picker navigation (default: false)' },
|
|
183
|
+
},
|
|
184
|
+
required: ['selector', 'date'],
|
|
185
|
+
},
|
|
186
|
+
},
|
|
134
187
|
{
|
|
135
188
|
name: 'browser_handle_dialog',
|
|
136
189
|
description: 'Handle JavaScript alert(), confirm(), or prompt() dialogs. Call this BEFORE triggering the action that causes the dialog. Waits for the dialog to appear, then accepts or dismisses it.',
|
|
@@ -307,7 +360,7 @@ export const TOOLS = [
|
|
|
307
360
|
},
|
|
308
361
|
{
|
|
309
362
|
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.',
|
|
363
|
+
description: 'Upload a file to a <input type="file"> element on the page. Uses Chrome Debugger API to set files programmatically — no dialog needed. For drag-drop zones without visible file input use browser_drop_file.',
|
|
311
364
|
inputSchema: {
|
|
312
365
|
type: 'object',
|
|
313
366
|
properties: {
|