chrome-bridge-mcp 1.9.0 → 1.10.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 +2 -2
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
- package/server/protocol.js +1 -1
- package/server/tools.js +198 -174
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Chrome Bridge
|
|
2
2
|
|
|
3
|
-
       [](https://chromewebstore.google.com/detail/chrome-bridge-for-claude/bioknpaeahidbelaljjohjofiloeodmb)
|
|
4
4
|
|
|
5
5
|
**Chrome Bridge is an MCP server that connects Claude Code to your real, logged-in Chrome browser — measured 2.75× fewer turns and 2.28× lower cost than the official "Claude in Chrome" extension on a form-filling task, with ~3× the toolset and no paid plan.**
|
|
6
6
|
|
|
@@ -63,7 +63,7 @@ Claude Code calls `navigate` (returning clickable element refs), `accessibility_
|
|
|
63
63
|
|
|
64
64
|
## Token-Efficient Design
|
|
65
65
|
|
|
66
|
-
- **Opt-in Surface**: 31 core tools cost ≈
|
|
66
|
+
- **Opt-in Surface**: 31 core tools cost ≈7.4k tokens of `tools/list`, all 60 cost ≈14.1k; specialized groups (audits, visual, network, storage, dom, files) are opt-in via `--caps`. That core figure was ≈3.8k in 1.8.0 and roughly doubled in 1.10.0: MCP annotations on every tool, rewritten descriptions, and a documented `.describe()` on all 254 parameters (coverage went from 35% to 100%). The trade is deliberate and it is not free — on the form benchmark it adds ≈8% to the cache-read tokens per turn — but the measured advantage was never prefix size, it was round trips: 2.75× fewer turns. Playwright MCP's 23 core tools measured ≈4.6k in July 2026, so on prefix size alone it is the leaner one. Measure ours with `npm run measure`.
|
|
67
67
|
- **Act-from-Result**: Tools attach a capped preview of interactive elements with short refs. Actions only report a `page_changed` delta when the URL or title actually changes.
|
|
68
68
|
- **Optimized Media**: Screenshots are downscaled to ≤1568px. Full-page captures are sliced into readable segments.
|
|
69
69
|
- **Zero-Token Escape Hatches**: The [CLI](#cli) skips MCP schemas entirely, and recorded flows [replay](#launch-mode-headless--ci) without any model in the loop.
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Chrome Bridge for Claude Code",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.10.0",
|
|
5
5
|
"description": "Bridge your browser to Claude Code: 60 token-efficient web-dev automation tools over a local WebSocket. ChromeOS included.",
|
|
6
6
|
"homepage_url": "https://github.com/frsorrentino/chrome-bridge",
|
|
7
7
|
"minimum_chrome_version": "135",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-bridge-mcp",
|
|
3
3
|
"mcpName": "io.github.frsorrentino/chrome-bridge",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.10.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "MCP server with 60 tools for browser automation via Chrome extension. Works on ChromeOS/Crostini.",
|
|
7
7
|
"main": "server/index.js",
|
package/server/protocol.js
CHANGED
package/server/tools.js
CHANGED
|
@@ -185,6 +185,18 @@ export const TOOL_CAPS = {
|
|
|
185
185
|
files: ['save_page', 'manage_downloads', 'extract_table', 'session_record'],
|
|
186
186
|
};
|
|
187
187
|
|
|
188
|
+
// Parametri ubiqui: un solo testo, così `tab_id` non significa una cosa in un
|
|
189
|
+
// tool e un'altra nel gemello. test/unit/tool-parameters.test.js lo verifica.
|
|
190
|
+
const selectorField = (extra = '') => z.string()
|
|
191
|
+
.describe(('CSS selector; ">>>" pierces shadow DOM. ' + extra).trim());
|
|
192
|
+
const waitAfter = z.enum(['none', 'navigation', 'networkidle']).optional().default('none')
|
|
193
|
+
.describe('Settle before returning: navigation waits for a page load, networkidle for quiet traffic');
|
|
194
|
+
|
|
195
|
+
const tabId = z.number().optional()
|
|
196
|
+
.describe('Target tab; omitted = last tab navigated in this session, else the active one');
|
|
197
|
+
const frameId = z.number().optional()
|
|
198
|
+
.describe('Target iframe id from get_frames; omitted = main frame');
|
|
199
|
+
|
|
188
200
|
const TOOL_TO_CAP = new Map();
|
|
189
201
|
for (const [group, names] of Object.entries(TOOL_CAPS)) {
|
|
190
202
|
for (const n of names) TOOL_TO_CAP.set(n, group);
|
|
@@ -472,8 +484,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
472
484
|
'navigate',
|
|
473
485
|
'Navigate a Chrome tab to a URL. Returns a preview of interactive elements with refs usable in click/type_text/hover.',
|
|
474
486
|
{
|
|
475
|
-
url: z.string(),
|
|
476
|
-
tab_id:
|
|
487
|
+
url: z.string().describe('Absolute URL, or a path resolved against the current page'),
|
|
488
|
+
tab_id: tabId,
|
|
477
489
|
},
|
|
478
490
|
async ({ url, tab_id }) => {
|
|
479
491
|
const data = await send(MessageType.NAVIGATE, { url, tab_id });
|
|
@@ -492,9 +504,11 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
492
504
|
// --- screenshot ---
|
|
493
505
|
server.tool(
|
|
494
506
|
'screenshot',
|
|
495
|
-
'Screenshot
|
|
507
|
+
'Screenshot of the visible viewport only (PNG), at the current scroll position. Read-only. '
|
|
508
|
+
+ 'Activates the tab in the background without stealing window focus, then restores the previous tab. '
|
|
509
|
+
+ 'Downscaled to ≤1568px, so fine print may not survive.',
|
|
496
510
|
{
|
|
497
|
-
tab_id:
|
|
511
|
+
tab_id: tabId,
|
|
498
512
|
},
|
|
499
513
|
async ({ tab_id }) => {
|
|
500
514
|
const data = await send(MessageType.SCREENSHOT, { tab_id });
|
|
@@ -522,9 +536,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
522
536
|
'execute_js',
|
|
523
537
|
'Run JavaScript in the page (MAIN world). Requires the extension\'s "Allow user scripts" toggle; errors explain setup if disabled.',
|
|
524
538
|
{
|
|
525
|
-
code: z.string(),
|
|
526
|
-
tab_id:
|
|
527
|
-
frame_id:
|
|
539
|
+
code: z.string().describe('JS evaluated in the page; the value of the last expression is returned'),
|
|
540
|
+
tab_id: tabId,
|
|
541
|
+
frame_id: frameId,
|
|
528
542
|
max_length: z.number().optional().default(20000).describe('Max output chars'),
|
|
529
543
|
},
|
|
530
544
|
async ({ code, tab_id, frame_id, max_length }) => {
|
|
@@ -545,12 +559,12 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
545
559
|
+ 'pointer sequence, so it can submit, open a dialog or navigate away — use wait_after to let that settle. '
|
|
546
560
|
+ 'Not idempotent, and a click that triggers a native confirm() blocks the bridge: install handle_dialogs first.',
|
|
547
561
|
{
|
|
548
|
-
selector: z.string().optional(),
|
|
562
|
+
selector: z.string().optional().describe('CSS selector; ">>>" pierces shadow DOM. Ignored when ref is given'),
|
|
549
563
|
ref: z.string().optional().describe('From get_interactives, e.g. "n3"'),
|
|
550
564
|
force: z.boolean().optional().default(false).describe('Click even if occluded'),
|
|
551
|
-
wait_after:
|
|
552
|
-
tab_id:
|
|
553
|
-
frame_id:
|
|
565
|
+
wait_after: waitAfter,
|
|
566
|
+
tab_id: tabId,
|
|
567
|
+
frame_id: frameId,
|
|
554
568
|
},
|
|
555
569
|
async ({ selector, ref, force, wait_after, tab_id, frame_id }) => {
|
|
556
570
|
const target = resolveTarget(selector, ref, tab_id);
|
|
@@ -574,13 +588,13 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
574
588
|
'type_text',
|
|
575
589
|
'Type text into an input, by CSS selector or by ref from get_interactives.',
|
|
576
590
|
{
|
|
577
|
-
selector: z.string().optional(),
|
|
591
|
+
selector: z.string().optional().describe('CSS selector; ">>>" pierces shadow DOM. Ignored when ref is given'),
|
|
578
592
|
ref: z.string().optional().describe('From get_interactives, e.g. "n3"'),
|
|
579
|
-
text: z.string(),
|
|
593
|
+
text: z.string().describe('Value to type; empty string clears the field'),
|
|
580
594
|
mode: z.enum(['set', 'keys']).optional().default('set').describe('set = assign value; keys = per-char events (autocomplete/masked)'),
|
|
581
|
-
wait_after:
|
|
582
|
-
tab_id:
|
|
583
|
-
frame_id:
|
|
595
|
+
wait_after: waitAfter,
|
|
596
|
+
tab_id: tabId,
|
|
597
|
+
frame_id: frameId,
|
|
584
598
|
},
|
|
585
599
|
async ({ selector, ref, text, mode, wait_after, tab_id, frame_id }) => {
|
|
586
600
|
const target = resolveTarget(selector, ref, tab_id);
|
|
@@ -603,9 +617,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
603
617
|
+ 'HTML on a big table costs tens of thousands of tokens for data you then filter anyway — prefer '
|
|
604
618
|
+ 'extract_table or extract for tabular and repeated content, and get_interactives to find click targets.',
|
|
605
619
|
{
|
|
606
|
-
mode: z.enum(['text', 'html', 'accessibility']).default('text'),
|
|
607
|
-
tab_id:
|
|
608
|
-
frame_id:
|
|
620
|
+
mode: z.enum(['text', 'html', 'accessibility']).default('text').describe('text strips markup, html keeps it (costly), accessibility returns the a11y tree'),
|
|
621
|
+
tab_id: tabId,
|
|
622
|
+
frame_id: frameId,
|
|
609
623
|
max_length: z.number().optional().default(50000).describe('Max output chars'),
|
|
610
624
|
},
|
|
611
625
|
async ({ mode, tab_id, frame_id, max_length }) => {
|
|
@@ -624,8 +638,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
624
638
|
'get_page_info',
|
|
625
639
|
'Get page metadata: meta tags, scripts, stylesheets, links, and forms',
|
|
626
640
|
{
|
|
627
|
-
tab_id:
|
|
628
|
-
frame_id:
|
|
641
|
+
tab_id: tabId,
|
|
642
|
+
frame_id: frameId,
|
|
629
643
|
},
|
|
630
644
|
async ({ tab_id, frame_id }) => {
|
|
631
645
|
const data = await send(MessageType.GET_PAGE_INFO, { tab_id, frame_id });
|
|
@@ -645,8 +659,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
645
659
|
+ 'Cookies come with domain, path, expiry and the httpOnly/secure flags, so this also answers whether a '
|
|
646
660
|
+ 'session cookie is present — write them back with set_storage, or snapshot a logged-in state with session_fixture.',
|
|
647
661
|
{
|
|
648
|
-
type: z.enum(['all', 'localStorage', 'sessionStorage', 'cookies']).default('all'),
|
|
649
|
-
tab_id:
|
|
662
|
+
type: z.enum(['all', 'localStorage', 'sessionStorage', 'cookies']).default('all').describe('all returns the three together'),
|
|
663
|
+
tab_id: tabId,
|
|
650
664
|
},
|
|
651
665
|
async ({ type, tab_id }) => {
|
|
652
666
|
const data = await send(MessageType.GET_STORAGE, { type, tab_id });
|
|
@@ -664,9 +678,10 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
664
678
|
'get_performance',
|
|
665
679
|
'Navigation timing, paint metrics, JS heap size and per-resource load times, as measured since the current '
|
|
666
680
|
+ 'document loaded. Read-only, no reload triggered — numbers are only meaningful once loading has settled, '
|
|
667
|
-
+ 'so on a page still fetching
|
|
681
|
+
+ 'so on a page still fetching wait for network idle first. Covers how fast the document arrived, '
|
|
682
|
+
+ 'not the layout-shift and interaction metrics that accumulate afterwards.',
|
|
668
683
|
{
|
|
669
|
-
tab_id:
|
|
684
|
+
tab_id: tabId,
|
|
670
685
|
},
|
|
671
686
|
async ({ tab_id }) => {
|
|
672
687
|
const data = await send(MessageType.GET_PERFORMANCE, { tab_id });
|
|
@@ -684,11 +699,11 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
684
699
|
'query_dom',
|
|
685
700
|
'Query DOM elements by CSS selector, returning structure, attributes, bounding rect, and computed styles.',
|
|
686
701
|
{
|
|
687
|
-
selector: z.string(),
|
|
702
|
+
selector: z.string().describe('CSS selector; ">>>" pierces shadow DOM. Matches all, not just the first'),
|
|
688
703
|
properties: z.array(z.string()).optional().describe('Computed styles to include, e.g. ["color"]'),
|
|
689
|
-
limit: z.number().optional().default(50),
|
|
690
|
-
tab_id:
|
|
691
|
-
frame_id:
|
|
704
|
+
limit: z.number().optional().default(50).describe('Max elements returned, from the top of the match list'),
|
|
705
|
+
tab_id: tabId,
|
|
706
|
+
frame_id: frameId,
|
|
692
707
|
},
|
|
693
708
|
async ({ selector, properties, limit, tab_id, frame_id }) => {
|
|
694
709
|
const data = await send(MessageType.QUERY_DOM, { selector, properties, limit, tab_id, frame_id });
|
|
@@ -708,13 +723,13 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
708
723
|
+ 'setTextContent. Nothing is persisted — the next reload restores the page as the server sends it. '
|
|
709
724
|
+ 'For styling many elements at once inject_css is one call instead of N.',
|
|
710
725
|
{
|
|
711
|
-
selector: z.string(),
|
|
712
|
-
action: z.enum(['setAttribute', 'removeAttribute', 'addClass', 'removeClass', 'setStyle', 'setTextContent']),
|
|
726
|
+
selector: z.string().describe('CSS selector; ">>>" pierces shadow DOM. Only the first match is changed'),
|
|
727
|
+
action: z.enum(['setAttribute', 'removeAttribute', 'addClass', 'removeClass', 'setStyle', 'setTextContent']).describe('setStyle takes a CSS declaration in value; addClass/removeClass take className'),
|
|
713
728
|
name: z.string().optional().describe('Attribute name'),
|
|
714
|
-
value: z.string().optional(),
|
|
715
|
-
className: z.string().optional(),
|
|
716
|
-
tab_id:
|
|
717
|
-
frame_id:
|
|
729
|
+
value: z.string().optional().describe('Attribute value, style declaration, or text, per action'),
|
|
730
|
+
className: z.string().optional().describe('Class to add or remove (addClass/removeClass)'),
|
|
731
|
+
tab_id: tabId,
|
|
732
|
+
frame_id: frameId,
|
|
718
733
|
},
|
|
719
734
|
async ({ selector, action, name, value, className, tab_id, frame_id }) => {
|
|
720
735
|
const data = await send(MessageType.MODIFY_DOM, { selector, action, name, value, className, tab_id, frame_id });
|
|
@@ -733,8 +748,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
733
748
|
'Inject a CSS rule into the page. Stays until the next navigation or reload, and re-injecting the same id '
|
|
734
749
|
+ 'replaces it rather than stacking. Affects only what is rendered — the stylesheet of the site is untouched.',
|
|
735
750
|
{
|
|
736
|
-
css: z.string(),
|
|
737
|
-
tab_id:
|
|
751
|
+
css: z.string().describe('One or more CSS rules, as they would appear in a stylesheet'),
|
|
752
|
+
tab_id: tabId,
|
|
738
753
|
},
|
|
739
754
|
async ({ css, tab_id }) => {
|
|
740
755
|
const data = await send(MessageType.INJECT_CSS, { css, tab_id });
|
|
@@ -753,10 +768,10 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
753
768
|
'Read console messages captured since page load, incl. uncaught errors and unhandled rejections.',
|
|
754
769
|
{
|
|
755
770
|
clear: z.boolean().optional().default(false).describe('Clear buffer after read'),
|
|
756
|
-
level: z.enum(['all', 'log', 'warn', 'error', 'info', 'debug']).optional().default('all'),
|
|
771
|
+
level: z.enum(['all', 'log', 'warn', 'error', 'info', 'debug']).optional().default('all').describe('all merges every level in one chronological list'),
|
|
757
772
|
limit: z.number().optional().default(50).describe('Most recent; buffer 1000'),
|
|
758
|
-
format: z.enum(['lines', 'json']).optional().default('lines'),
|
|
759
|
-
tab_id:
|
|
773
|
+
format: z.enum(['lines', 'json']).optional().default('lines').describe('lines is compact; json keeps timestamps and stack traces'),
|
|
774
|
+
tab_id: tabId,
|
|
760
775
|
},
|
|
761
776
|
async ({ clear, level, limit, format, tab_id }) => {
|
|
762
777
|
// limit va all'estensione: taglia in pagina e cancella (con clear) solo
|
|
@@ -788,10 +803,10 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
788
803
|
'Monitor network requests. source=page: XHR/fetch hook (installed on first call); source=browser: all requests incl. static assets. format=har exports HAR 1.2.',
|
|
789
804
|
{
|
|
790
805
|
clear: z.boolean().optional().default(false).describe('Clear buffer after read'),
|
|
791
|
-
source: z.enum(['page', 'browser']).optional().default('page'),
|
|
792
|
-
format: z.enum(['lines', 'json', 'har']).optional().default('lines'),
|
|
806
|
+
source: z.enum(['page', 'browser']).optional().default('page').describe('page sees XHR/fetch only; browser also sees static assets'),
|
|
807
|
+
format: z.enum(['lines', 'json', 'har']).optional().default('lines').describe('har exports HAR 1.2 for external tooling'),
|
|
793
808
|
limit: z.number().optional().default(100).describe('Most recent; buffer 1000'),
|
|
794
|
-
tab_id:
|
|
809
|
+
tab_id: tabId,
|
|
795
810
|
},
|
|
796
811
|
async ({ clear, source, format, limit, tab_id }) => {
|
|
797
812
|
// limit va all'estensione (taglia in pagina, clear solo del restituito);
|
|
@@ -827,7 +842,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
827
842
|
+ 'Each call creates another tab: reuse a tab_id or navigate() to move an existing one instead of piling up tabs.',
|
|
828
843
|
{
|
|
829
844
|
url: z.string().optional().describe('URL to open (default: new tab page)'),
|
|
830
|
-
active: z.boolean().optional().default(true),
|
|
845
|
+
active: z.boolean().optional().default(true).describe('false opens the tab in the background, leaving the current one focused'),
|
|
831
846
|
},
|
|
832
847
|
async ({ url, active }) => {
|
|
833
848
|
const data = await send(MessageType.CREATE_TAB, { url, active });
|
|
@@ -846,7 +861,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
846
861
|
'wait_for',
|
|
847
862
|
'Wait for: element (selector in DOM), function (JS expression truthy; needs "Allow user scripts"), navigation (mode=spa for route changes), network_idle.',
|
|
848
863
|
{
|
|
849
|
-
condition: z.enum(['element', 'function', 'navigation', 'network_idle']),
|
|
864
|
+
condition: z.enum(['element', 'function', 'navigation', 'network_idle']).describe('function needs expression; element needs selector'),
|
|
850
865
|
selector: z.string().optional().describe('condition=element'),
|
|
851
866
|
expression: z.string().optional().describe('JS expression (condition=function)'),
|
|
852
867
|
visible: z.boolean().optional().default(false).describe('Element must also be visible'),
|
|
@@ -854,8 +869,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
854
869
|
idle_ms: z.number().optional().default(500).describe('Quiet period ms (network_idle)'),
|
|
855
870
|
timeout: z.number().optional().describe('Max ms (default 10000; 15000 navigation/network_idle)'),
|
|
856
871
|
interval: z.number().optional().describe('Poll ms, min 50'),
|
|
857
|
-
tab_id:
|
|
858
|
-
frame_id:
|
|
872
|
+
tab_id: tabId,
|
|
873
|
+
frame_id: frameId,
|
|
859
874
|
},
|
|
860
875
|
async ({ condition, selector, expression, visible, mode, idle_ms, timeout, interval, tab_id, frame_id }) => {
|
|
861
876
|
let data;
|
|
@@ -885,18 +900,18 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
885
900
|
'scroll',
|
|
886
901
|
'Scroll. action=to: once to element/coordinates; action=until: repeatedly until element visible, network idle, or no new content (infinite scroll).',
|
|
887
902
|
{
|
|
888
|
-
action: z.enum(['to', 'until']).optional().default('to'),
|
|
903
|
+
action: z.enum(['to', 'until']).optional().default('to').describe('to jumps to a position or element; until scrolls repeatedly to load more'),
|
|
889
904
|
selector: z.string().optional().describe('Target (to) or stop element (until=element)'),
|
|
890
|
-
x: z.number().optional(),
|
|
891
|
-
y: z.number().optional(),
|
|
892
|
-
behavior: z.enum(['smooth', 'instant', 'auto']).optional().default('auto'),
|
|
905
|
+
x: z.number().optional().describe('Absolute horizontal position in px (action=to, without selector)'),
|
|
906
|
+
y: z.number().optional().describe('Absolute vertical position in px (action=to, without selector)'),
|
|
907
|
+
behavior: z.enum(['smooth', 'instant', 'auto']).optional().default('auto').describe('instant avoids waiting for smooth-scroll animations'),
|
|
893
908
|
offset_y: z.number().optional().default(0).describe('px offset for fixed headers (to)'),
|
|
894
|
-
until: z.enum(['element', 'network_idle', 'no_new_content']).optional().default('no_new_content'),
|
|
895
|
-
max_scrolls: z.number().optional().default(20),
|
|
909
|
+
until: z.enum(['element', 'network_idle', 'no_new_content']).optional().default('no_new_content').describe('no_new_content stops when the page height stops growing'),
|
|
910
|
+
max_scrolls: z.number().optional().default(20).describe('Cap on scroll steps, so an infinite feed terminates'),
|
|
896
911
|
step_px: z.number().optional().describe('px per step, default viewport height'),
|
|
897
912
|
settle_ms: z.number().optional().default(400).describe('Pause ms after each step'),
|
|
898
|
-
tab_id:
|
|
899
|
-
frame_id:
|
|
913
|
+
tab_id: tabId,
|
|
914
|
+
frame_id: frameId,
|
|
900
915
|
},
|
|
901
916
|
async ({ action, selector, x, y, behavior, offset_y, until, max_scrolls, step_px, settle_ms, tab_id, frame_id }) => {
|
|
902
917
|
const data = (action ?? 'to') === 'until'
|
|
@@ -913,17 +928,17 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
913
928
|
+ 'action=clear wipes every entry of that type for the origin and cannot be undone — on a site the user is '
|
|
914
929
|
+ 'logged into, clearing cookies logs them out. Read the current values first with get_storage.',
|
|
915
930
|
{
|
|
916
|
-
type: z.enum(['localStorage', 'sessionStorage', 'cookie']),
|
|
917
|
-
action: z.enum(['set', 'delete', 'clear']),
|
|
931
|
+
type: z.enum(['localStorage', 'sessionStorage', 'cookie']).describe('cookie writes a real cookie, not a storage key'),
|
|
932
|
+
action: z.enum(['set', 'delete', 'clear']).describe('clear ignores key and wipes every entry of that type'),
|
|
918
933
|
key: z.string().optional().describe('Required for set/delete'),
|
|
919
|
-
value: z.string().optional(),
|
|
934
|
+
value: z.string().optional().describe('Required for action=set'),
|
|
920
935
|
path: z.string().optional().describe('Cookie path (default /)'),
|
|
921
936
|
domain: z.string().optional().describe('Cookie domain'),
|
|
922
937
|
expires: z.string().optional().describe('UTC date string (cookie)'),
|
|
923
|
-
secure: z.boolean().optional(),
|
|
924
|
-
sameSite: z.enum(['Strict', 'Lax', 'None']).optional(),
|
|
925
|
-
http_only: z.boolean().optional(),
|
|
926
|
-
tab_id:
|
|
938
|
+
secure: z.boolean().optional().describe('Cookie sent over HTTPS only'),
|
|
939
|
+
sameSite: z.enum(['Strict', 'Lax', 'None']).optional().describe('None requires secure=true'),
|
|
940
|
+
http_only: z.boolean().optional().describe('Cookie hidden from page JS'),
|
|
941
|
+
tab_id: tabId,
|
|
927
942
|
},
|
|
928
943
|
async ({ type, action, key, value, path, domain, expires, secure, sameSite, http_only, tab_id }) => {
|
|
929
944
|
const data = await send(MessageType.SET_STORAGE, { type, action, key, value, path, domain, expires, secure, sameSite, http_only, tab_id });
|
|
@@ -946,9 +961,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
946
961
|
value: z.string(),
|
|
947
962
|
})).describe('{selector, value} pairs'),
|
|
948
963
|
submit_selector: z.string().optional().describe('Submit button to click after filling'),
|
|
949
|
-
wait_after:
|
|
950
|
-
tab_id:
|
|
951
|
-
frame_id:
|
|
964
|
+
wait_after: waitAfter,
|
|
965
|
+
tab_id: tabId,
|
|
966
|
+
frame_id: frameId,
|
|
952
967
|
},
|
|
953
968
|
async ({ fields, submit_selector, wait_after, tab_id, frame_id }) => {
|
|
954
969
|
const before = await tabSnapshot(tab_id);
|
|
@@ -973,7 +988,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
973
988
|
preset: z.enum(['mobile', 'tablet', 'desktop']).optional().describe('375x812, 768x1024, 1440x900'),
|
|
974
989
|
width: z.number().optional().describe('Overrides preset'),
|
|
975
990
|
height: z.number().optional().describe('Overrides preset'),
|
|
976
|
-
tab_id:
|
|
991
|
+
tab_id: tabId,
|
|
977
992
|
},
|
|
978
993
|
async ({ preset, width, height, tab_id }) => {
|
|
979
994
|
const data = await send(MessageType.VIEWPORT_RESIZE, { preset, width, height, tab_id });
|
|
@@ -989,10 +1004,12 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
989
1004
|
// --- element_screenshot ---
|
|
990
1005
|
server.tool(
|
|
991
1006
|
'element_screenshot',
|
|
992
|
-
'Screenshot
|
|
1007
|
+
'Screenshot cropped to one element (PNG), scrolled into view first. Read-only. '
|
|
1008
|
+
+ 'The cheapest image in the set, because it carries only the box you asked for: '
|
|
1009
|
+
+ 'a component, a chart, a table cell.',
|
|
993
1010
|
{
|
|
994
|
-
selector: z.string(),
|
|
995
|
-
tab_id:
|
|
1011
|
+
selector: z.string().describe('CSS selector; ">>>" pierces shadow DOM. The element is scrolled into view first'),
|
|
1012
|
+
tab_id: tabId,
|
|
996
1013
|
},
|
|
997
1014
|
async ({ selector, tab_id }) => {
|
|
998
1015
|
const data = await send(MessageType.ELEMENT_SCREENSHOT, { selector, tab_id });
|
|
@@ -1011,12 +1028,12 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1011
1028
|
+ 'max_segments are returned and the rest need segment_offset. On a long page, reading the text you actually need '
|
|
1012
1029
|
+ 'is cheaper by an order of magnitude — prefer read_page, extract or find_text unless the layout itself is the question.',
|
|
1013
1030
|
{
|
|
1014
|
-
max_scrolls: z.number().optional().default(20),
|
|
1031
|
+
max_scrolls: z.number().optional().default(20).describe('Cap on scroll steps: a taller page is captured only up to here'),
|
|
1015
1032
|
delay: z.number().optional().default(500).describe('ms between captures (min 500, Chrome quota)'),
|
|
1016
1033
|
stitch: z.boolean().optional().default(true).describe('false = one image per viewport'),
|
|
1017
1034
|
max_segments: z.number().optional().default(3).describe('Images returned, from the top (each ≈2.7k image tokens); raise or use segment_offset for the rest'),
|
|
1018
1035
|
segment_offset: z.number().optional().default(0).describe('Skip the first N segments'),
|
|
1019
|
-
tab_id:
|
|
1036
|
+
tab_id: tabId,
|
|
1020
1037
|
},
|
|
1021
1038
|
async ({ max_scrolls, delay, stitch, max_segments, segment_offset, tab_id }) => {
|
|
1022
1039
|
const data = await send(MessageType.FULL_PAGE_SCREENSHOT, { max_scrolls, delay, stitch, tab_id });
|
|
@@ -1057,12 +1074,12 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1057
1074
|
'highlight_elements',
|
|
1058
1075
|
'Add colored overlay on elements matching a CSS selector. remove=true clears all highlights.',
|
|
1059
1076
|
{
|
|
1060
|
-
selector: z.string().optional(),
|
|
1061
|
-
color: z.string().optional().default('rgba(255,0,0,0.3)'),
|
|
1062
|
-
border: z.string().optional().default('2px solid red'),
|
|
1077
|
+
selector: z.string().optional().describe('CSS selector; ">>>" pierces shadow DOM. Every match is outlined'),
|
|
1078
|
+
color: z.string().optional().default('rgba(255,0,0,0.3)').describe('Any CSS color for the overlay label'),
|
|
1079
|
+
border: z.string().optional().default('2px solid red').describe('CSS border shorthand, e.g. "2px solid red"'),
|
|
1063
1080
|
label: z.boolean().optional().default(false).describe('Show tag.class (WxH) label'),
|
|
1064
|
-
remove: z.boolean().optional().default(false),
|
|
1065
|
-
tab_id:
|
|
1081
|
+
remove: z.boolean().optional().default(false).describe('Remove previously injected highlights instead of adding'),
|
|
1082
|
+
tab_id: tabId,
|
|
1066
1083
|
},
|
|
1067
1084
|
async ({ selector, color, border, label, remove, tab_id }) => {
|
|
1068
1085
|
const data = await send(MessageType.HIGHLIGHT_ELEMENTS, { selector, color, border, label, remove, tab_id });
|
|
@@ -1081,8 +1098,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1081
1098
|
'A11y audit: missing alt, empty links, heading hierarchy, ARIA, contrast (approximate), form labels.',
|
|
1082
1099
|
{
|
|
1083
1100
|
scope: z.string().optional().describe('Limit scope (CSS selector)'),
|
|
1084
|
-
checks: z.array(z.enum(['images', 'links', 'headings', 'aria', 'contrast', 'forms', 'all'])).optional().default(['all']),
|
|
1085
|
-
tab_id:
|
|
1101
|
+
checks: z.array(z.enum(['images', 'links', 'headings', 'aria', 'contrast', 'forms', 'all'])).optional().default(['all']).describe('Subset to run; fewer checks means a shorter answer'),
|
|
1102
|
+
tab_id: tabId,
|
|
1086
1103
|
},
|
|
1087
1104
|
async ({ scope, checks, tab_id }) => {
|
|
1088
1105
|
const data = await send(MessageType.ACCESSIBILITY_AUDIT, { scope, checks, tab_id });
|
|
@@ -1100,12 +1117,12 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1100
1117
|
'check_links',
|
|
1101
1118
|
'Check page links for broken URLs, verified server-side (no CORS limits, real HTTP status).',
|
|
1102
1119
|
{
|
|
1103
|
-
scope: z.enum(['same-origin', 'all', 'external']).optional().default('all'),
|
|
1104
|
-
selector: z.string().optional().default('a[href]'),
|
|
1120
|
+
scope: z.enum(['same-origin', 'all', 'external']).optional().default('all').describe('same-origin skips third-party links, which are the slow ones'),
|
|
1121
|
+
selector: z.string().optional().default('a[href]').describe('CSS selector; ">>>" pierces shadow DOM. Restricts which links are collected'),
|
|
1105
1122
|
timeout: z.number().optional().default(5000).describe('Per-link ms'),
|
|
1106
|
-
max_links: z.number().optional().default(50),
|
|
1107
|
-
format: z.enum(['lines', 'json']).optional().default('lines'),
|
|
1108
|
-
tab_id:
|
|
1123
|
+
max_links: z.number().optional().default(50).describe('Cap on links fetched: each one is a real HTTP request'),
|
|
1124
|
+
format: z.enum(['lines', 'json']).optional().default('lines').describe('lines is compact; json keeps per-link status and timing'),
|
|
1125
|
+
tab_id: tabId,
|
|
1109
1126
|
},
|
|
1110
1127
|
async ({ scope, selector, timeout, max_links, format, tab_id }) => {
|
|
1111
1128
|
const data = await send(MessageType.COLLECT_LINKS, { scope, selector, max_links, tab_id });
|
|
@@ -1136,9 +1153,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1136
1153
|
+ 'Read-only. Values come from the current layout, so zoom and viewport size change them: set_zoom(1) and a '
|
|
1137
1154
|
+ 'fixed viewport_resize make results comparable across runs.',
|
|
1138
1155
|
{
|
|
1139
|
-
selector1: z.string(),
|
|
1140
|
-
selector2: z.string(),
|
|
1141
|
-
tab_id:
|
|
1156
|
+
selector1: z.string().describe('First element; distances are measured from its box'),
|
|
1157
|
+
selector2: z.string().describe('Second element; ">>>" pierces shadow DOM'),
|
|
1158
|
+
tab_id: tabId,
|
|
1142
1159
|
},
|
|
1143
1160
|
async ({ selector1, selector2, tab_id }) => {
|
|
1144
1161
|
const data = await send(MessageType.MEASURE_SPACING, { selector1, selector2, tab_id });
|
|
@@ -1156,15 +1173,15 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1156
1173
|
'watch_dom',
|
|
1157
1174
|
'Watch DOM mutations (MutationObserver). First call installs the watcher; later calls read accumulated mutations.',
|
|
1158
1175
|
{
|
|
1159
|
-
selector: z.string().optional().default('body'),
|
|
1160
|
-
attributes: z.boolean().optional().default(true),
|
|
1161
|
-
childList: z.boolean().optional().default(true),
|
|
1162
|
-
characterData: z.boolean().optional().default(false),
|
|
1163
|
-
subtree: z.boolean().optional().default(true),
|
|
1176
|
+
selector: z.string().optional().default('body').describe('Subtree to observe; ">>>" pierces shadow DOM'),
|
|
1177
|
+
attributes: z.boolean().optional().default(true).describe('Report attribute changes'),
|
|
1178
|
+
childList: z.boolean().optional().default(true).describe('Report added and removed children'),
|
|
1179
|
+
characterData: z.boolean().optional().default(false).describe('Report text content changes'),
|
|
1180
|
+
subtree: z.boolean().optional().default(true).describe('Observe descendants too, not just the matched node'),
|
|
1164
1181
|
clear: z.boolean().optional().default(false).describe('Clear buffer after read'),
|
|
1165
1182
|
stop: z.boolean().optional().default(false).describe('Disconnect observer'),
|
|
1166
1183
|
limit: z.number().optional().default(100).describe('Most recent mutations; buffer 1000'),
|
|
1167
|
-
tab_id:
|
|
1184
|
+
tab_id: tabId,
|
|
1168
1185
|
},
|
|
1169
1186
|
async ({ selector, attributes, childList, characterData, subtree, clear, stop, limit, tab_id }) => {
|
|
1170
1187
|
const data = await send(MessageType.WATCH_DOM, { selector, attributes, childList, characterData, subtree, clear, stop, limit, tab_id });
|
|
@@ -1185,12 +1202,12 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1185
1202
|
+ 'user_agent only changes what page JS reads — the request header is a separate thing, set it with '
|
|
1186
1203
|
+ 'network_rules(action=modify_header, header="User-Agent"). Pair with viewport_resize to emulate a device.',
|
|
1187
1204
|
{
|
|
1188
|
-
colorScheme: z.enum(['dark', 'light', 'no-preference']).optional(),
|
|
1189
|
-
reducedMotion: z.enum(['reduce', 'no-preference']).optional(),
|
|
1190
|
-
printMode: z.boolean().optional().default(false),
|
|
1205
|
+
colorScheme: z.enum(['dark', 'light', 'no-preference']).optional().describe('Value reported to prefers-color-scheme queries'),
|
|
1206
|
+
reducedMotion: z.enum(['reduce', 'no-preference']).optional().describe('Value reported to prefers-reduced-motion queries'),
|
|
1207
|
+
printMode: z.boolean().optional().default(false).describe('Make print media queries match, without opening a print dialog'),
|
|
1191
1208
|
user_agent: z.string().optional().describe('Overrides navigator.userAgent and appVersion in the page (not the HTTP header)'),
|
|
1192
1209
|
reset: z.boolean().optional().default(false).describe('Remove all emulations'),
|
|
1193
|
-
tab_id:
|
|
1210
|
+
tab_id: tabId,
|
|
1194
1211
|
},
|
|
1195
1212
|
async ({ colorScheme, reducedMotion, printMode, user_agent, reset, tab_id }) => {
|
|
1196
1213
|
const data = await send(MessageType.EMULATE_MEDIA, { colorScheme, reducedMotion, printMode, user_agent, reset, tab_id });
|
|
@@ -1208,10 +1225,10 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1208
1225
|
'hover',
|
|
1209
1226
|
'Hover over an element (mouseenter/mouseover), by CSS selector or ref.',
|
|
1210
1227
|
{
|
|
1211
|
-
selector: z.string().optional(),
|
|
1228
|
+
selector: z.string().optional().describe('CSS selector; ">>>" pierces shadow DOM. Triggers CSS and JS hover handlers'),
|
|
1212
1229
|
ref: z.string().optional().describe('From get_interactives'),
|
|
1213
|
-
tab_id:
|
|
1214
|
-
frame_id:
|
|
1230
|
+
tab_id: tabId,
|
|
1231
|
+
frame_id: frameId,
|
|
1215
1232
|
},
|
|
1216
1233
|
async ({ selector, ref, tab_id, frame_id }) => {
|
|
1217
1234
|
const data = await send(MessageType.HOVER, { selector: resolveTarget(selector, ref, tab_id), tab_id, frame_id });
|
|
@@ -1233,12 +1250,12 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1233
1250
|
{
|
|
1234
1251
|
key: z.string().describe('e.g. "Enter", "Escape", "Tab", "ArrowDown"'),
|
|
1235
1252
|
selector: z.string().optional().describe('Target (default: activeElement)'),
|
|
1236
|
-
ctrl: z.boolean().optional().default(false),
|
|
1237
|
-
shift: z.boolean().optional().default(false),
|
|
1238
|
-
alt: z.boolean().optional().default(false),
|
|
1239
|
-
meta: z.boolean().optional().default(false),
|
|
1240
|
-
tab_id:
|
|
1241
|
-
frame_id:
|
|
1253
|
+
ctrl: z.boolean().optional().default(false).describe('Hold Control'),
|
|
1254
|
+
shift: z.boolean().optional().default(false).describe('Hold Shift'),
|
|
1255
|
+
alt: z.boolean().optional().default(false).describe('Hold Alt'),
|
|
1256
|
+
meta: z.boolean().optional().default(false).describe('Hold Meta (Command/Windows)'),
|
|
1257
|
+
tab_id: tabId,
|
|
1258
|
+
frame_id: frameId,
|
|
1242
1259
|
},
|
|
1243
1260
|
async ({ key, selector, ctrl, shift, alt, meta, tab_id, frame_id }) => {
|
|
1244
1261
|
const data = await send(MessageType.PRESS_KEY, { key, selector, ctrl, shift, alt, meta, tab_id, frame_id });
|
|
@@ -1256,7 +1273,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1256
1273
|
'get_frames',
|
|
1257
1274
|
'List frames (main + iframes) with frameId, parent, URL — for the frame_id parameter of DOM tools.',
|
|
1258
1275
|
{
|
|
1259
|
-
tab_id:
|
|
1276
|
+
tab_id: tabId,
|
|
1260
1277
|
},
|
|
1261
1278
|
async ({ tab_id }) => {
|
|
1262
1279
|
const data = await send(MessageType.GET_FRAMES, { tab_id });
|
|
@@ -1271,9 +1288,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1271
1288
|
+ 'tab and anything unsaved in it, and cannot be undone — it may be a tab the user is working in. '
|
|
1272
1289
|
+ 'reload and navigation drop injected CSS, emulations and page hooks.',
|
|
1273
1290
|
{
|
|
1274
|
-
action: z.enum(['close', 'activate', 'reload', 'back', 'forward']),
|
|
1291
|
+
action: z.enum(['close', 'activate', 'reload', 'back', 'forward']).describe('close cannot be undone; reload drops injected CSS and page hooks'),
|
|
1275
1292
|
bypass_cache: z.boolean().optional().default(false).describe('reload only'),
|
|
1276
|
-
tab_id:
|
|
1293
|
+
tab_id: tabId,
|
|
1277
1294
|
},
|
|
1278
1295
|
async ({ action, bypass_cache, tab_id }) => {
|
|
1279
1296
|
const data = await send(MessageType.TAB_ACTION, { action, bypass_cache, tab_id });
|
|
@@ -1288,10 +1305,10 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1288
1305
|
'upload_file',
|
|
1289
1306
|
'Set a file on input[type=file] from the server filesystem via DataTransfer (max 10MB).',
|
|
1290
1307
|
{
|
|
1291
|
-
selector: z.string(),
|
|
1308
|
+
selector: z.string().describe('The file input to fill; ">>>" pierces shadow DOM'),
|
|
1292
1309
|
path: z.string().describe('Absolute path on the server machine'),
|
|
1293
1310
|
mime_type: z.string().optional().describe('Default: inferred from extension'),
|
|
1294
|
-
tab_id:
|
|
1311
|
+
tab_id: tabId,
|
|
1295
1312
|
},
|
|
1296
1313
|
async ({ selector, path, mime_type, tab_id }) => {
|
|
1297
1314
|
const buf = await readFile(path);
|
|
@@ -1309,7 +1326,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1309
1326
|
'dismiss_overlays',
|
|
1310
1327
|
'Dismiss cookie banners/modal overlays: OneTrust, Cookiebot, Usercentrics, then generic heuristic. Idempotent.',
|
|
1311
1328
|
{
|
|
1312
|
-
tab_id:
|
|
1329
|
+
tab_id: tabId,
|
|
1313
1330
|
},
|
|
1314
1331
|
async ({ tab_id }) => {
|
|
1315
1332
|
const data = await send(MessageType.DISMISS_OVERLAYS, { tab_id });
|
|
@@ -1322,9 +1339,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1322
1339
|
'handle_dialogs',
|
|
1323
1340
|
'Auto-accept/dismiss future JS dialogs (alert/confirm/prompt), logging them. reset restores native dialogs and returns the log.',
|
|
1324
1341
|
{
|
|
1325
|
-
action: z.enum(['accept', 'dismiss', 'reset']).optional().default('accept'),
|
|
1342
|
+
action: z.enum(['accept', 'dismiss', 'reset']).optional().default('accept').describe('accept/dismiss auto-answer future dialogs; reset restores native behaviour'),
|
|
1326
1343
|
prompt_text: z.string().optional().describe('Returned by window.prompt on accept'),
|
|
1327
|
-
tab_id:
|
|
1344
|
+
tab_id: tabId,
|
|
1328
1345
|
},
|
|
1329
1346
|
async ({ action, prompt_text, tab_id }) => {
|
|
1330
1347
|
const data = await send(MessageType.HANDLE_DIALOGS, { action, prompt_text, tab_id });
|
|
@@ -1337,10 +1354,10 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1337
1354
|
'find_text',
|
|
1338
1355
|
'Find text on the page: parent selector, context, visibility, position per match. Attaches nearby interactive elements (with refs for click/type_text/hover) for the first visible match.',
|
|
1339
1356
|
{
|
|
1340
|
-
text: z.string(),
|
|
1341
|
-
case_sensitive: z.boolean().optional().default(false),
|
|
1342
|
-
max_results: z.number().optional().default(20),
|
|
1343
|
-
tab_id:
|
|
1357
|
+
text: z.string().describe('Literal text to find, not a regex'),
|
|
1358
|
+
case_sensitive: z.boolean().optional().default(false).describe('Match case exactly'),
|
|
1359
|
+
max_results: z.number().optional().default(20).describe('Cap on matches returned, in document order'),
|
|
1360
|
+
tab_id: tabId,
|
|
1344
1361
|
},
|
|
1345
1362
|
async ({ text, case_sensitive, max_results, tab_id }) => {
|
|
1346
1363
|
const data = await send(MessageType.FIND_TEXT, { text, case_sensitive, max_results, tab_id });
|
|
@@ -1381,16 +1398,16 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1381
1398
|
'network_rules',
|
|
1382
1399
|
'Network interception, browser-wide, survives reloads until cleared: block requests, redirect URLs, set/remove request headers, or stub responses with a synthetic body (served by a local helper; from HTTPS pages the stub host must be trustworthy).',
|
|
1383
1400
|
{
|
|
1384
|
-
action: z.enum(['block', 'redirect', 'modify_header', 'stub', 'list', 'clear']),
|
|
1401
|
+
action: z.enum(['block', 'redirect', 'modify_header', 'stub', 'list', 'clear']).describe('list and clear inspect and drop the rules already installed'),
|
|
1385
1402
|
url_filter: z.string().optional().describe('declarativeNetRequest urlFilter, e.g. "||example.com/api/*"'),
|
|
1386
|
-
redirect_url: z.string().optional(),
|
|
1387
|
-
header: z.string().optional(),
|
|
1403
|
+
redirect_url: z.string().optional().describe('Destination for action=redirect'),
|
|
1404
|
+
header: z.string().optional().describe('Header name for action=modify_header, e.g. "User-Agent"'),
|
|
1388
1405
|
header_value: z.string().optional().describe('Omit to remove header'),
|
|
1389
1406
|
header_target: z.enum(['request', 'response']).optional().default('request').describe("response = strip content-security-policy / x-frame-options, inject CORS"),
|
|
1390
1407
|
body: z.string().optional().describe('Response body (action=stub)'),
|
|
1391
1408
|
status: z.number().optional().default(200).describe('action=stub'),
|
|
1392
1409
|
content_type: z.string().optional().default('application/json').describe('action=stub'),
|
|
1393
|
-
resource_types: z.array(z.enum(['main_frame', 'sub_frame', 'stylesheet', 'script', 'image', 'font', 'object', 'xmlhttprequest', 'ping', 'csp_report', 'media', 'websocket', 'other'])).optional(),
|
|
1410
|
+
resource_types: z.array(z.enum(['main_frame', 'sub_frame', 'stylesheet', 'script', 'image', 'font', 'object', 'xmlhttprequest', 'ping', 'csp_report', 'media', 'websocket', 'other'])).optional().describe('Limit the rule to these request types; omitted = all of them'),
|
|
1394
1411
|
},
|
|
1395
1412
|
async ({ action, url_filter, redirect_url, header, header_value, header_target, body, status, content_type, resource_types }) => {
|
|
1396
1413
|
if (action === 'stub') {
|
|
@@ -1417,11 +1434,11 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1417
1434
|
'screenshot_diff',
|
|
1418
1435
|
'Visual regression: save a named baseline (viewport or element), compare later — returns changed-pixel % and red-highlighted diff image. Baselines are in-memory (lost on service worker restart).',
|
|
1419
1436
|
{
|
|
1420
|
-
action: z.enum(['baseline', 'compare', 'list', 'clear']),
|
|
1421
|
-
name: z.string().optional().default('default'),
|
|
1437
|
+
action: z.enum(['baseline', 'compare', 'list', 'clear']).describe('baseline stores, compare measures against it, clear drops baselines'),
|
|
1438
|
+
name: z.string().optional().default('default').describe('Baseline id: reuse the same one to compare across runs'),
|
|
1422
1439
|
selector: z.string().optional().describe('Capture one element (default viewport)'),
|
|
1423
1440
|
threshold: z.number().optional().default(10).describe('Per-channel tolerance 0-255'),
|
|
1424
|
-
tab_id:
|
|
1441
|
+
tab_id: tabId,
|
|
1425
1442
|
},
|
|
1426
1443
|
async ({ action, name, selector, threshold, tab_id }) => {
|
|
1427
1444
|
const data = await send(MessageType.SCREENSHOT_DIFF, { action, name, selector, threshold, tab_id });
|
|
@@ -1443,9 +1460,10 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1443
1460
|
'web_vitals',
|
|
1444
1461
|
'Core Web Vitals accumulated since the current document loaded: CLS, LCP, FCP, TTFB, long tasks and an INP '
|
|
1445
1462
|
+ 'approximation. Read-only. Needs the page instrumentation active from before load, so it reports whether it '
|
|
1446
|
-
+ 'was hooked instead of silently returning zeros — navigate() installs it.
|
|
1463
|
+
+ 'was hooked instead of silently returning zeros — navigate() installs it. '
|
|
1464
|
+
+ 'Covers user-perceived stability and responsiveness after load, not the load timings themselves.',
|
|
1447
1465
|
{
|
|
1448
|
-
tab_id:
|
|
1466
|
+
tab_id: tabId,
|
|
1449
1467
|
},
|
|
1450
1468
|
async ({ tab_id }) => {
|
|
1451
1469
|
const data = await send(MessageType.WEB_VITALS, { tab_id });
|
|
@@ -1459,8 +1477,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1459
1477
|
'List addEventListener registrations since page load: counts by type + recent entries.',
|
|
1460
1478
|
{
|
|
1461
1479
|
type: z.string().optional().describe('e.g. "click"'),
|
|
1462
|
-
limit: z.number().optional().default(100),
|
|
1463
|
-
tab_id:
|
|
1480
|
+
limit: z.number().optional().default(100).describe('Max listeners returned, from the top of the match list'),
|
|
1481
|
+
tab_id: tabId,
|
|
1464
1482
|
},
|
|
1465
1483
|
async ({ type, limit, tab_id }) => {
|
|
1466
1484
|
const data = await send(MessageType.LIST_EVENT_LISTENERS, { type, limit, tab_id });
|
|
@@ -1474,7 +1492,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1474
1492
|
'Monitor WebSocket connections/messages (500-char previews). Hook installs on first call; earlier connections are missed.',
|
|
1475
1493
|
{
|
|
1476
1494
|
clear: z.boolean().optional().default(false).describe('Clear buffer after read'),
|
|
1477
|
-
tab_id:
|
|
1495
|
+
tab_id: tabId,
|
|
1478
1496
|
},
|
|
1479
1497
|
async ({ clear, tab_id }) => {
|
|
1480
1498
|
const data = await send(MessageType.MONITOR_WEBSOCKET, { clear, tab_id });
|
|
@@ -1487,7 +1505,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1487
1505
|
'seo_audit',
|
|
1488
1506
|
'SEO audit: title/description lengths, canonical, robots, h1 count, Open Graph, Twitter card, JSON-LD validity, hreflang, lang, viewport, favicon',
|
|
1489
1507
|
{
|
|
1490
|
-
tab_id:
|
|
1508
|
+
tab_id: tabId,
|
|
1491
1509
|
},
|
|
1492
1510
|
async ({ tab_id }) => {
|
|
1493
1511
|
const data = await send(MessageType.SEO_AUDIT, { tab_id });
|
|
@@ -1498,16 +1516,19 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1498
1516
|
// --- extract_table ---
|
|
1499
1517
|
server.tool(
|
|
1500
1518
|
'extract_table',
|
|
1501
|
-
'
|
|
1519
|
+
'Read a real <table> as JSON: thead cells become keys, each row an object. Read-only. '
|
|
1520
|
+
+ 'Only for tabular markup — `where` filters rows server-side, so one matching row out of 1500 '
|
|
1521
|
+
+ 'costs a few hundred bytes instead of the whole document. `row_count` is the table total, '
|
|
1522
|
+
+ '`match_count` how many rows passed the filter.',
|
|
1502
1523
|
{
|
|
1503
|
-
selector: z.string().optional().default('table'),
|
|
1504
|
-
index: z.number().optional().default(0),
|
|
1524
|
+
selector: z.string().optional().default('table').describe('The table to read; ">>>" pierces shadow DOM'),
|
|
1525
|
+
index: z.number().optional().default(0).describe('Which table to take when the selector matches several, 0-based'),
|
|
1505
1526
|
max_rows: z.number().optional().default(100).describe('Max rows returned to you (output cap).'),
|
|
1506
|
-
where: z.record(z.string(), z.string()).optional().describe('
|
|
1527
|
+
where: z.record(z.string(), z.string()).optional().describe('{column: substring} rows must match, case-insensitive contains. Key "any" matches any cell.'),
|
|
1507
1528
|
columns: z.array(z.string()).optional().describe('Return only these columns per row.'),
|
|
1508
1529
|
offset: z.number().optional().default(0).describe('Skip N rows of the (filtered) set before applying max_rows.'),
|
|
1509
1530
|
scan_rows: z.number().optional().default(2000).describe('Max rows materialized in-page to scan/filter; raise for very large tables.'),
|
|
1510
|
-
tab_id:
|
|
1531
|
+
tab_id: tabId,
|
|
1511
1532
|
},
|
|
1512
1533
|
async ({ selector, index, max_rows, where, columns, offset, scan_rows, tab_id }) => {
|
|
1513
1534
|
const data = await send(MessageType.EXTRACT_TABLE, { selector, index, scan_rows, tab_id });
|
|
@@ -1521,8 +1542,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1521
1542
|
'unused_css',
|
|
1522
1543
|
'List CSS selectors matching nothing in the current DOM (approximate; cross-origin sheets unreadable).',
|
|
1523
1544
|
{
|
|
1524
|
-
max_selectors: z.number().optional().default(200),
|
|
1525
|
-
tab_id:
|
|
1545
|
+
max_selectors: z.number().optional().default(200).describe('Cap on unused selectors reported: a large stylesheet has thousands'),
|
|
1546
|
+
tab_id: tabId,
|
|
1526
1547
|
},
|
|
1527
1548
|
async ({ max_selectors, tab_id }) => {
|
|
1528
1549
|
const data = await send(MessageType.UNUSED_CSS, { max_selectors, tab_id });
|
|
@@ -1535,11 +1556,11 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1535
1556
|
'drag_and_drop',
|
|
1536
1557
|
'Drag an element onto another. html5 = DragEvent+DataTransfer; pointer = pointer/mouse events (sortable libraries).',
|
|
1537
1558
|
{
|
|
1538
|
-
source_selector: z.string(),
|
|
1539
|
-
target_selector: z.string(),
|
|
1540
|
-
mode: z.enum(['html5', 'pointer']).optional().default('html5'),
|
|
1541
|
-
frame_id:
|
|
1542
|
-
tab_id:
|
|
1559
|
+
source_selector: z.string().describe('Element to drag; ">>>" pierces shadow DOM'),
|
|
1560
|
+
target_selector: z.string().describe('Drop target; ">>>" pierces shadow DOM'),
|
|
1561
|
+
mode: z.enum(['html5', 'pointer']).optional().default('html5').describe('Which event family to emit, since libraries listen to different ones'),
|
|
1562
|
+
frame_id: frameId,
|
|
1563
|
+
tab_id: tabId,
|
|
1543
1564
|
},
|
|
1544
1565
|
async ({ source_selector, target_selector, mode, frame_id, tab_id }) => {
|
|
1545
1566
|
const data = await send(MessageType.DRAG_AND_DROP, { source_selector, target_selector, mode, frame_id, tab_id });
|
|
@@ -1552,9 +1573,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1552
1573
|
'clipboard',
|
|
1553
1574
|
'Read or write the system clipboard (text). Activates the tab first.',
|
|
1554
1575
|
{
|
|
1555
|
-
action: z.enum(['read', 'write']),
|
|
1576
|
+
action: z.enum(['read', 'write']).describe('write takes text; read returns the current clipboard contents'),
|
|
1556
1577
|
text: z.string().optional().describe('For write'),
|
|
1557
|
-
tab_id:
|
|
1578
|
+
tab_id: tabId,
|
|
1558
1579
|
},
|
|
1559
1580
|
async ({ action, text, tab_id }) => {
|
|
1560
1581
|
const data = await send(MessageType.CLIPBOARD, { action, text, tab_id });
|
|
@@ -1567,11 +1588,11 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1567
1588
|
'set_geolocation',
|
|
1568
1589
|
'Override navigator.geolocation with fixed coordinates (page-level patch). reset restores native.',
|
|
1569
1590
|
{
|
|
1570
|
-
latitude: z.number().optional(),
|
|
1571
|
-
longitude: z.number().optional(),
|
|
1591
|
+
latitude: z.number().optional().describe('Decimal degrees, -90 to 90'),
|
|
1592
|
+
longitude: z.number().optional().describe('Decimal degrees, -180 to 180'),
|
|
1572
1593
|
accuracy: z.number().optional().default(10).describe('Meters'),
|
|
1573
|
-
reset: z.boolean().optional().default(false),
|
|
1574
|
-
tab_id:
|
|
1594
|
+
reset: z.boolean().optional().default(false).describe('Restore the real position and stop overriding'),
|
|
1595
|
+
tab_id: tabId,
|
|
1575
1596
|
},
|
|
1576
1597
|
async ({ latitude, longitude, accuracy, reset, tab_id }) => {
|
|
1577
1598
|
const data = await send(MessageType.SET_GEOLOCATION, { latitude, longitude, accuracy, reset, tab_id });
|
|
@@ -1584,9 +1605,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1584
1605
|
'manage_downloads',
|
|
1585
1606
|
'List downloads or wait for one to complete. Files land in the browser Downloads folder, not on the server.',
|
|
1586
1607
|
{
|
|
1587
|
-
action: z.enum(['list', 'wait_for_complete']),
|
|
1608
|
+
action: z.enum(['list', 'wait_for_complete']).describe('wait_for_complete blocks until the newest download finishes or times out'),
|
|
1588
1609
|
timeout: z.number().optional().default(30000).describe('Max ms (wait_for_complete)'),
|
|
1589
|
-
limit: z.number().optional().default(10),
|
|
1610
|
+
limit: z.number().optional().default(10).describe('Max download entries returned, newest first'),
|
|
1590
1611
|
},
|
|
1591
1612
|
async ({ action, timeout, limit }) => {
|
|
1592
1613
|
const data = await send(MessageType.MANAGE_DOWNLOADS, { action, timeout, limit });
|
|
@@ -1600,7 +1621,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1600
1621
|
'Save the full page (DOM, styles, images) as an MHTML archive file on the server filesystem.',
|
|
1601
1622
|
{
|
|
1602
1623
|
output_path: z.string().describe('Absolute file path to write (e.g. /tmp/page.mhtml)'),
|
|
1603
|
-
tab_id:
|
|
1624
|
+
tab_id: tabId,
|
|
1604
1625
|
},
|
|
1605
1626
|
async ({ output_path, tab_id }) => {
|
|
1606
1627
|
const data = await send(MessageType.SAVE_PAGE, { tab_id });
|
|
@@ -1618,7 +1639,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1618
1639
|
+ 'which is how you read a PDF — Chrome renders PDFs in a viewer no content script can reach, so read_page returns nothing on a PDF tab.',
|
|
1619
1640
|
{
|
|
1620
1641
|
url: z.string().describe('Absolute URL. Cookies are sent for its origin'),
|
|
1621
|
-
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']).optional().default('GET'),
|
|
1642
|
+
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']).optional().default('GET').describe('HEAD fetches headers only, without the body'),
|
|
1622
1643
|
headers: z.record(z.string(), z.string()).optional().describe('Extra request headers'),
|
|
1623
1644
|
body: z.string().optional().describe('Request body (POST/PUT/PATCH)'),
|
|
1624
1645
|
save_to: z.string().optional().describe('Absolute path: write the response bytes here instead of returning them (PDF, images, archives)'),
|
|
@@ -1673,8 +1694,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1673
1694
|
'Get or set tab zoom (0.25–5). No factor = read current; reset restores default.',
|
|
1674
1695
|
{
|
|
1675
1696
|
factor: z.number().optional().describe('1 = 100%'),
|
|
1676
|
-
reset: z.boolean().optional().default(false),
|
|
1677
|
-
tab_id:
|
|
1697
|
+
reset: z.boolean().optional().default(false).describe('Restore the default zoom for this origin'),
|
|
1698
|
+
tab_id: tabId,
|
|
1678
1699
|
},
|
|
1679
1700
|
async ({ factor, reset, tab_id }) => {
|
|
1680
1701
|
const data = await send(MessageType.SET_ZOOM, { factor, reset, tab_id });
|
|
@@ -1687,9 +1708,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1687
1708
|
'http_auth',
|
|
1688
1709
|
'Set/clear credentials for HTTP Basic/Digest auth dialogs (browser-wide, in-memory only).',
|
|
1689
1710
|
{
|
|
1690
|
-
action: z.enum(['set', 'clear']),
|
|
1691
|
-
username: z.string().optional(),
|
|
1692
|
-
password: z.string().optional(),
|
|
1711
|
+
action: z.enum(['set', 'clear']).describe('set installs credentials for HTTP auth prompts; clear removes them'),
|
|
1712
|
+
username: z.string().optional().describe('Required for action=set'),
|
|
1713
|
+
password: z.string().optional().describe('Required for action=set; kept in memory, never written to disk'),
|
|
1693
1714
|
},
|
|
1694
1715
|
async ({ action, username, password }) => {
|
|
1695
1716
|
if (action === 'set' && !username) throw new Error('username is required for action=set');
|
|
@@ -1703,7 +1724,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1703
1724
|
'security_headers',
|
|
1704
1725
|
'Audit security headers (CSP, HSTS, XCTO, clickjacking, Referrer/Permissions-Policy, version leaks). Captured from real navigations — reload if unavailable.',
|
|
1705
1726
|
{
|
|
1706
|
-
tab_id:
|
|
1727
|
+
tab_id: tabId,
|
|
1707
1728
|
},
|
|
1708
1729
|
async ({ tab_id }) => {
|
|
1709
1730
|
const data = await send(MessageType.GET_RESPONSE_HEADERS, { tab_id });
|
|
@@ -1721,9 +1742,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1721
1742
|
'session_fixture',
|
|
1722
1743
|
'Save/restore localStorage+sessionStorage+cookies as a named fixture (e.g. a logged-in state). Restore requires the tab to be on the origin recorded at save.',
|
|
1723
1744
|
{
|
|
1724
|
-
action: z.enum(['save', 'restore', 'list']),
|
|
1745
|
+
action: z.enum(['save', 'restore', 'list']).describe('save snapshots the current origin; restore writes it back'),
|
|
1725
1746
|
name: z.string().optional().describe('Required for save/restore'),
|
|
1726
|
-
tab_id:
|
|
1747
|
+
tab_id: tabId,
|
|
1727
1748
|
},
|
|
1728
1749
|
async ({ action, name, tab_id }) => {
|
|
1729
1750
|
if (action === 'list') {
|
|
@@ -1793,11 +1814,12 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1793
1814
|
'List actionable elements (buttons, links, inputs, [role], [onclick]) with ready-to-use CSS selector, label, position, flags. Prefer this over dumping HTML to discover selectors.',
|
|
1794
1815
|
{
|
|
1795
1816
|
scope: z.string().optional().describe('Limit search (CSS selector)'),
|
|
1796
|
-
limit: z.number().optional().default(100),
|
|
1797
|
-
visible_only: z.boolean().optional().default(true)
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1817
|
+
limit: z.number().optional().default(100).describe('Max elements returned; raise it on a dense page'),
|
|
1818
|
+
visible_only: z.boolean().optional().default(true)
|
|
1819
|
+
.describe('false also lists elements hidden or scrolled out of view'),
|
|
1820
|
+
format: z.enum(['lines', 'json']).optional().default('lines').describe('lines is compact; json adds full attributes per element'),
|
|
1821
|
+
frame_id: frameId,
|
|
1822
|
+
tab_id: tabId,
|
|
1801
1823
|
},
|
|
1802
1824
|
async ({ scope, limit, visible_only, format, frame_id, tab_id }) => {
|
|
1803
1825
|
const data = await send(MessageType.GET_INTERACTIVES, { scope, limit, visible_only, frame_id, tab_id });
|
|
@@ -1823,16 +1845,18 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1823
1845
|
// --- extract ---
|
|
1824
1846
|
server.tool(
|
|
1825
1847
|
'extract',
|
|
1826
|
-
'
|
|
1848
|
+
'Read repeated non-tabular structures — product cards, list items, search results — as one record per item: '
|
|
1849
|
+
+ 'item_selector matches the repeating block, fields map output names to selectors relative to it. '
|
|
1850
|
+
+ 'Read-only, deterministic, parsed server-side from the main-document HTML.',
|
|
1827
1851
|
{
|
|
1828
1852
|
item_selector: z.string().describe('CSS selector matching each item'),
|
|
1829
1853
|
fields: z.record(z.string(), z.object({
|
|
1830
1854
|
selector: z.string().optional().describe('Relative to the item (default: the item itself)'),
|
|
1831
1855
|
attr: z.string().optional().describe('Attribute to read (default: text content)'),
|
|
1832
1856
|
})).describe('field name → {selector, attr}'),
|
|
1833
|
-
max_items: z.number().optional().default(50),
|
|
1834
|
-
format: z.enum(['lines', 'json']).optional().default('lines'),
|
|
1835
|
-
tab_id:
|
|
1857
|
+
max_items: z.number().optional().default(50).describe('Cap on records returned, in document order'),
|
|
1858
|
+
format: z.enum(['lines', 'json']).optional().default('lines').describe('lines is compact; json keeps one object per record'),
|
|
1859
|
+
tab_id: tabId,
|
|
1836
1860
|
max_length: z.number().optional().default(20000).describe('Max output chars'),
|
|
1837
1861
|
},
|
|
1838
1862
|
async ({ item_selector, fields, max_items, format, tab_id, max_length }) => {
|
|
@@ -1866,14 +1890,14 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1866
1890
|
'assert',
|
|
1867
1891
|
'Assert a page condition, polling until timeout: element exists/visible (optionally with count or containing text), text on page, tab url/title. Patterns: substring, or "/…/" for regex. Recorded flows replay it as a test.',
|
|
1868
1892
|
{
|
|
1869
|
-
selector: z.string().optional(),
|
|
1870
|
-
state: z.enum(['attached', 'visible']).optional().default('attached'),
|
|
1893
|
+
selector: z.string().optional().describe('Element the assertion is about; ">>>" pierces shadow DOM'),
|
|
1894
|
+
state: z.enum(['attached', 'visible']).optional().default('attached').describe('attached means present in the DOM, visible also requires a rendered box'),
|
|
1871
1895
|
text: z.string().optional().describe('In the element (with selector) or anywhere on the page'),
|
|
1872
1896
|
count: z.number().optional().describe('Exact match count for selector'),
|
|
1873
1897
|
url: z.string().optional().describe('Tab url pattern'),
|
|
1874
1898
|
title: z.string().optional().describe('Tab title pattern'),
|
|
1875
|
-
timeout: z.number().optional().default(5000),
|
|
1876
|
-
tab_id:
|
|
1899
|
+
timeout: z.number().optional().default(5000).describe('Max ms to wait for the condition before failing'),
|
|
1900
|
+
tab_id: tabId,
|
|
1877
1901
|
},
|
|
1878
1902
|
async ({ selector, state, text, count, url, title, timeout, tab_id }) => {
|
|
1879
1903
|
const params = { selector, state, text, count, url, title, timeout, tab_id: tab_id ?? sessionTabId ?? undefined };
|
|
@@ -1896,7 +1920,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1896
1920
|
'session_record',
|
|
1897
1921
|
'Record the commands of this session as a replayable jsonl file (replay with the CLI: chrome-bridge replay --file <path>). tab_id is stripped — replays target the tab they navigate.',
|
|
1898
1922
|
{
|
|
1899
|
-
action: z.enum(['start', 'stop', 'status', 'list']),
|
|
1923
|
+
action: z.enum(['start', 'stop', 'status', 'list']).describe('start begins recording, stop writes the file and returns its path'),
|
|
1900
1924
|
name: z.string().optional().describe('Required for start'),
|
|
1901
1925
|
},
|
|
1902
1926
|
async ({ action, name }) => {
|