@agent360/browser-mcp 1.25.0 → 1.29.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 +95 -21
- package/bin/cli.js +102 -44
- package/extension/background.js +1033 -111
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/manifest.json +2 -2
- package/extension/offscreen.js +142 -10
- package/extension/popup.html +36 -2
- package/extension/popup.js +37 -21
- package/index.js +830 -60
- package/package.json +17 -3
- package/tools.js +95 -8
- package/vagt.js +57 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent360/browser-mcp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Browser MCP —
|
|
3
|
+
"version": "1.29.0",
|
|
4
|
+
"description": "Browser MCP — give your AI agent a real, logged-in Chrome: test authenticated apps, read 2FA codes from Gmail, scrape behind logins, solve CAPTCHAs. 40 tools, multi-session, human-in-the-loop.",
|
|
5
5
|
"mcpName": "io.github.Agent360dk/browser-mcp",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.js",
|
|
@@ -11,14 +11,20 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"index.js",
|
|
13
13
|
"tools.js",
|
|
14
|
+
"vagt.js",
|
|
14
15
|
"bin/",
|
|
15
16
|
"extension/",
|
|
16
17
|
"README.md"
|
|
17
18
|
],
|
|
18
19
|
"scripts": {
|
|
20
|
+
"test": "node --test ../test/*.test.mjs",
|
|
21
|
+
"flow": "node ../test/flow/run.mjs",
|
|
22
|
+
"sessions": "node ../test/flow/sessions.mjs",
|
|
23
|
+
"feedback": "node ../scripts/feedback-report.mjs",
|
|
19
24
|
"start": "node index.js",
|
|
20
25
|
"publish:cws": "../scripts/publish-cws.sh",
|
|
21
|
-
"publish:cws:draft": "../scripts/publish-cws.sh --draft"
|
|
26
|
+
"publish:cws:draft": "../scripts/publish-cws.sh --draft",
|
|
27
|
+
"samtidighed": "node ../test/flow/samtidighed.mjs"
|
|
22
28
|
},
|
|
23
29
|
"keywords": [
|
|
24
30
|
"mcp",
|
|
@@ -56,5 +62,13 @@
|
|
|
56
62
|
"dependencies": {
|
|
57
63
|
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
58
64
|
"ws": "^8.18.0"
|
|
65
|
+
},
|
|
66
|
+
"overrides": {
|
|
67
|
+
"fast-uri": "^3.1.7",
|
|
68
|
+
"hono": "^4.12.34",
|
|
69
|
+
"ip-address": "^10.3.1",
|
|
70
|
+
"@hono/node-server": "^1.19.15",
|
|
71
|
+
"body-parser": "^2.2.3",
|
|
72
|
+
"qs": "^6.16.0"
|
|
59
73
|
}
|
|
60
74
|
}
|
package/tools.js
CHANGED
|
@@ -27,6 +27,22 @@ export const TOOLS = [
|
|
|
27
27
|
},
|
|
28
28
|
},
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
name: 'browser_extract_list',
|
|
32
|
+
description: 'Read EVERY row of a long or virtualised list by scrolling its container until no new rows appear. Use this instead of browser_get_page_content whenever a page shows a repeating list longer than the viewport — mail lists (Outlook, Gmail), invoice/billing tables, search results, transaction histories. Those UIs keep only ~7 rows in the DOM at a time, so a single page read returns a sliver and looks complete. Pass the CSS selector of one repeating row (e.g. \'[role="option"]\', \'tr\', \'[role="listitem"]\'); the scrollable ancestor is found automatically. Returns deduplicated row text plus reached_end so you know whether you saw the whole list.',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
selector: { type: 'string', description: 'CSS selector matching ONE repeating row (e.g. \'[role="option"]\' in Outlook, \'tr\' in a table)' },
|
|
37
|
+
container: { type: 'string', description: 'Optional CSS selector for the scrollable container. Omit to auto-detect the row\'s nearest scrollable ancestor.' },
|
|
38
|
+
max_rows: { type: 'number', description: 'Stop after this many unique rows (default 500, max 5000)' },
|
|
39
|
+
stable_rounds: { type: 'number', description: 'Consecutive scrolls with no new rows before stopping (default 3)' },
|
|
40
|
+
scroll_step: { type: 'number', description: 'Pixels per scroll. Omit for 85% of the container height.' },
|
|
41
|
+
wait_ms: { type: 'number', description: 'Wait after each scroll so new rows can render (default 350)' },
|
|
42
|
+
},
|
|
43
|
+
required: ['selector'],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
30
46
|
{
|
|
31
47
|
name: 'browser_screenshot',
|
|
32
48
|
description: 'Take a screenshot of the visible area of the current tab. Returns base64 PNG, or saves to disk if path is provided.',
|
|
@@ -39,15 +55,54 @@ export const TOOLS = [
|
|
|
39
55
|
},
|
|
40
56
|
{
|
|
41
57
|
name: 'browser_execute_script',
|
|
42
|
-
description: 'Execute JavaScript
|
|
58
|
+
description: 'Execute JavaScript in the current page. IMPORTANT: the parameter is `code` (NOT `script` — though that alias is accepted), and it must be an EXPRESSION, not statements: use an IIFE `(() => { ...; return x; })()`. Top-level `return` is a syntax error (the handler wraps code in parentheses).',
|
|
43
59
|
inputSchema: {
|
|
44
60
|
type: 'object',
|
|
45
61
|
properties: {
|
|
46
|
-
code: { type: 'string', description: 'JavaScript
|
|
62
|
+
code: { type: 'string', description: 'JavaScript EXPRESSION to evaluate in page context. For multi-statement logic use an IIFE: (() => { ...; return result; })()' },
|
|
47
63
|
},
|
|
48
64
|
required: ['code'],
|
|
49
65
|
},
|
|
50
66
|
},
|
|
67
|
+
{
|
|
68
|
+
name: 'browser_double_click',
|
|
69
|
+
description: 'True double-click on an element (two trusted press/release pairs with escalating clickCount). Use for open-item actions (calendar events, file lists) where two single clicks would trigger inline-rename instead (e.g. OWA month view).',
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
selector: { type: 'string', description: 'CSS or text selector' },
|
|
74
|
+
},
|
|
75
|
+
required: ['selector'],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'browser_right_click',
|
|
80
|
+
description: 'Right-click an element (trusted CDP mouse events) to open page-level context menus (web apps like OWA/Google Docs render their own). Note: Chrome\'s NATIVE context menu does not open via CDP — only in-page menus.',
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: {
|
|
84
|
+
selector: { type: 'string', description: 'CSS or text selector' },
|
|
85
|
+
},
|
|
86
|
+
required: ['selector'],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'browser_click_xy',
|
|
91
|
+
description: 'ESCAPE HATCH: Click at raw viewport coordinates (CSS pixels) with fully trusted mouse events. Use when a visible button resists every selector strategy (Azure portal dialogs, Knockout-bound divs, canvas UIs): take a screenshot, read the button\'s position, click its center. Combine with browser_screenshot for coordinates.',
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
x: { type: 'number', description: 'X coordinate (CSS pixels, from left of viewport)' },
|
|
96
|
+
y: { type: 'number', description: 'Y coordinate (CSS pixels, from top of viewport)' },
|
|
97
|
+
},
|
|
98
|
+
required: ['x', 'y'],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'browser_reattach_debugger',
|
|
103
|
+
description: 'RECOVERY: Force-detach and re-attach the Chrome debugger on the current tab. Use when interactive tools (click/fill/press_key) start timing out or reporting ghost-attach ("Debugger attach failed ... ghost") while list_tabs still works — faster than reloading the extension.',
|
|
104
|
+
inputSchema: { type: 'object', properties: {} },
|
|
105
|
+
},
|
|
51
106
|
{
|
|
52
107
|
name: 'browser_click',
|
|
53
108
|
description: 'Click an element on the page. Supports CSS selectors AND text-based selectors. Auto-scrolls element into view. Uses real mouse events (works on Angular/React SPAs and CSP-strict sites like Google, Stripe). Examples: "button:text(Get started)", "text=Submit", "#my-button", "a.btn-primary"',
|
|
@@ -130,7 +185,7 @@ export const TOOLS = [
|
|
|
130
185
|
type: 'object',
|
|
131
186
|
properties: {
|
|
132
187
|
selector: { type: 'string', description: 'CSS or text selector for the dropdown trigger / <select> element' },
|
|
133
|
-
option: { type: 'string', description: 'Text of the option to select
|
|
188
|
+
option: { type: 'string', description: 'Text OR value of the option to select. Exact value match wins, then exact text, then partial text. Aliases: `value`, `label`. Fails loudly with the available options if nothing matches — it never reports success without the field actually changing.' },
|
|
134
189
|
wait: { type: 'number', description: 'Ms to wait after clicking trigger for options to appear (default: 300)' },
|
|
135
190
|
},
|
|
136
191
|
required: ['selector', 'option'],
|
|
@@ -165,12 +220,12 @@ export const TOOLS = [
|
|
|
165
220
|
},
|
|
166
221
|
{
|
|
167
222
|
name: 'browser_drop_file',
|
|
168
|
-
description: 'Upload a file
|
|
223
|
+
description: 'Upload a file when browser_upload_file fails. Two strategies: (1) finds a hidden <input type="file"> in the target\'s subtree or up to 2 ancestor levels; (2) if there is no input at all, intercepts the NATIVE OS file-chooser — pass the selector of the button that opens the dialog, and the file is supplied programmatically without the dialog ever appearing. Strategy 2 handles sites like Google Ads that never put a file input in the DOM.',
|
|
169
224
|
inputSchema: {
|
|
170
225
|
type: 'object',
|
|
171
226
|
properties: {
|
|
172
227
|
selector: { type: 'string', description: 'CSS selector for the drop-zone target element (e.g. ".upload-area")' },
|
|
173
|
-
file: { type: 'string', description: 'Single absolute file path' },
|
|
228
|
+
file: { type: 'string', description: 'Single absolute file path. Alias: `file_path`.' },
|
|
174
229
|
files: { type: 'array', items: { type: 'string' }, description: 'Array of absolute file paths' },
|
|
175
230
|
},
|
|
176
231
|
required: ['selector'],
|
|
@@ -191,13 +246,14 @@ export const TOOLS = [
|
|
|
191
246
|
},
|
|
192
247
|
{
|
|
193
248
|
name: 'browser_handle_dialog',
|
|
194
|
-
description: '
|
|
249
|
+
description: 'Arm automatic handling of the next JavaScript alert/confirm/prompt on the active tab, then return immediately. Call this BEFORE the click that opens the dialog — a dialog freezes the tab, so nothing can be clicked once it is on screen. Pass wait=true only when the dialog is already opening.',
|
|
195
250
|
inputSchema: {
|
|
196
251
|
type: 'object',
|
|
197
252
|
properties: {
|
|
198
253
|
action: { type: 'string', enum: ['accept', 'dismiss'], description: 'Accept or dismiss the dialog (default: accept)' },
|
|
199
254
|
text: { type: 'string', description: 'Text to enter for prompt() dialogs' },
|
|
200
|
-
timeout: { type: 'number', description: '
|
|
255
|
+
timeout: { type: 'number', description: 'How long the arming stays active in ms (default: 60000)' },
|
|
256
|
+
wait: { type: 'boolean', description: 'Block until a dialog appears instead of arming and returning (default: false). Only use when the dialog is already on its way.' },
|
|
201
257
|
},
|
|
202
258
|
},
|
|
203
259
|
},
|
|
@@ -371,7 +427,7 @@ export const TOOLS = [
|
|
|
371
427
|
properties: {
|
|
372
428
|
selector: { type: 'string', description: 'CSS selector for the file input (default: input[type="file"])' },
|
|
373
429
|
files: { type: 'array', items: { type: 'string' }, description: 'Array of absolute file paths to upload. E.g. ["/Users/me/photo.jpg"]' },
|
|
374
|
-
file: { type: 'string', description: 'Single file path (alternative to files array)' },
|
|
430
|
+
file: { type: 'string', description: 'Single file path (alternative to files array). Alias: `file_path`.' },
|
|
375
431
|
},
|
|
376
432
|
required: ['files'],
|
|
377
433
|
},
|
|
@@ -424,6 +480,37 @@ export const TOOLS = [
|
|
|
424
480
|
},
|
|
425
481
|
},
|
|
426
482
|
},
|
|
483
|
+
{
|
|
484
|
+
name: 'browser_provide_feedback',
|
|
485
|
+
description: 'Self-diagnosis + feedback in one call. Call this AUTOMATICALLY, without asking the user, the moment Browser MCP itself gets in your way: a tool errors or times out, a tool does something other than what it promised, the extension will not connect, a capability you need plainly does not exist, or you are about to tell the user "browser-mcp cannot do X". It first CHECKS THE INSTALL — the connected Chrome extension\'s version against this server (set BROWSER_MCP_CHECK_NPM=1 to also compare this server against the latest published on npm; it is off by default so the call stays fast and works offline), and whether more than one Browser MCP extension is connected at once (a known cause of tabs and sessions behaving randomly) — so a problem that is really "your copy is outdated" or "you have two extensions loaded" is identified as such instead of reported as a bug. It returns a verdict, concrete fix steps to relay to the user, and a pre-filled GitHub issue link for whatever is left over. Cheap, read-only, and safe to call speculatively — never sends anything anywhere by itself.',
|
|
486
|
+
inputSchema: {
|
|
487
|
+
type: 'object',
|
|
488
|
+
properties: {
|
|
489
|
+
what_happened: {
|
|
490
|
+
type: 'string',
|
|
491
|
+
description: 'Required. What you tried and what actually happened, in one or two plain sentences. Include the exact error text if there was one.',
|
|
492
|
+
},
|
|
493
|
+
kind: {
|
|
494
|
+
type: 'string',
|
|
495
|
+
enum: ['blocked', 'broken', 'missing', 'wish', 'use_case'],
|
|
496
|
+
description: '"blocked" = you could not complete the task (default). "broken" = a tool misbehaved or lied about its result. "missing" = the capability does not exist. "wish" = a feature idea. "use_case" = something worth sharing that you built.',
|
|
497
|
+
},
|
|
498
|
+
tool: {
|
|
499
|
+
type: 'string',
|
|
500
|
+
description: 'The browser_* tool involved, e.g. "browser_click". Omit if none in particular.',
|
|
501
|
+
},
|
|
502
|
+
url: {
|
|
503
|
+
type: 'string',
|
|
504
|
+
description: 'The page it happened on, if relevant. Strip query strings that contain tokens.',
|
|
505
|
+
},
|
|
506
|
+
attempted: {
|
|
507
|
+
type: 'string',
|
|
508
|
+
description: 'What you already tried (other selectors, other tools, retries) so the report does not suggest what you have ruled out.',
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
required: ['what_happened'],
|
|
512
|
+
},
|
|
513
|
+
},
|
|
427
514
|
];
|
|
428
515
|
|
|
429
516
|
// Known provider token pages for browser_extract_token
|
package/vagt.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foraeldre-vagten — hvornaar er en proces i kaeden faktisk doed?
|
|
3
|
+
*
|
|
4
|
+
* Ligger i sit eget modul UDEN sideeffekter, saa den kan importeres og koeres i en
|
|
5
|
+
* test. `index.js` starter en WebSocket-server, forbinder MCP-transporten og saetter
|
|
6
|
+
* timere op ved import — den kan ikke importeres af en test uden at haenge. Derfor
|
|
7
|
+
* blev vagt-logikken tidligere kun grepped efter som tekst, og en regex kan ikke se
|
|
8
|
+
* om FORTOLKNINGEN af en fejl er rigtig.
|
|
9
|
+
*
|
|
10
|
+
* MAALT 22/8 mod en levende server:
|
|
11
|
+
*
|
|
12
|
+
* [MCP] vagt-kaede: 1
|
|
13
|
+
* [MCP] Chrome extension connected on port 9882
|
|
14
|
+
* [MCP] Proces 1 i kaeden doede — chatten bag denne server er vaek
|
|
15
|
+
*
|
|
16
|
+
* Pid 1 er launchd. Den doede ikke. `process.kill(1, 0)` kaster EPERM for en
|
|
17
|
+
* almindelig bruger, og koden tolkede ENHVER exception som doed. Vagten der skulle
|
|
18
|
+
* frigive tomme porte draebte i stedet levende chats fem sekunder efter opstart.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param pid processen der skal tjekkes
|
|
23
|
+
* @param kill injicerbar for test; default er den aegte process.kill
|
|
24
|
+
* @returns true KUN naar processen beviseligt ikke findes
|
|
25
|
+
*/
|
|
26
|
+
export function ledErDoedt(pid, kill = process.kill.bind(process)) {
|
|
27
|
+
try {
|
|
28
|
+
kill(pid, 0); // signal 0 = findes processen?
|
|
29
|
+
return false; // svarede uden fejl → lever
|
|
30
|
+
} catch (e) {
|
|
31
|
+
// ESRCH ("no such process") er den ENESTE fejl der betyder doed.
|
|
32
|
+
// EPERM betyder at processen LEVER og bare ejes af en anden bruger.
|
|
33
|
+
// Alt andet er ukendt, og paa tvivl draeber vi ikke: at lukke en levende chat
|
|
34
|
+
// ned er en vaerre fejl end at holde en port lidt for laenge.
|
|
35
|
+
if (e?.code === 'ESRCH') return true;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Gaar kaeden op fra `start` til roden. Hvert led hentes med ét `ps`-kald.
|
|
42
|
+
* Doer et vilkaarligt led, er forbindelsen til den chat der ejer os brudt.
|
|
43
|
+
*
|
|
44
|
+
* @param start pid at gaa op fra
|
|
45
|
+
* @param laesPpid injicerbar for test; skal returnere forældrens pid eller null
|
|
46
|
+
*/
|
|
47
|
+
export function forfaedreKaede(start, laesPpid) {
|
|
48
|
+
const kaede = [];
|
|
49
|
+
let p = start;
|
|
50
|
+
for (let i = 0; i < 12 && p > 1; i++) {
|
|
51
|
+
kaede.push(p);
|
|
52
|
+
const naeste = laesPpid(p);
|
|
53
|
+
if (!Number.isFinite(naeste) || naeste <= 1 || naeste === p) break;
|
|
54
|
+
p = naeste;
|
|
55
|
+
}
|
|
56
|
+
return kaede;
|
|
57
|
+
}
|