@agent360/browser-mcp 1.24.0 → 1.28.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agent360/browser-mcp",
3
- "version": "1.24.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.",
3
+ "version": "1.28.1",
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.5",
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.15.2"
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 code in the context of the current page. Returns the result.',
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 expression to evaluate (runs in page context)' },
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 (partial match supported)' },
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 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.',
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: '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.',
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: 'Max wait for dialog in ms (default: 10000)' },
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
+ }