@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 +1 -1
- package/extension/background.js +106 -35
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
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.
|
|
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`):
|
package/extension/background.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
628
|
+
// Standard input/textarea — focus, clear, fill
|
|
570
629
|
await debuggerFocus(tabId, selector);
|
|
571
630
|
await debuggerAttach(tabId);
|
|
572
631
|
try {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
await cdpSend(tabId, 'Input.
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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) {
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Agent360 Browser MCP",
|
|
4
|
-
"version": "1.
|
|
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.
|
|
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",
|