chrome-bridge-mcp 1.9.0 → 1.10.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/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 +222 -179
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.6k tokens of `tools/list`, all 60 cost ≈14.6k; 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.1",
|
|
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.1",
|
|
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);
|
|
@@ -572,15 +586,19 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
572
586
|
// --- type_text ---
|
|
573
587
|
server.tool(
|
|
574
588
|
'type_text',
|
|
575
|
-
'
|
|
589
|
+
'Put text into an input, textarea or contenteditable, by CSS selector or by a ref from get_interactives. '
|
|
590
|
+
+ 'Replaces the whole value rather than appending, and assigns through the native setter so React and '
|
|
591
|
+
+ 'Vue controlled inputs register the change, then fires input and change. mode=keys instead emits '
|
|
592
|
+
+ 'keydown/input/keyup per character, which is what autocomplete and masked fields need — slower, so '
|
|
593
|
+
+ 'reach for it only when mode=set leaves the field empty or the dropdown never opens.',
|
|
576
594
|
{
|
|
577
|
-
selector: z.string().optional(),
|
|
595
|
+
selector: z.string().optional().describe('CSS selector; ">>>" pierces shadow DOM. Ignored when ref is given'),
|
|
578
596
|
ref: z.string().optional().describe('From get_interactives, e.g. "n3"'),
|
|
579
|
-
text: z.string(),
|
|
597
|
+
text: z.string().describe('Value to type; empty string clears the field'),
|
|
580
598
|
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:
|
|
599
|
+
wait_after: waitAfter,
|
|
600
|
+
tab_id: tabId,
|
|
601
|
+
frame_id: frameId,
|
|
584
602
|
},
|
|
585
603
|
async ({ selector, ref, text, mode, wait_after, tab_id, frame_id }) => {
|
|
586
604
|
const target = resolveTarget(selector, ref, tab_id);
|
|
@@ -603,9 +621,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
603
621
|
+ 'HTML on a big table costs tens of thousands of tokens for data you then filter anyway — prefer '
|
|
604
622
|
+ 'extract_table or extract for tabular and repeated content, and get_interactives to find click targets.',
|
|
605
623
|
{
|
|
606
|
-
mode: z.enum(['text', 'html', 'accessibility']).default('text'),
|
|
607
|
-
tab_id:
|
|
608
|
-
frame_id:
|
|
624
|
+
mode: z.enum(['text', 'html', 'accessibility']).default('text').describe('text strips markup, html keeps it (costly), accessibility returns the a11y tree'),
|
|
625
|
+
tab_id: tabId,
|
|
626
|
+
frame_id: frameId,
|
|
609
627
|
max_length: z.number().optional().default(50000).describe('Max output chars'),
|
|
610
628
|
},
|
|
611
629
|
async ({ mode, tab_id, frame_id, max_length }) => {
|
|
@@ -624,8 +642,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
624
642
|
'get_page_info',
|
|
625
643
|
'Get page metadata: meta tags, scripts, stylesheets, links, and forms',
|
|
626
644
|
{
|
|
627
|
-
tab_id:
|
|
628
|
-
frame_id:
|
|
645
|
+
tab_id: tabId,
|
|
646
|
+
frame_id: frameId,
|
|
629
647
|
},
|
|
630
648
|
async ({ tab_id, frame_id }) => {
|
|
631
649
|
const data = await send(MessageType.GET_PAGE_INFO, { tab_id, frame_id });
|
|
@@ -645,8 +663,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
645
663
|
+ 'Cookies come with domain, path, expiry and the httpOnly/secure flags, so this also answers whether a '
|
|
646
664
|
+ 'session cookie is present — write them back with set_storage, or snapshot a logged-in state with session_fixture.',
|
|
647
665
|
{
|
|
648
|
-
type: z.enum(['all', 'localStorage', 'sessionStorage', 'cookies']).default('all'),
|
|
649
|
-
tab_id:
|
|
666
|
+
type: z.enum(['all', 'localStorage', 'sessionStorage', 'cookies']).default('all').describe('all returns the three together'),
|
|
667
|
+
tab_id: tabId,
|
|
650
668
|
},
|
|
651
669
|
async ({ type, tab_id }) => {
|
|
652
670
|
const data = await send(MessageType.GET_STORAGE, { type, tab_id });
|
|
@@ -664,9 +682,10 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
664
682
|
'get_performance',
|
|
665
683
|
'Navigation timing, paint metrics, JS heap size and per-resource load times, as measured since the current '
|
|
666
684
|
+ 'document loaded. Read-only, no reload triggered — numbers are only meaningful once loading has settled, '
|
|
667
|
-
+ 'so on a page still fetching
|
|
685
|
+
+ 'so on a page still fetching wait for network idle first. Covers how fast the document arrived, '
|
|
686
|
+
+ 'not the layout-shift and interaction metrics that accumulate afterwards.',
|
|
668
687
|
{
|
|
669
|
-
tab_id:
|
|
688
|
+
tab_id: tabId,
|
|
670
689
|
},
|
|
671
690
|
async ({ tab_id }) => {
|
|
672
691
|
const data = await send(MessageType.GET_PERFORMANCE, { tab_id });
|
|
@@ -684,11 +703,11 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
684
703
|
'query_dom',
|
|
685
704
|
'Query DOM elements by CSS selector, returning structure, attributes, bounding rect, and computed styles.',
|
|
686
705
|
{
|
|
687
|
-
selector: z.string(),
|
|
706
|
+
selector: z.string().describe('CSS selector; ">>>" pierces shadow DOM. Matches all, not just the first'),
|
|
688
707
|
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:
|
|
708
|
+
limit: z.number().optional().default(50).describe('Max elements returned, from the top of the match list'),
|
|
709
|
+
tab_id: tabId,
|
|
710
|
+
frame_id: frameId,
|
|
692
711
|
},
|
|
693
712
|
async ({ selector, properties, limit, tab_id, frame_id }) => {
|
|
694
713
|
const data = await send(MessageType.QUERY_DOM, { selector, properties, limit, tab_id, frame_id });
|
|
@@ -708,13 +727,13 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
708
727
|
+ 'setTextContent. Nothing is persisted — the next reload restores the page as the server sends it. '
|
|
709
728
|
+ 'For styling many elements at once inject_css is one call instead of N.',
|
|
710
729
|
{
|
|
711
|
-
selector: z.string(),
|
|
712
|
-
action: z.enum(['setAttribute', 'removeAttribute', 'addClass', 'removeClass', 'setStyle', 'setTextContent']),
|
|
730
|
+
selector: z.string().describe('CSS selector; ">>>" pierces shadow DOM. Only the first match is changed'),
|
|
731
|
+
action: z.enum(['setAttribute', 'removeAttribute', 'addClass', 'removeClass', 'setStyle', 'setTextContent']).describe('setStyle takes a CSS declaration in value; addClass/removeClass take className'),
|
|
713
732
|
name: z.string().optional().describe('Attribute name'),
|
|
714
|
-
value: z.string().optional(),
|
|
715
|
-
className: z.string().optional(),
|
|
716
|
-
tab_id:
|
|
717
|
-
frame_id:
|
|
733
|
+
value: z.string().optional().describe('Attribute value, style declaration, or text, per action'),
|
|
734
|
+
className: z.string().optional().describe('Class to add or remove (addClass/removeClass)'),
|
|
735
|
+
tab_id: tabId,
|
|
736
|
+
frame_id: frameId,
|
|
718
737
|
},
|
|
719
738
|
async ({ selector, action, name, value, className, tab_id, frame_id }) => {
|
|
720
739
|
const data = await send(MessageType.MODIFY_DOM, { selector, action, name, value, className, tab_id, frame_id });
|
|
@@ -733,8 +752,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
733
752
|
'Inject a CSS rule into the page. Stays until the next navigation or reload, and re-injecting the same id '
|
|
734
753
|
+ 'replaces it rather than stacking. Affects only what is rendered — the stylesheet of the site is untouched.',
|
|
735
754
|
{
|
|
736
|
-
css: z.string(),
|
|
737
|
-
tab_id:
|
|
755
|
+
css: z.string().describe('One or more CSS rules, as they would appear in a stylesheet'),
|
|
756
|
+
tab_id: tabId,
|
|
738
757
|
},
|
|
739
758
|
async ({ css, tab_id }) => {
|
|
740
759
|
const data = await send(MessageType.INJECT_CSS, { css, tab_id });
|
|
@@ -753,10 +772,10 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
753
772
|
'Read console messages captured since page load, incl. uncaught errors and unhandled rejections.',
|
|
754
773
|
{
|
|
755
774
|
clear: z.boolean().optional().default(false).describe('Clear buffer after read'),
|
|
756
|
-
level: z.enum(['all', 'log', 'warn', 'error', 'info', 'debug']).optional().default('all'),
|
|
775
|
+
level: z.enum(['all', 'log', 'warn', 'error', 'info', 'debug']).optional().default('all').describe('all merges every level in one chronological list'),
|
|
757
776
|
limit: z.number().optional().default(50).describe('Most recent; buffer 1000'),
|
|
758
|
-
format: z.enum(['lines', 'json']).optional().default('lines'),
|
|
759
|
-
tab_id:
|
|
777
|
+
format: z.enum(['lines', 'json']).optional().default('lines').describe('lines is compact; json keeps timestamps and stack traces'),
|
|
778
|
+
tab_id: tabId,
|
|
760
779
|
},
|
|
761
780
|
async ({ clear, level, limit, format, tab_id }) => {
|
|
762
781
|
// limit va all'estensione: taglia in pagina e cancella (con clear) solo
|
|
@@ -788,10 +807,10 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
788
807
|
'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
808
|
{
|
|
790
809
|
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'),
|
|
810
|
+
source: z.enum(['page', 'browser']).optional().default('page').describe('page sees XHR/fetch only; browser also sees static assets'),
|
|
811
|
+
format: z.enum(['lines', 'json', 'har']).optional().default('lines').describe('har exports HAR 1.2 for external tooling'),
|
|
793
812
|
limit: z.number().optional().default(100).describe('Most recent; buffer 1000'),
|
|
794
|
-
tab_id:
|
|
813
|
+
tab_id: tabId,
|
|
795
814
|
},
|
|
796
815
|
async ({ clear, source, format, limit, tab_id }) => {
|
|
797
816
|
// limit va all'estensione (taglia in pagina, clear solo del restituito);
|
|
@@ -827,7 +846,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
827
846
|
+ 'Each call creates another tab: reuse a tab_id or navigate() to move an existing one instead of piling up tabs.',
|
|
828
847
|
{
|
|
829
848
|
url: z.string().optional().describe('URL to open (default: new tab page)'),
|
|
830
|
-
active: z.boolean().optional().default(true),
|
|
849
|
+
active: z.boolean().optional().default(true).describe('false opens the tab in the background, leaving the current one focused'),
|
|
831
850
|
},
|
|
832
851
|
async ({ url, active }) => {
|
|
833
852
|
const data = await send(MessageType.CREATE_TAB, { url, active });
|
|
@@ -844,9 +863,13 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
844
863
|
// --- wait_for ---
|
|
845
864
|
server.tool(
|
|
846
865
|
'wait_for',
|
|
847
|
-
'
|
|
866
|
+
'Block until a condition holds: element (selector in the DOM), function (JS expression turns truthy; needs '
|
|
867
|
+
+ 'the "Allow user scripts" toggle), navigation (mode=spa for client-side route changes), network_idle. '
|
|
868
|
+
+ 'Polls until timeout — 10s for element and function, 15s for navigation and network_idle — then '
|
|
869
|
+
+ '**returns `found: false` with a reason instead of raising**, so a caller that ignores the result '
|
|
870
|
+
+ 'silently proceeds as if the wait had succeeded. Read-only: waiting changes nothing on the page.',
|
|
848
871
|
{
|
|
849
|
-
condition: z.enum(['element', 'function', 'navigation', 'network_idle']),
|
|
872
|
+
condition: z.enum(['element', 'function', 'navigation', 'network_idle']).describe('function needs expression; element needs selector'),
|
|
850
873
|
selector: z.string().optional().describe('condition=element'),
|
|
851
874
|
expression: z.string().optional().describe('JS expression (condition=function)'),
|
|
852
875
|
visible: z.boolean().optional().default(false).describe('Element must also be visible'),
|
|
@@ -854,8 +877,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
854
877
|
idle_ms: z.number().optional().default(500).describe('Quiet period ms (network_idle)'),
|
|
855
878
|
timeout: z.number().optional().describe('Max ms (default 10000; 15000 navigation/network_idle)'),
|
|
856
879
|
interval: z.number().optional().describe('Poll ms, min 50'),
|
|
857
|
-
tab_id:
|
|
858
|
-
frame_id:
|
|
880
|
+
tab_id: tabId,
|
|
881
|
+
frame_id: frameId,
|
|
859
882
|
},
|
|
860
883
|
async ({ condition, selector, expression, visible, mode, idle_ms, timeout, interval, tab_id, frame_id }) => {
|
|
861
884
|
let data;
|
|
@@ -885,18 +908,18 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
885
908
|
'scroll',
|
|
886
909
|
'Scroll. action=to: once to element/coordinates; action=until: repeatedly until element visible, network idle, or no new content (infinite scroll).',
|
|
887
910
|
{
|
|
888
|
-
action: z.enum(['to', 'until']).optional().default('to'),
|
|
911
|
+
action: z.enum(['to', 'until']).optional().default('to').describe('to jumps to a position or element; until scrolls repeatedly to load more'),
|
|
889
912
|
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'),
|
|
913
|
+
x: z.number().optional().describe('Absolute horizontal position in px (action=to, without selector)'),
|
|
914
|
+
y: z.number().optional().describe('Absolute vertical position in px (action=to, without selector)'),
|
|
915
|
+
behavior: z.enum(['smooth', 'instant', 'auto']).optional().default('auto').describe('instant avoids waiting for smooth-scroll animations'),
|
|
893
916
|
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),
|
|
917
|
+
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'),
|
|
918
|
+
max_scrolls: z.number().optional().default(20).describe('Cap on scroll steps, so an infinite feed terminates'),
|
|
896
919
|
step_px: z.number().optional().describe('px per step, default viewport height'),
|
|
897
920
|
settle_ms: z.number().optional().default(400).describe('Pause ms after each step'),
|
|
898
|
-
tab_id:
|
|
899
|
-
frame_id:
|
|
921
|
+
tab_id: tabId,
|
|
922
|
+
frame_id: frameId,
|
|
900
923
|
},
|
|
901
924
|
async ({ action, selector, x, y, behavior, offset_y, until, max_scrolls, step_px, settle_ms, tab_id, frame_id }) => {
|
|
902
925
|
const data = (action ?? 'to') === 'until'
|
|
@@ -913,17 +936,17 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
913
936
|
+ 'action=clear wipes every entry of that type for the origin and cannot be undone — on a site the user is '
|
|
914
937
|
+ 'logged into, clearing cookies logs them out. Read the current values first with get_storage.',
|
|
915
938
|
{
|
|
916
|
-
type: z.enum(['localStorage', 'sessionStorage', 'cookie']),
|
|
917
|
-
action: z.enum(['set', 'delete', 'clear']),
|
|
939
|
+
type: z.enum(['localStorage', 'sessionStorage', 'cookie']).describe('cookie writes a real cookie, not a storage key'),
|
|
940
|
+
action: z.enum(['set', 'delete', 'clear']).describe('clear ignores key and wipes every entry of that type'),
|
|
918
941
|
key: z.string().optional().describe('Required for set/delete'),
|
|
919
|
-
value: z.string().optional(),
|
|
942
|
+
value: z.string().optional().describe('Required for action=set'),
|
|
920
943
|
path: z.string().optional().describe('Cookie path (default /)'),
|
|
921
944
|
domain: z.string().optional().describe('Cookie domain'),
|
|
922
945
|
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:
|
|
946
|
+
secure: z.boolean().optional().describe('Cookie sent over HTTPS only'),
|
|
947
|
+
sameSite: z.enum(['Strict', 'Lax', 'None']).optional().describe('None requires secure=true'),
|
|
948
|
+
http_only: z.boolean().optional().describe('Cookie hidden from page JS'),
|
|
949
|
+
tab_id: tabId,
|
|
927
950
|
},
|
|
928
951
|
async ({ type, action, key, value, path, domain, expires, secure, sameSite, http_only, tab_id }) => {
|
|
929
952
|
const data = await send(MessageType.SET_STORAGE, { type, action, key, value, path, domain, expires, secure, sameSite, http_only, tab_id });
|
|
@@ -946,9 +969,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
946
969
|
value: z.string(),
|
|
947
970
|
})).describe('{selector, value} pairs'),
|
|
948
971
|
submit_selector: z.string().optional().describe('Submit button to click after filling'),
|
|
949
|
-
wait_after:
|
|
950
|
-
tab_id:
|
|
951
|
-
frame_id:
|
|
972
|
+
wait_after: waitAfter,
|
|
973
|
+
tab_id: tabId,
|
|
974
|
+
frame_id: frameId,
|
|
952
975
|
},
|
|
953
976
|
async ({ fields, submit_selector, wait_after, tab_id, frame_id }) => {
|
|
954
977
|
const before = await tabSnapshot(tab_id);
|
|
@@ -968,12 +991,16 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
968
991
|
// --- viewport_resize ---
|
|
969
992
|
server.tool(
|
|
970
993
|
'viewport_resize',
|
|
971
|
-
'Resize Chrome window to preset (mobile
|
|
994
|
+
'Resize the Chrome **window** to a preset (mobile 375x812, tablet 768x1024, desktop 1440x900) or to explicit '
|
|
995
|
+
+ 'dimensions. The rendered viewport ends up smaller than what you ask for, by the height of the browser '
|
|
996
|
+
+ 'chrome — measure it with execute_js if the exact number matters. width and height each override the '
|
|
997
|
+
+ 'corresponding half of the preset, so preset plus width gives a custom width at the preset height. '
|
|
998
|
+
+ 'A maximized window on ChromeOS ignores the request.',
|
|
972
999
|
{
|
|
973
1000
|
preset: z.enum(['mobile', 'tablet', 'desktop']).optional().describe('375x812, 768x1024, 1440x900'),
|
|
974
1001
|
width: z.number().optional().describe('Overrides preset'),
|
|
975
1002
|
height: z.number().optional().describe('Overrides preset'),
|
|
976
|
-
tab_id:
|
|
1003
|
+
tab_id: tabId,
|
|
977
1004
|
},
|
|
978
1005
|
async ({ preset, width, height, tab_id }) => {
|
|
979
1006
|
const data = await send(MessageType.VIEWPORT_RESIZE, { preset, width, height, tab_id });
|
|
@@ -989,10 +1016,12 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
989
1016
|
// --- element_screenshot ---
|
|
990
1017
|
server.tool(
|
|
991
1018
|
'element_screenshot',
|
|
992
|
-
'Screenshot
|
|
1019
|
+
'Screenshot cropped to one element (PNG), scrolled into view first. Read-only. '
|
|
1020
|
+
+ 'The cheapest image in the set, because it carries only the box you asked for: '
|
|
1021
|
+
+ 'a component, a chart, a table cell.',
|
|
993
1022
|
{
|
|
994
|
-
selector: z.string(),
|
|
995
|
-
tab_id:
|
|
1023
|
+
selector: z.string().describe('CSS selector; ">>>" pierces shadow DOM. The element is scrolled into view first'),
|
|
1024
|
+
tab_id: tabId,
|
|
996
1025
|
},
|
|
997
1026
|
async ({ selector, tab_id }) => {
|
|
998
1027
|
const data = await send(MessageType.ELEMENT_SCREENSHOT, { selector, tab_id });
|
|
@@ -1011,12 +1040,12 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1011
1040
|
+ 'max_segments are returned and the rest need segment_offset. On a long page, reading the text you actually need '
|
|
1012
1041
|
+ 'is cheaper by an order of magnitude — prefer read_page, extract or find_text unless the layout itself is the question.',
|
|
1013
1042
|
{
|
|
1014
|
-
max_scrolls: z.number().optional().default(20),
|
|
1043
|
+
max_scrolls: z.number().optional().default(20).describe('Cap on scroll steps: a taller page is captured only up to here'),
|
|
1015
1044
|
delay: z.number().optional().default(500).describe('ms between captures (min 500, Chrome quota)'),
|
|
1016
1045
|
stitch: z.boolean().optional().default(true).describe('false = one image per viewport'),
|
|
1017
1046
|
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
1047
|
segment_offset: z.number().optional().default(0).describe('Skip the first N segments'),
|
|
1019
|
-
tab_id:
|
|
1048
|
+
tab_id: tabId,
|
|
1020
1049
|
},
|
|
1021
1050
|
async ({ max_scrolls, delay, stitch, max_segments, segment_offset, tab_id }) => {
|
|
1022
1051
|
const data = await send(MessageType.FULL_PAGE_SCREENSHOT, { max_scrolls, delay, stitch, tab_id });
|
|
@@ -1055,14 +1084,17 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1055
1084
|
// --- highlight_elements ---
|
|
1056
1085
|
server.tool(
|
|
1057
1086
|
'highlight_elements',
|
|
1058
|
-
'
|
|
1087
|
+
'Outline every element matching a selector with a coloured overlay, to see on a screenshot what a selector '
|
|
1088
|
+
+ 'actually caught. Each call clears the overlays left by the previous one instead of stacking them, and '
|
|
1089
|
+
+ 'remove=true clears without adding. The overlays are injected DOM nodes: a reload or a navigation drops '
|
|
1090
|
+
+ 'them, and they sit above the page without altering its layout or its own styles.',
|
|
1059
1091
|
{
|
|
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'),
|
|
1092
|
+
selector: z.string().optional().describe('CSS selector; ">>>" pierces shadow DOM. Every match is outlined'),
|
|
1093
|
+
color: z.string().optional().default('rgba(255,0,0,0.3)').describe('Any CSS color for the overlay label'),
|
|
1094
|
+
border: z.string().optional().default('2px solid red').describe('CSS border shorthand, e.g. "2px solid red"'),
|
|
1063
1095
|
label: z.boolean().optional().default(false).describe('Show tag.class (WxH) label'),
|
|
1064
|
-
remove: z.boolean().optional().default(false),
|
|
1065
|
-
tab_id:
|
|
1096
|
+
remove: z.boolean().optional().default(false).describe('Remove previously injected highlights instead of adding'),
|
|
1097
|
+
tab_id: tabId,
|
|
1066
1098
|
},
|
|
1067
1099
|
async ({ selector, color, border, label, remove, tab_id }) => {
|
|
1068
1100
|
const data = await send(MessageType.HIGHLIGHT_ELEMENTS, { selector, color, border, label, remove, tab_id });
|
|
@@ -1081,8 +1113,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1081
1113
|
'A11y audit: missing alt, empty links, heading hierarchy, ARIA, contrast (approximate), form labels.',
|
|
1082
1114
|
{
|
|
1083
1115
|
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:
|
|
1116
|
+
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'),
|
|
1117
|
+
tab_id: tabId,
|
|
1086
1118
|
},
|
|
1087
1119
|
async ({ scope, checks, tab_id }) => {
|
|
1088
1120
|
const data = await send(MessageType.ACCESSIBILITY_AUDIT, { scope, checks, tab_id });
|
|
@@ -1100,12 +1132,12 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1100
1132
|
'check_links',
|
|
1101
1133
|
'Check page links for broken URLs, verified server-side (no CORS limits, real HTTP status).',
|
|
1102
1134
|
{
|
|
1103
|
-
scope: z.enum(['same-origin', 'all', 'external']).optional().default('all'),
|
|
1104
|
-
selector: z.string().optional().default('a[href]'),
|
|
1135
|
+
scope: z.enum(['same-origin', 'all', 'external']).optional().default('all').describe('same-origin skips third-party links, which are the slow ones'),
|
|
1136
|
+
selector: z.string().optional().default('a[href]').describe('CSS selector; ">>>" pierces shadow DOM. Restricts which links are collected'),
|
|
1105
1137
|
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:
|
|
1138
|
+
max_links: z.number().optional().default(50).describe('Cap on links fetched: each one is a real HTTP request'),
|
|
1139
|
+
format: z.enum(['lines', 'json']).optional().default('lines').describe('lines is compact; json keeps per-link status and timing'),
|
|
1140
|
+
tab_id: tabId,
|
|
1109
1141
|
},
|
|
1110
1142
|
async ({ scope, selector, timeout, max_links, format, tab_id }) => {
|
|
1111
1143
|
const data = await send(MessageType.COLLECT_LINKS, { scope, selector, max_links, tab_id });
|
|
@@ -1136,9 +1168,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1136
1168
|
+ 'Read-only. Values come from the current layout, so zoom and viewport size change them: set_zoom(1) and a '
|
|
1137
1169
|
+ 'fixed viewport_resize make results comparable across runs.',
|
|
1138
1170
|
{
|
|
1139
|
-
selector1: z.string(),
|
|
1140
|
-
selector2: z.string(),
|
|
1141
|
-
tab_id:
|
|
1171
|
+
selector1: z.string().describe('First element; distances are measured from its box'),
|
|
1172
|
+
selector2: z.string().describe('Second element; ">>>" pierces shadow DOM'),
|
|
1173
|
+
tab_id: tabId,
|
|
1142
1174
|
},
|
|
1143
1175
|
async ({ selector1, selector2, tab_id }) => {
|
|
1144
1176
|
const data = await send(MessageType.MEASURE_SPACING, { selector1, selector2, tab_id });
|
|
@@ -1156,15 +1188,15 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1156
1188
|
'watch_dom',
|
|
1157
1189
|
'Watch DOM mutations (MutationObserver). First call installs the watcher; later calls read accumulated mutations.',
|
|
1158
1190
|
{
|
|
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),
|
|
1191
|
+
selector: z.string().optional().default('body').describe('Subtree to observe; ">>>" pierces shadow DOM'),
|
|
1192
|
+
attributes: z.boolean().optional().default(true).describe('Report attribute changes'),
|
|
1193
|
+
childList: z.boolean().optional().default(true).describe('Report added and removed children'),
|
|
1194
|
+
characterData: z.boolean().optional().default(false).describe('Report text content changes'),
|
|
1195
|
+
subtree: z.boolean().optional().default(true).describe('Observe descendants too, not just the matched node'),
|
|
1164
1196
|
clear: z.boolean().optional().default(false).describe('Clear buffer after read'),
|
|
1165
1197
|
stop: z.boolean().optional().default(false).describe('Disconnect observer'),
|
|
1166
1198
|
limit: z.number().optional().default(100).describe('Most recent mutations; buffer 1000'),
|
|
1167
|
-
tab_id:
|
|
1199
|
+
tab_id: tabId,
|
|
1168
1200
|
},
|
|
1169
1201
|
async ({ selector, attributes, childList, characterData, subtree, clear, stop, limit, tab_id }) => {
|
|
1170
1202
|
const data = await send(MessageType.WATCH_DOM, { selector, attributes, childList, characterData, subtree, clear, stop, limit, tab_id });
|
|
@@ -1185,12 +1217,12 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1185
1217
|
+ 'user_agent only changes what page JS reads — the request header is a separate thing, set it with '
|
|
1186
1218
|
+ 'network_rules(action=modify_header, header="User-Agent"). Pair with viewport_resize to emulate a device.',
|
|
1187
1219
|
{
|
|
1188
|
-
colorScheme: z.enum(['dark', 'light', 'no-preference']).optional(),
|
|
1189
|
-
reducedMotion: z.enum(['reduce', 'no-preference']).optional(),
|
|
1190
|
-
printMode: z.boolean().optional().default(false),
|
|
1220
|
+
colorScheme: z.enum(['dark', 'light', 'no-preference']).optional().describe('Value reported to prefers-color-scheme queries'),
|
|
1221
|
+
reducedMotion: z.enum(['reduce', 'no-preference']).optional().describe('Value reported to prefers-reduced-motion queries'),
|
|
1222
|
+
printMode: z.boolean().optional().default(false).describe('Make print media queries match, without opening a print dialog'),
|
|
1191
1223
|
user_agent: z.string().optional().describe('Overrides navigator.userAgent and appVersion in the page (not the HTTP header)'),
|
|
1192
1224
|
reset: z.boolean().optional().default(false).describe('Remove all emulations'),
|
|
1193
|
-
tab_id:
|
|
1225
|
+
tab_id: tabId,
|
|
1194
1226
|
},
|
|
1195
1227
|
async ({ colorScheme, reducedMotion, printMode, user_agent, reset, tab_id }) => {
|
|
1196
1228
|
const data = await send(MessageType.EMULATE_MEDIA, { colorScheme, reducedMotion, printMode, user_agent, reset, tab_id });
|
|
@@ -1208,10 +1240,10 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1208
1240
|
'hover',
|
|
1209
1241
|
'Hover over an element (mouseenter/mouseover), by CSS selector or ref.',
|
|
1210
1242
|
{
|
|
1211
|
-
selector: z.string().optional(),
|
|
1243
|
+
selector: z.string().optional().describe('CSS selector; ">>>" pierces shadow DOM. Triggers CSS and JS hover handlers'),
|
|
1212
1244
|
ref: z.string().optional().describe('From get_interactives'),
|
|
1213
|
-
tab_id:
|
|
1214
|
-
frame_id:
|
|
1245
|
+
tab_id: tabId,
|
|
1246
|
+
frame_id: frameId,
|
|
1215
1247
|
},
|
|
1216
1248
|
async ({ selector, ref, tab_id, frame_id }) => {
|
|
1217
1249
|
const data = await send(MessageType.HOVER, { selector: resolveTarget(selector, ref, tab_id), tab_id, frame_id });
|
|
@@ -1233,12 +1265,12 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1233
1265
|
{
|
|
1234
1266
|
key: z.string().describe('e.g. "Enter", "Escape", "Tab", "ArrowDown"'),
|
|
1235
1267
|
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:
|
|
1268
|
+
ctrl: z.boolean().optional().default(false).describe('Hold Control'),
|
|
1269
|
+
shift: z.boolean().optional().default(false).describe('Hold Shift'),
|
|
1270
|
+
alt: z.boolean().optional().default(false).describe('Hold Alt'),
|
|
1271
|
+
meta: z.boolean().optional().default(false).describe('Hold Meta (Command/Windows)'),
|
|
1272
|
+
tab_id: tabId,
|
|
1273
|
+
frame_id: frameId,
|
|
1242
1274
|
},
|
|
1243
1275
|
async ({ key, selector, ctrl, shift, alt, meta, tab_id, frame_id }) => {
|
|
1244
1276
|
const data = await send(MessageType.PRESS_KEY, { key, selector, ctrl, shift, alt, meta, tab_id, frame_id });
|
|
@@ -1256,7 +1288,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1256
1288
|
'get_frames',
|
|
1257
1289
|
'List frames (main + iframes) with frameId, parent, URL — for the frame_id parameter of DOM tools.',
|
|
1258
1290
|
{
|
|
1259
|
-
tab_id:
|
|
1291
|
+
tab_id: tabId,
|
|
1260
1292
|
},
|
|
1261
1293
|
async ({ tab_id }) => {
|
|
1262
1294
|
const data = await send(MessageType.GET_FRAMES, { tab_id });
|
|
@@ -1271,9 +1303,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1271
1303
|
+ 'tab and anything unsaved in it, and cannot be undone — it may be a tab the user is working in. '
|
|
1272
1304
|
+ 'reload and navigation drop injected CSS, emulations and page hooks.',
|
|
1273
1305
|
{
|
|
1274
|
-
action: z.enum(['close', 'activate', 'reload', 'back', 'forward']),
|
|
1306
|
+
action: z.enum(['close', 'activate', 'reload', 'back', 'forward']).describe('close cannot be undone; reload drops injected CSS and page hooks'),
|
|
1275
1307
|
bypass_cache: z.boolean().optional().default(false).describe('reload only'),
|
|
1276
|
-
tab_id:
|
|
1308
|
+
tab_id: tabId,
|
|
1277
1309
|
},
|
|
1278
1310
|
async ({ action, bypass_cache, tab_id }) => {
|
|
1279
1311
|
const data = await send(MessageType.TAB_ACTION, { action, bypass_cache, tab_id });
|
|
@@ -1288,10 +1320,10 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1288
1320
|
'upload_file',
|
|
1289
1321
|
'Set a file on input[type=file] from the server filesystem via DataTransfer (max 10MB).',
|
|
1290
1322
|
{
|
|
1291
|
-
selector: z.string(),
|
|
1323
|
+
selector: z.string().describe('The file input to fill; ">>>" pierces shadow DOM'),
|
|
1292
1324
|
path: z.string().describe('Absolute path on the server machine'),
|
|
1293
1325
|
mime_type: z.string().optional().describe('Default: inferred from extension'),
|
|
1294
|
-
tab_id:
|
|
1326
|
+
tab_id: tabId,
|
|
1295
1327
|
},
|
|
1296
1328
|
async ({ selector, path, mime_type, tab_id }) => {
|
|
1297
1329
|
const buf = await readFile(path);
|
|
@@ -1309,7 +1341,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1309
1341
|
'dismiss_overlays',
|
|
1310
1342
|
'Dismiss cookie banners/modal overlays: OneTrust, Cookiebot, Usercentrics, then generic heuristic. Idempotent.',
|
|
1311
1343
|
{
|
|
1312
|
-
tab_id:
|
|
1344
|
+
tab_id: tabId,
|
|
1313
1345
|
},
|
|
1314
1346
|
async ({ tab_id }) => {
|
|
1315
1347
|
const data = await send(MessageType.DISMISS_OVERLAYS, { tab_id });
|
|
@@ -1322,9 +1354,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1322
1354
|
'handle_dialogs',
|
|
1323
1355
|
'Auto-accept/dismiss future JS dialogs (alert/confirm/prompt), logging them. reset restores native dialogs and returns the log.',
|
|
1324
1356
|
{
|
|
1325
|
-
action: z.enum(['accept', 'dismiss', 'reset']).optional().default('accept'),
|
|
1357
|
+
action: z.enum(['accept', 'dismiss', 'reset']).optional().default('accept').describe('accept/dismiss auto-answer future dialogs; reset restores native behaviour'),
|
|
1326
1358
|
prompt_text: z.string().optional().describe('Returned by window.prompt on accept'),
|
|
1327
|
-
tab_id:
|
|
1359
|
+
tab_id: tabId,
|
|
1328
1360
|
},
|
|
1329
1361
|
async ({ action, prompt_text, tab_id }) => {
|
|
1330
1362
|
const data = await send(MessageType.HANDLE_DIALOGS, { action, prompt_text, tab_id });
|
|
@@ -1337,10 +1369,10 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1337
1369
|
'find_text',
|
|
1338
1370
|
'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
1371
|
{
|
|
1340
|
-
text: z.string(),
|
|
1341
|
-
case_sensitive: z.boolean().optional().default(false),
|
|
1342
|
-
max_results: z.number().optional().default(20),
|
|
1343
|
-
tab_id:
|
|
1372
|
+
text: z.string().describe('Literal text to find, not a regex'),
|
|
1373
|
+
case_sensitive: z.boolean().optional().default(false).describe('Match case exactly'),
|
|
1374
|
+
max_results: z.number().optional().default(20).describe('Cap on matches returned, in document order'),
|
|
1375
|
+
tab_id: tabId,
|
|
1344
1376
|
},
|
|
1345
1377
|
async ({ text, case_sensitive, max_results, tab_id }) => {
|
|
1346
1378
|
const data = await send(MessageType.FIND_TEXT, { text, case_sensitive, max_results, tab_id });
|
|
@@ -1381,16 +1413,16 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1381
1413
|
'network_rules',
|
|
1382
1414
|
'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
1415
|
{
|
|
1384
|
-
action: z.enum(['block', 'redirect', 'modify_header', 'stub', 'list', 'clear']),
|
|
1416
|
+
action: z.enum(['block', 'redirect', 'modify_header', 'stub', 'list', 'clear']).describe('list and clear inspect and drop the rules already installed'),
|
|
1385
1417
|
url_filter: z.string().optional().describe('declarativeNetRequest urlFilter, e.g. "||example.com/api/*"'),
|
|
1386
|
-
redirect_url: z.string().optional(),
|
|
1387
|
-
header: z.string().optional(),
|
|
1418
|
+
redirect_url: z.string().optional().describe('Destination for action=redirect'),
|
|
1419
|
+
header: z.string().optional().describe('Header name for action=modify_header, e.g. "User-Agent"'),
|
|
1388
1420
|
header_value: z.string().optional().describe('Omit to remove header'),
|
|
1389
1421
|
header_target: z.enum(['request', 'response']).optional().default('request').describe("response = strip content-security-policy / x-frame-options, inject CORS"),
|
|
1390
1422
|
body: z.string().optional().describe('Response body (action=stub)'),
|
|
1391
1423
|
status: z.number().optional().default(200).describe('action=stub'),
|
|
1392
1424
|
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(),
|
|
1425
|
+
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
1426
|
},
|
|
1395
1427
|
async ({ action, url_filter, redirect_url, header, header_value, header_target, body, status, content_type, resource_types }) => {
|
|
1396
1428
|
if (action === 'stub') {
|
|
@@ -1417,11 +1449,11 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1417
1449
|
'screenshot_diff',
|
|
1418
1450
|
'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
1451
|
{
|
|
1420
|
-
action: z.enum(['baseline', 'compare', 'list', 'clear']),
|
|
1421
|
-
name: z.string().optional().default('default'),
|
|
1452
|
+
action: z.enum(['baseline', 'compare', 'list', 'clear']).describe('baseline stores, compare measures against it, clear drops baselines'),
|
|
1453
|
+
name: z.string().optional().default('default').describe('Baseline id: reuse the same one to compare across runs'),
|
|
1422
1454
|
selector: z.string().optional().describe('Capture one element (default viewport)'),
|
|
1423
1455
|
threshold: z.number().optional().default(10).describe('Per-channel tolerance 0-255'),
|
|
1424
|
-
tab_id:
|
|
1456
|
+
tab_id: tabId,
|
|
1425
1457
|
},
|
|
1426
1458
|
async ({ action, name, selector, threshold, tab_id }) => {
|
|
1427
1459
|
const data = await send(MessageType.SCREENSHOT_DIFF, { action, name, selector, threshold, tab_id });
|
|
@@ -1443,9 +1475,10 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1443
1475
|
'web_vitals',
|
|
1444
1476
|
'Core Web Vitals accumulated since the current document loaded: CLS, LCP, FCP, TTFB, long tasks and an INP '
|
|
1445
1477
|
+ '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.
|
|
1478
|
+
+ 'was hooked instead of silently returning zeros — navigate() installs it. '
|
|
1479
|
+
+ 'Covers user-perceived stability and responsiveness after load, not the load timings themselves.',
|
|
1447
1480
|
{
|
|
1448
|
-
tab_id:
|
|
1481
|
+
tab_id: tabId,
|
|
1449
1482
|
},
|
|
1450
1483
|
async ({ tab_id }) => {
|
|
1451
1484
|
const data = await send(MessageType.WEB_VITALS, { tab_id });
|
|
@@ -1459,8 +1492,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1459
1492
|
'List addEventListener registrations since page load: counts by type + recent entries.',
|
|
1460
1493
|
{
|
|
1461
1494
|
type: z.string().optional().describe('e.g. "click"'),
|
|
1462
|
-
limit: z.number().optional().default(100),
|
|
1463
|
-
tab_id:
|
|
1495
|
+
limit: z.number().optional().default(100).describe('Max listeners returned, from the top of the match list'),
|
|
1496
|
+
tab_id: tabId,
|
|
1464
1497
|
},
|
|
1465
1498
|
async ({ type, limit, tab_id }) => {
|
|
1466
1499
|
const data = await send(MessageType.LIST_EVENT_LISTENERS, { type, limit, tab_id });
|
|
@@ -1474,7 +1507,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1474
1507
|
'Monitor WebSocket connections/messages (500-char previews). Hook installs on first call; earlier connections are missed.',
|
|
1475
1508
|
{
|
|
1476
1509
|
clear: z.boolean().optional().default(false).describe('Clear buffer after read'),
|
|
1477
|
-
tab_id:
|
|
1510
|
+
tab_id: tabId,
|
|
1478
1511
|
},
|
|
1479
1512
|
async ({ clear, tab_id }) => {
|
|
1480
1513
|
const data = await send(MessageType.MONITOR_WEBSOCKET, { clear, tab_id });
|
|
@@ -1487,7 +1520,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1487
1520
|
'seo_audit',
|
|
1488
1521
|
'SEO audit: title/description lengths, canonical, robots, h1 count, Open Graph, Twitter card, JSON-LD validity, hreflang, lang, viewport, favicon',
|
|
1489
1522
|
{
|
|
1490
|
-
tab_id:
|
|
1523
|
+
tab_id: tabId,
|
|
1491
1524
|
},
|
|
1492
1525
|
async ({ tab_id }) => {
|
|
1493
1526
|
const data = await send(MessageType.SEO_AUDIT, { tab_id });
|
|
@@ -1498,16 +1531,19 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1498
1531
|
// --- extract_table ---
|
|
1499
1532
|
server.tool(
|
|
1500
1533
|
'extract_table',
|
|
1501
|
-
'
|
|
1534
|
+
'Read a real <table> as JSON: thead cells become keys, each row an object. Read-only. '
|
|
1535
|
+
+ 'Only for tabular markup — `where` filters rows server-side, so one matching row out of 1500 '
|
|
1536
|
+
+ 'costs a few hundred bytes instead of the whole document. `row_count` is the table total, '
|
|
1537
|
+
+ '`match_count` how many rows passed the filter.',
|
|
1502
1538
|
{
|
|
1503
|
-
selector: z.string().optional().default('table'),
|
|
1504
|
-
index: z.number().optional().default(0),
|
|
1539
|
+
selector: z.string().optional().default('table').describe('The table to read; ">>>" pierces shadow DOM'),
|
|
1540
|
+
index: z.number().optional().default(0).describe('Which table to take when the selector matches several, 0-based'),
|
|
1505
1541
|
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('
|
|
1542
|
+
where: z.record(z.string(), z.string()).optional().describe('{column: substring} rows must match, case-insensitive contains. Key "any" matches any cell.'),
|
|
1507
1543
|
columns: z.array(z.string()).optional().describe('Return only these columns per row.'),
|
|
1508
1544
|
offset: z.number().optional().default(0).describe('Skip N rows of the (filtered) set before applying max_rows.'),
|
|
1509
1545
|
scan_rows: z.number().optional().default(2000).describe('Max rows materialized in-page to scan/filter; raise for very large tables.'),
|
|
1510
|
-
tab_id:
|
|
1546
|
+
tab_id: tabId,
|
|
1511
1547
|
},
|
|
1512
1548
|
async ({ selector, index, max_rows, where, columns, offset, scan_rows, tab_id }) => {
|
|
1513
1549
|
const data = await send(MessageType.EXTRACT_TABLE, { selector, index, scan_rows, tab_id });
|
|
@@ -1521,8 +1557,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1521
1557
|
'unused_css',
|
|
1522
1558
|
'List CSS selectors matching nothing in the current DOM (approximate; cross-origin sheets unreadable).',
|
|
1523
1559
|
{
|
|
1524
|
-
max_selectors: z.number().optional().default(200),
|
|
1525
|
-
tab_id:
|
|
1560
|
+
max_selectors: z.number().optional().default(200).describe('Cap on unused selectors reported: a large stylesheet has thousands'),
|
|
1561
|
+
tab_id: tabId,
|
|
1526
1562
|
},
|
|
1527
1563
|
async ({ max_selectors, tab_id }) => {
|
|
1528
1564
|
const data = await send(MessageType.UNUSED_CSS, { max_selectors, tab_id });
|
|
@@ -1535,11 +1571,11 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1535
1571
|
'drag_and_drop',
|
|
1536
1572
|
'Drag an element onto another. html5 = DragEvent+DataTransfer; pointer = pointer/mouse events (sortable libraries).',
|
|
1537
1573
|
{
|
|
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:
|
|
1574
|
+
source_selector: z.string().describe('Element to drag; ">>>" pierces shadow DOM'),
|
|
1575
|
+
target_selector: z.string().describe('Drop target; ">>>" pierces shadow DOM'),
|
|
1576
|
+
mode: z.enum(['html5', 'pointer']).optional().default('html5').describe('Which event family to emit, since libraries listen to different ones'),
|
|
1577
|
+
frame_id: frameId,
|
|
1578
|
+
tab_id: tabId,
|
|
1543
1579
|
},
|
|
1544
1580
|
async ({ source_selector, target_selector, mode, frame_id, tab_id }) => {
|
|
1545
1581
|
const data = await send(MessageType.DRAG_AND_DROP, { source_selector, target_selector, mode, frame_id, tab_id });
|
|
@@ -1552,9 +1588,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1552
1588
|
'clipboard',
|
|
1553
1589
|
'Read or write the system clipboard (text). Activates the tab first.',
|
|
1554
1590
|
{
|
|
1555
|
-
action: z.enum(['read', 'write']),
|
|
1591
|
+
action: z.enum(['read', 'write']).describe('write takes text; read returns the current clipboard contents'),
|
|
1556
1592
|
text: z.string().optional().describe('For write'),
|
|
1557
|
-
tab_id:
|
|
1593
|
+
tab_id: tabId,
|
|
1558
1594
|
},
|
|
1559
1595
|
async ({ action, text, tab_id }) => {
|
|
1560
1596
|
const data = await send(MessageType.CLIPBOARD, { action, text, tab_id });
|
|
@@ -1567,11 +1603,11 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1567
1603
|
'set_geolocation',
|
|
1568
1604
|
'Override navigator.geolocation with fixed coordinates (page-level patch). reset restores native.',
|
|
1569
1605
|
{
|
|
1570
|
-
latitude: z.number().optional(),
|
|
1571
|
-
longitude: z.number().optional(),
|
|
1606
|
+
latitude: z.number().optional().describe('Decimal degrees, -90 to 90'),
|
|
1607
|
+
longitude: z.number().optional().describe('Decimal degrees, -180 to 180'),
|
|
1572
1608
|
accuracy: z.number().optional().default(10).describe('Meters'),
|
|
1573
|
-
reset: z.boolean().optional().default(false),
|
|
1574
|
-
tab_id:
|
|
1609
|
+
reset: z.boolean().optional().default(false).describe('Restore the real position and stop overriding'),
|
|
1610
|
+
tab_id: tabId,
|
|
1575
1611
|
},
|
|
1576
1612
|
async ({ latitude, longitude, accuracy, reset, tab_id }) => {
|
|
1577
1613
|
const data = await send(MessageType.SET_GEOLOCATION, { latitude, longitude, accuracy, reset, tab_id });
|
|
@@ -1584,9 +1620,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1584
1620
|
'manage_downloads',
|
|
1585
1621
|
'List downloads or wait for one to complete. Files land in the browser Downloads folder, not on the server.',
|
|
1586
1622
|
{
|
|
1587
|
-
action: z.enum(['list', 'wait_for_complete']),
|
|
1623
|
+
action: z.enum(['list', 'wait_for_complete']).describe('wait_for_complete blocks until the newest download finishes or times out'),
|
|
1588
1624
|
timeout: z.number().optional().default(30000).describe('Max ms (wait_for_complete)'),
|
|
1589
|
-
limit: z.number().optional().default(10),
|
|
1625
|
+
limit: z.number().optional().default(10).describe('Max download entries returned, newest first'),
|
|
1590
1626
|
},
|
|
1591
1627
|
async ({ action, timeout, limit }) => {
|
|
1592
1628
|
const data = await send(MessageType.MANAGE_DOWNLOADS, { action, timeout, limit });
|
|
@@ -1600,7 +1636,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1600
1636
|
'Save the full page (DOM, styles, images) as an MHTML archive file on the server filesystem.',
|
|
1601
1637
|
{
|
|
1602
1638
|
output_path: z.string().describe('Absolute file path to write (e.g. /tmp/page.mhtml)'),
|
|
1603
|
-
tab_id:
|
|
1639
|
+
tab_id: tabId,
|
|
1604
1640
|
},
|
|
1605
1641
|
async ({ output_path, tab_id }) => {
|
|
1606
1642
|
const data = await send(MessageType.SAVE_PAGE, { tab_id });
|
|
@@ -1618,7 +1654,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1618
1654
|
+ '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
1655
|
{
|
|
1620
1656
|
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'),
|
|
1657
|
+
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']).optional().default('GET').describe('HEAD fetches headers only, without the body'),
|
|
1622
1658
|
headers: z.record(z.string(), z.string()).optional().describe('Extra request headers'),
|
|
1623
1659
|
body: z.string().optional().describe('Request body (POST/PUT/PATCH)'),
|
|
1624
1660
|
save_to: z.string().optional().describe('Absolute path: write the response bytes here instead of returning them (PDF, images, archives)'),
|
|
@@ -1673,8 +1709,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1673
1709
|
'Get or set tab zoom (0.25–5). No factor = read current; reset restores default.',
|
|
1674
1710
|
{
|
|
1675
1711
|
factor: z.number().optional().describe('1 = 100%'),
|
|
1676
|
-
reset: z.boolean().optional().default(false),
|
|
1677
|
-
tab_id:
|
|
1712
|
+
reset: z.boolean().optional().default(false).describe('Restore the default zoom for this origin'),
|
|
1713
|
+
tab_id: tabId,
|
|
1678
1714
|
},
|
|
1679
1715
|
async ({ factor, reset, tab_id }) => {
|
|
1680
1716
|
const data = await send(MessageType.SET_ZOOM, { factor, reset, tab_id });
|
|
@@ -1687,9 +1723,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1687
1723
|
'http_auth',
|
|
1688
1724
|
'Set/clear credentials for HTTP Basic/Digest auth dialogs (browser-wide, in-memory only).',
|
|
1689
1725
|
{
|
|
1690
|
-
action: z.enum(['set', 'clear']),
|
|
1691
|
-
username: z.string().optional(),
|
|
1692
|
-
password: z.string().optional(),
|
|
1726
|
+
action: z.enum(['set', 'clear']).describe('set installs credentials for HTTP auth prompts; clear removes them'),
|
|
1727
|
+
username: z.string().optional().describe('Required for action=set'),
|
|
1728
|
+
password: z.string().optional().describe('Required for action=set; kept in memory, never written to disk'),
|
|
1693
1729
|
},
|
|
1694
1730
|
async ({ action, username, password }) => {
|
|
1695
1731
|
if (action === 'set' && !username) throw new Error('username is required for action=set');
|
|
@@ -1703,7 +1739,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1703
1739
|
'security_headers',
|
|
1704
1740
|
'Audit security headers (CSP, HSTS, XCTO, clickjacking, Referrer/Permissions-Policy, version leaks). Captured from real navigations — reload if unavailable.',
|
|
1705
1741
|
{
|
|
1706
|
-
tab_id:
|
|
1742
|
+
tab_id: tabId,
|
|
1707
1743
|
},
|
|
1708
1744
|
async ({ tab_id }) => {
|
|
1709
1745
|
const data = await send(MessageType.GET_RESPONSE_HEADERS, { tab_id });
|
|
@@ -1719,11 +1755,15 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1719
1755
|
// --- session_fixture ---
|
|
1720
1756
|
server.tool(
|
|
1721
1757
|
'session_fixture',
|
|
1722
|
-
'
|
|
1758
|
+
'Snapshot localStorage, sessionStorage and cookies of the current origin into a named fixture on the server, '
|
|
1759
|
+
+ 'restore one, or list what has been saved. A logged-in state is the usual reason. save overwrites a '
|
|
1760
|
+
+ 'fixture of the same name without asking; restore writes entries on top of what is already there '
|
|
1761
|
+
+ 'rather than clearing first, and refuses outright when the tab sits on a different origin than the '
|
|
1762
|
+
+ 'one recorded — cookies would otherwise attach to the wrong site. name is required except for list.',
|
|
1723
1763
|
{
|
|
1724
|
-
action: z.enum(['save', 'restore', 'list']),
|
|
1764
|
+
action: z.enum(['save', 'restore', 'list']).describe('save snapshots the current origin; restore writes it back'),
|
|
1725
1765
|
name: z.string().optional().describe('Required for save/restore'),
|
|
1726
|
-
tab_id:
|
|
1766
|
+
tab_id: tabId,
|
|
1727
1767
|
},
|
|
1728
1768
|
async ({ action, name, tab_id }) => {
|
|
1729
1769
|
if (action === 'list') {
|
|
@@ -1793,11 +1833,12 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1793
1833
|
'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
1834
|
{
|
|
1795
1835
|
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
|
-
|
|
1836
|
+
limit: z.number().optional().default(100).describe('Max elements returned; raise it on a dense page'),
|
|
1837
|
+
visible_only: z.boolean().optional().default(true)
|
|
1838
|
+
.describe('false also lists elements hidden or scrolled out of view'),
|
|
1839
|
+
format: z.enum(['lines', 'json']).optional().default('lines').describe('lines is compact; json adds full attributes per element'),
|
|
1840
|
+
frame_id: frameId,
|
|
1841
|
+
tab_id: tabId,
|
|
1801
1842
|
},
|
|
1802
1843
|
async ({ scope, limit, visible_only, format, frame_id, tab_id }) => {
|
|
1803
1844
|
const data = await send(MessageType.GET_INTERACTIVES, { scope, limit, visible_only, frame_id, tab_id });
|
|
@@ -1823,16 +1864,18 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1823
1864
|
// --- extract ---
|
|
1824
1865
|
server.tool(
|
|
1825
1866
|
'extract',
|
|
1826
|
-
'
|
|
1867
|
+
'Read repeated non-tabular structures — product cards, list items, search results — as one record per item: '
|
|
1868
|
+
+ 'item_selector matches the repeating block, fields map output names to selectors relative to it. '
|
|
1869
|
+
+ 'Read-only, deterministic, parsed server-side from the main-document HTML.',
|
|
1827
1870
|
{
|
|
1828
1871
|
item_selector: z.string().describe('CSS selector matching each item'),
|
|
1829
1872
|
fields: z.record(z.string(), z.object({
|
|
1830
1873
|
selector: z.string().optional().describe('Relative to the item (default: the item itself)'),
|
|
1831
1874
|
attr: z.string().optional().describe('Attribute to read (default: text content)'),
|
|
1832
1875
|
})).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:
|
|
1876
|
+
max_items: z.number().optional().default(50).describe('Cap on records returned, in document order'),
|
|
1877
|
+
format: z.enum(['lines', 'json']).optional().default('lines').describe('lines is compact; json keeps one object per record'),
|
|
1878
|
+
tab_id: tabId,
|
|
1836
1879
|
max_length: z.number().optional().default(20000).describe('Max output chars'),
|
|
1837
1880
|
},
|
|
1838
1881
|
async ({ item_selector, fields, max_items, format, tab_id, max_length }) => {
|
|
@@ -1866,14 +1909,14 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1866
1909
|
'assert',
|
|
1867
1910
|
'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
1911
|
{
|
|
1869
|
-
selector: z.string().optional(),
|
|
1870
|
-
state: z.enum(['attached', 'visible']).optional().default('attached'),
|
|
1912
|
+
selector: z.string().optional().describe('Element the assertion is about; ">>>" pierces shadow DOM'),
|
|
1913
|
+
state: z.enum(['attached', 'visible']).optional().default('attached').describe('attached means present in the DOM, visible also requires a rendered box'),
|
|
1871
1914
|
text: z.string().optional().describe('In the element (with selector) or anywhere on the page'),
|
|
1872
1915
|
count: z.number().optional().describe('Exact match count for selector'),
|
|
1873
1916
|
url: z.string().optional().describe('Tab url pattern'),
|
|
1874
1917
|
title: z.string().optional().describe('Tab title pattern'),
|
|
1875
|
-
timeout: z.number().optional().default(5000),
|
|
1876
|
-
tab_id:
|
|
1918
|
+
timeout: z.number().optional().default(5000).describe('Max ms to wait for the condition before failing'),
|
|
1919
|
+
tab_id: tabId,
|
|
1877
1920
|
},
|
|
1878
1921
|
async ({ selector, state, text, count, url, title, timeout, tab_id }) => {
|
|
1879
1922
|
const params = { selector, state, text, count, url, title, timeout, tab_id: tab_id ?? sessionTabId ?? undefined };
|
|
@@ -1896,7 +1939,7 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1896
1939
|
'session_record',
|
|
1897
1940
|
'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
1941
|
{
|
|
1899
|
-
action: z.enum(['start', 'stop', 'status', 'list']),
|
|
1942
|
+
action: z.enum(['start', 'stop', 'status', 'list']).describe('start begins recording, stop writes the file and returns its path'),
|
|
1900
1943
|
name: z.string().optional().describe('Required for start'),
|
|
1901
1944
|
},
|
|
1902
1945
|
async ({ action, name }) => {
|