@agent360/browser-mcp 1.24.0 → 1.25.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 CHANGED
@@ -48,7 +48,7 @@ That's it. The Browser MCP icon will appear in your toolbar, and 34 browser tool
48
48
 
49
49
  If you don't want to use npm, download the extension directly:
50
50
 
51
- 1. [Download `browser-mcp-v1.23.0.zip`](https://github.com/Agent360dk/browser-mcp/releases/latest) from the latest GitHub release
51
+ 1. [Download `browser-mcp-v1.25.0.zip`](https://github.com/Agent360dk/browser-mcp/releases/latest) from the latest GitHub release
52
52
  2. Unzip the file (anywhere — e.g. `~/Downloads/browser-mcp-extension/`)
53
53
  3. Follow Step 2 above, but select the unzipped folder instead of `~/.browser-mcp/extension/`
54
54
  4. Configure Claude Code manually by adding this to your `~/.claude.json` (or run `npx @agent360/browser-mcp install --skip-extension`):
@@ -399,29 +399,62 @@ chrome.tabs.onRemoved.addListener((tabId) => {
399
399
  }
400
400
  });
401
401
 
402
+ // Physical-key `code` for a character, US layout. We used to build this as
403
+ // `Key${char.toUpperCase()}`, which is only correct for letters: "1" became "Key1",
404
+ // "@" became "Key@", " " became "Key ". Frameworks that branch on event.code —
405
+ // masked inputs, shortcut handlers, several React form libraries — see an unknown
406
+ // code and drop the keystroke, so typing an email or URL misbehaved on strict SPAs.
407
+ // Shifted symbols report the code of the physical key they sit on ("@" is Digit2).
408
+ const CDP_CHAR_CODES = {
409
+ ' ': 'Space', '\n': 'Enter', '\t': 'Tab',
410
+ '-': 'Minus', '_': 'Minus', '=': 'Equal', '+': 'Equal',
411
+ '[': 'BracketLeft', '{': 'BracketLeft', ']': 'BracketRight', '}': 'BracketRight',
412
+ '\\': 'Backslash', '|': 'Backslash', ';': 'Semicolon', ':': 'Semicolon',
413
+ "'": 'Quote', '"': 'Quote', ',': 'Comma', '<': 'Comma',
414
+ '.': 'Period', '>': 'Period', '/': 'Slash', '?': 'Slash',
415
+ '`': 'Backquote', '~': 'Backquote',
416
+ '!': 'Digit1', '@': 'Digit2', '#': 'Digit3', '$': 'Digit4', '%': 'Digit5',
417
+ '^': 'Digit6', '&': 'Digit7', '*': 'Digit8', '(': 'Digit9', ')': 'Digit0',
418
+ };
419
+ function cdpCodeForChar(ch) {
420
+ if (ch >= 'a' && ch <= 'z') return `Key${ch.toUpperCase()}`;
421
+ if (ch >= 'A' && ch <= 'Z') return `Key${ch}`;
422
+ if (ch >= '0' && ch <= '9') return `Digit${ch}`;
423
+ // Unknown (accented letters, CJK, emoji): omit it. CDP accepts a missing code,
424
+ // and an omitted code is honest where a fabricated one is actively misleading.
425
+ return CDP_CHAR_CODES[ch] || '';
426
+ }
427
+
428
+ // Types text as individual key events. Assumes the debugger is already attached —
429
+ // debuggerType() is the public wrapper that manages attach/detach.
430
+ async function typeCharsAttached(tabId, text) {
431
+ for (let i = 0; i < text.length; i++) {
432
+ const char = text[i];
433
+ const code = cdpCodeForChar(char);
434
+ await cdpSend(tabId, 'Input.dispatchKeyEvent', {
435
+ type: 'keyDown',
436
+ text: char,
437
+ key: char,
438
+ ...(code ? { code } : {}),
439
+ unmodifiedText: char,
440
+ });
441
+ await cdpSend(tabId, 'Input.dispatchKeyEvent', {
442
+ type: 'keyUp',
443
+ key: char,
444
+ ...(code ? { code } : {}),
445
+ });
446
+ // Human-like typing: random 30-120ms, occasional longer pause
447
+ const pause = (i > 0 && i % (7 + Math.floor(Math.random() * 5)) === 0)
448
+ ? 150 + Math.random() * 200 // thinking pause every ~10 chars
449
+ : 30 + Math.random() * 90; // normal keystroke
450
+ await new Promise(r => setTimeout(r, pause));
451
+ }
452
+ }
453
+
402
454
  async function debuggerType(tabId, text) {
403
455
  await debuggerAttach(tabId);
404
456
  try {
405
- for (let i = 0; i < text.length; i++) {
406
- const char = text[i];
407
- await cdpSend(tabId, 'Input.dispatchKeyEvent', {
408
- type: 'keyDown',
409
- text: char,
410
- key: char,
411
- code: `Key${char.toUpperCase()}`,
412
- unmodifiedText: char,
413
- });
414
- await cdpSend(tabId, 'Input.dispatchKeyEvent', {
415
- type: 'keyUp',
416
- key: char,
417
- code: `Key${char.toUpperCase()}`,
418
- });
419
- // Human-like typing: random 30-120ms, occasional longer pause
420
- const pause = (i > 0 && i % (7 + Math.floor(Math.random() * 5)) === 0)
421
- ? 150 + Math.random() * 200 // thinking pause every ~10 chars
422
- : 30 + Math.random() * 90; // normal keystroke
423
- await new Promise(r => setTimeout(r, pause));
424
- }
457
+ await typeCharsAttached(tabId, text);
425
458
  } finally {
426
459
  await debuggerDetach(tabId);
427
460
  }
@@ -539,6 +572,32 @@ async function debuggerFocus(tabId, selector) {
539
572
  }
540
573
  }
541
574
 
575
+ // Runtime.evaluate that leaves attach state alone. debuggerEval() detaches in its
576
+ // finally, which would pull the debugger out from under a fill that is mid-flight.
577
+ async function evalAttached(tabId, expression) {
578
+ const result = await cdpSend(tabId, 'Runtime.evaluate', { expression, returnByValue: true });
579
+ if (result.exceptionDetails) {
580
+ throw new Error(result.exceptionDetails.text || 'Script execution failed');
581
+ }
582
+ return result.result?.value;
583
+ }
584
+
585
+ // Select-all + Backspace. Assumes the debugger is already attached.
586
+ async function clearFieldAttached(tabId) {
587
+ await cdpSend(tabId, 'Input.dispatchKeyEvent', {
588
+ type: 'keyDown', key: 'a', code: 'KeyA', modifiers: SELECT_ALL_MODS,
589
+ });
590
+ await cdpSend(tabId, 'Input.dispatchKeyEvent', {
591
+ type: 'keyUp', key: 'a', code: 'KeyA',
592
+ });
593
+ await cdpSend(tabId, 'Input.dispatchKeyEvent', {
594
+ type: 'keyDown', key: 'Backspace', code: 'Backspace',
595
+ });
596
+ await cdpSend(tabId, 'Input.dispatchKeyEvent', {
597
+ type: 'keyUp', key: 'Backspace', code: 'Backspace',
598
+ });
599
+ }
600
+
542
601
  async function debuggerFill(tabId, selector, value) {
543
602
  // Check if element is contenteditable (rich text editors: LinkedIn, Slack)
544
603
  const isContentEditable = await debuggerEval(tabId, `
@@ -566,27 +625,39 @@ async function debuggerFill(tabId, selector, value) {
566
625
  return;
567
626
  }
568
627
 
569
- // Standard input/textarea — focus, clear, type
628
+ // Standard input/textarea — focus, clear, fill
570
629
  await debuggerFocus(tabId, selector);
571
630
  await debuggerAttach(tabId);
572
631
  try {
573
- // Select-all (Cmd+A on macOS, Ctrl+A elsewhere), then Backspace to clear
574
- await cdpSend(tabId, 'Input.dispatchKeyEvent', {
575
- type: 'keyDown', key: 'a', code: 'KeyA', modifiers: SELECT_ALL_MODS,
576
- });
577
- await cdpSend(tabId, 'Input.dispatchKeyEvent', {
578
- type: 'keyUp', key: 'a', code: 'KeyA',
579
- });
580
- await cdpSend(tabId, 'Input.dispatchKeyEvent', {
581
- type: 'keyDown', key: 'Backspace', code: 'Backspace',
582
- });
583
- await cdpSend(tabId, 'Input.dispatchKeyEvent', {
584
- type: 'keyUp', key: 'Backspace', code: 'Backspace',
585
- });
632
+ await clearFieldAttached(tabId);
633
+
634
+ // Fast path: one trusted InputEvent instead of N key events. This is the same
635
+ // primitive set_combobox and set_date already rely on, it avoids per-key `code`
636
+ // mapping entirely, and it turns a 40-character value from ~3 seconds of
637
+ // keystrokes into a single call — which also shrinks the window in which the
638
+ // debugger can detach mid-fill.
639
+ await cdpSend(tabId, 'Input.insertText', { text: value });
640
+
641
+ // Verify something actually landed. Masked inputs, maxlength enforcement and
642
+ // autocompletes that filter per keydown can swallow an inserted string, and
643
+ // until now that failed silently: the caller got "ok" and the field stayed
644
+ // empty. Only an EMPTY field triggers the fallback — a field that transformed
645
+ // the text (phone/date masks reformatting it) did accept the input, and
646
+ // retyping it per character would produce the same transform for no gain.
647
+ const landed = await evalAttached(tabId, `
648
+ (function() {
649
+ const el = document.querySelector(${JSON.stringify(selector)});
650
+ if (!el) return null;
651
+ return ('value' in el) ? el.value : el.textContent;
652
+ })()
653
+ `);
654
+ if (!landed) {
655
+ await clearFieldAttached(tabId);
656
+ await typeCharsAttached(tabId, value);
657
+ }
586
658
  } finally {
587
659
  await debuggerDetach(tabId);
588
660
  }
589
- await debuggerType(tabId, value);
590
661
  }
591
662
 
592
663
  async function debuggerEval(tabId, expression) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Agent360 Browser MCP",
4
- "version": "1.24.0",
4
+ "version": "1.25.0",
5
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent360/browser-mcp",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
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",