@wdio/mcp 2.2.1 → 2.3.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 +15 -1
- package/lib/server.js +503 -269
- package/lib/server.js.map +1 -1
- package/lib/snapshot.d.ts +40 -56
- package/lib/snapshot.js +355 -161
- package/lib/snapshot.js.map +1 -1
- package/package.json +10 -6
package/lib/server.js
CHANGED
|
@@ -14,10 +14,11 @@ var startBrowserToolDefinition = {
|
|
|
14
14
|
description: "starts a browser session (Chrome, Firefox, Edge, Safari) and sets it to the current state",
|
|
15
15
|
inputSchema: {
|
|
16
16
|
browser: browserSchema.describe("Browser to launch: chrome, firefox, edge, safari (default: chrome)"),
|
|
17
|
-
headless: z.boolean().optional(),
|
|
17
|
+
headless: z.boolean().optional().default(true),
|
|
18
18
|
windowWidth: z.number().min(400).max(3840).optional().default(1920),
|
|
19
19
|
windowHeight: z.number().min(400).max(2160).optional().default(1080),
|
|
20
|
-
navigationUrl: z.string().optional().describe("URL to navigate to after starting the browser")
|
|
20
|
+
navigationUrl: z.string().optional().describe("URL to navigate to after starting the browser"),
|
|
21
|
+
capabilities: z.record(z.string(), z.unknown()).optional().describe("Additional W3C capabilities to merge with defaults (e.g. goog:chromeOptions args/extensions/prefs)")
|
|
21
22
|
}
|
|
22
23
|
};
|
|
23
24
|
var closeSessionToolDefinition = {
|
|
@@ -42,10 +43,11 @@ var getBrowser = () => {
|
|
|
42
43
|
getBrowser.__state = state;
|
|
43
44
|
var startBrowserTool = async ({
|
|
44
45
|
browser = "chrome",
|
|
45
|
-
headless =
|
|
46
|
+
headless = true,
|
|
46
47
|
windowWidth = 1920,
|
|
47
48
|
windowHeight = 1080,
|
|
48
|
-
navigationUrl
|
|
49
|
+
navigationUrl,
|
|
50
|
+
capabilities: userCapabilities = {}
|
|
49
51
|
}) => {
|
|
50
52
|
const browserDisplayNames = {
|
|
51
53
|
chrome: "Chrome",
|
|
@@ -98,8 +100,35 @@ var startBrowserTool = async ({
|
|
|
98
100
|
capabilities.browserName = "safari";
|
|
99
101
|
break;
|
|
100
102
|
}
|
|
103
|
+
const mergeCapabilityOptions = (defaultOptions, customOptions) => {
|
|
104
|
+
if (!defaultOptions || typeof defaultOptions !== "object" || !customOptions || typeof customOptions !== "object") {
|
|
105
|
+
return customOptions ?? defaultOptions;
|
|
106
|
+
}
|
|
107
|
+
const defaultRecord = defaultOptions;
|
|
108
|
+
const customRecord = customOptions;
|
|
109
|
+
const merged = { ...defaultRecord, ...customRecord };
|
|
110
|
+
if (Array.isArray(defaultRecord.args) || Array.isArray(customRecord.args)) {
|
|
111
|
+
merged.args = [
|
|
112
|
+
...Array.isArray(defaultRecord.args) ? defaultRecord.args : [],
|
|
113
|
+
...Array.isArray(customRecord.args) ? customRecord.args : []
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
return merged;
|
|
117
|
+
};
|
|
118
|
+
const mergedCapabilities = {
|
|
119
|
+
...capabilities,
|
|
120
|
+
...userCapabilities,
|
|
121
|
+
"goog:chromeOptions": mergeCapabilityOptions(capabilities["goog:chromeOptions"], userCapabilities["goog:chromeOptions"]),
|
|
122
|
+
"ms:edgeOptions": mergeCapabilityOptions(capabilities["ms:edgeOptions"], userCapabilities["ms:edgeOptions"]),
|
|
123
|
+
"moz:firefoxOptions": mergeCapabilityOptions(capabilities["moz:firefoxOptions"], userCapabilities["moz:firefoxOptions"])
|
|
124
|
+
};
|
|
125
|
+
for (const [key, value] of Object.entries(mergedCapabilities)) {
|
|
126
|
+
if (value === void 0) {
|
|
127
|
+
delete mergedCapabilities[key];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
101
130
|
const wdioBrowser = await remote({
|
|
102
|
-
capabilities
|
|
131
|
+
capabilities: mergedCapabilities
|
|
103
132
|
});
|
|
104
133
|
const { sessionId } = wdioBrowser;
|
|
105
134
|
state.browsers.set(sessionId, wdioBrowser);
|
|
@@ -346,7 +375,8 @@ var startAppToolDefinition = {
|
|
|
346
375
|
udid: z5.string().optional().describe('Unique Device Identifier for iOS real device testing (e.g., "00008030-001234567890002E")'),
|
|
347
376
|
noReset: z5.boolean().optional().describe("Do not reset app state before session (preserves app data). Default: false"),
|
|
348
377
|
fullReset: z5.boolean().optional().describe("Uninstall app before/after session. Default: true. Set to false with noReset=true to preserve app state completely"),
|
|
349
|
-
newCommandTimeout: z5.number().min(0).optional().describe("How long (in seconds) Appium will wait for a new command before assuming the client has quit and ending the session. Default: 60. Set to 300 for 5 minutes, etc.")
|
|
378
|
+
newCommandTimeout: z5.number().min(0).optional().describe("How long (in seconds) Appium will wait for a new command before assuming the client has quit and ending the session. Default: 60. Set to 300 for 5 minutes, etc."),
|
|
379
|
+
capabilities: z5.record(z5.string(), z5.unknown()).optional().describe("Additional Appium/WebDriver capabilities to merge with defaults (e.g. appium:udid, appium:chromedriverExecutable, appium:autoWebview)")
|
|
350
380
|
}
|
|
351
381
|
};
|
|
352
382
|
var getState = () => {
|
|
@@ -374,7 +404,8 @@ var startAppTool = async (args) => {
|
|
|
374
404
|
udid,
|
|
375
405
|
noReset,
|
|
376
406
|
fullReset,
|
|
377
|
-
newCommandTimeout
|
|
407
|
+
newCommandTimeout,
|
|
408
|
+
capabilities: userCapabilities = {}
|
|
378
409
|
} = args;
|
|
379
410
|
if (!appPath && noReset !== true) {
|
|
380
411
|
return {
|
|
@@ -412,12 +443,21 @@ var startAppTool = async (args) => {
|
|
|
412
443
|
fullReset,
|
|
413
444
|
newCommandTimeout
|
|
414
445
|
});
|
|
446
|
+
const mergedCapabilities = {
|
|
447
|
+
...capabilities,
|
|
448
|
+
...userCapabilities
|
|
449
|
+
};
|
|
450
|
+
for (const [key, value] of Object.entries(mergedCapabilities)) {
|
|
451
|
+
if (value === void 0) {
|
|
452
|
+
delete mergedCapabilities[key];
|
|
453
|
+
}
|
|
454
|
+
}
|
|
415
455
|
const browser = await remote2({
|
|
416
456
|
protocol: "http",
|
|
417
457
|
hostname: serverConfig.hostname,
|
|
418
458
|
port: serverConfig.port,
|
|
419
459
|
path: serverConfig.path,
|
|
420
|
-
capabilities
|
|
460
|
+
capabilities: mergedCapabilities
|
|
421
461
|
});
|
|
422
462
|
const { sessionId } = browser;
|
|
423
463
|
const shouldAutoDetach = noReset === true || !appPath;
|
|
@@ -426,7 +466,7 @@ var startAppTool = async (args) => {
|
|
|
426
466
|
state2.currentSession = sessionId;
|
|
427
467
|
state2.sessionMetadata.set(sessionId, {
|
|
428
468
|
type: platform.toLowerCase(),
|
|
429
|
-
capabilities,
|
|
469
|
+
capabilities: mergedCapabilities,
|
|
430
470
|
isAttached: shouldAutoDetach
|
|
431
471
|
});
|
|
432
472
|
const appInfo = appPath ? `
|
|
@@ -483,160 +523,157 @@ var scrollTool = async ({ direction, pixels = 500 }) => {
|
|
|
483
523
|
};
|
|
484
524
|
|
|
485
525
|
// src/scripts/get-interactable-browser-elements.ts
|
|
486
|
-
var elementsScript = (
|
|
526
|
+
var elementsScript = (includeBounds) => (function() {
|
|
487
527
|
const interactableSelectors = [
|
|
488
528
|
"a[href]",
|
|
489
|
-
// Links with href
|
|
490
529
|
"button",
|
|
491
|
-
// Buttons
|
|
492
530
|
'input:not([type="hidden"])',
|
|
493
|
-
// Input fields (except hidden)
|
|
494
531
|
"select",
|
|
495
|
-
// Select dropdowns
|
|
496
532
|
"textarea",
|
|
497
|
-
// Text areas
|
|
498
533
|
'[role="button"]',
|
|
499
|
-
// Elements with button role
|
|
500
534
|
'[role="link"]',
|
|
501
|
-
// Elements with link role
|
|
502
535
|
'[role="checkbox"]',
|
|
503
|
-
// Elements with checkbox role
|
|
504
536
|
'[role="radio"]',
|
|
505
|
-
// Elements with radio role
|
|
506
537
|
'[role="tab"]',
|
|
507
|
-
// Elements with tab role
|
|
508
538
|
'[role="menuitem"]',
|
|
509
|
-
// Elements with menuitem role
|
|
510
539
|
'[role="combobox"]',
|
|
511
|
-
// Elements with combobox role
|
|
512
540
|
'[role="option"]',
|
|
513
|
-
// Elements with option role
|
|
514
541
|
'[role="switch"]',
|
|
515
|
-
// Elements with switch role
|
|
516
542
|
'[role="slider"]',
|
|
517
|
-
// Elements with slider role
|
|
518
543
|
'[role="textbox"]',
|
|
519
|
-
// Elements with textbox role
|
|
520
544
|
'[role="searchbox"]',
|
|
521
|
-
|
|
545
|
+
'[role="spinbutton"]',
|
|
522
546
|
'[contenteditable="true"]',
|
|
523
|
-
// Editable content
|
|
524
547
|
'[tabindex]:not([tabindex="-1"])'
|
|
525
|
-
|
|
526
|
-
];
|
|
527
|
-
const visualSelectors = [
|
|
528
|
-
"img",
|
|
529
|
-
// Images
|
|
530
|
-
"picture",
|
|
531
|
-
// Picture elements
|
|
532
|
-
"svg",
|
|
533
|
-
// SVG graphics
|
|
534
|
-
"video",
|
|
535
|
-
// Video elements
|
|
536
|
-
"canvas",
|
|
537
|
-
// Canvas elements
|
|
538
|
-
'[style*="background-image"]'
|
|
539
|
-
// Elements with background images
|
|
540
|
-
];
|
|
548
|
+
].join(",");
|
|
541
549
|
function isVisible(element) {
|
|
542
550
|
if (typeof element.checkVisibility === "function") {
|
|
543
|
-
return element.checkVisibility({
|
|
544
|
-
opacityProperty: true,
|
|
545
|
-
visibilityProperty: true,
|
|
546
|
-
contentVisibilityAuto: true
|
|
547
|
-
});
|
|
551
|
+
return element.checkVisibility({ opacityProperty: true, visibilityProperty: true, contentVisibilityAuto: true });
|
|
548
552
|
}
|
|
549
553
|
const style = window.getComputedStyle(element);
|
|
550
554
|
return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0" && element.offsetWidth > 0 && element.offsetHeight > 0;
|
|
551
555
|
}
|
|
552
|
-
function
|
|
553
|
-
|
|
554
|
-
|
|
556
|
+
function getAccessibleName(el) {
|
|
557
|
+
const ariaLabel = el.getAttribute("aria-label");
|
|
558
|
+
if (ariaLabel) return ariaLabel.trim();
|
|
559
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
560
|
+
if (labelledBy) {
|
|
561
|
+
const texts = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim() || "").filter(Boolean);
|
|
562
|
+
if (texts.length > 0) return texts.join(" ").slice(0, 100);
|
|
563
|
+
}
|
|
564
|
+
const tag = el.tagName.toLowerCase();
|
|
565
|
+
if (tag === "img" || tag === "input" && el.getAttribute("type") === "image") {
|
|
566
|
+
const alt = el.getAttribute("alt");
|
|
567
|
+
if (alt !== null) return alt.trim();
|
|
568
|
+
}
|
|
569
|
+
if (["input", "select", "textarea"].includes(tag)) {
|
|
570
|
+
const id = el.getAttribute("id");
|
|
571
|
+
if (id) {
|
|
572
|
+
const label = document.querySelector(`label[for="${CSS.escape(id)}"]`);
|
|
573
|
+
if (label) return label.textContent?.trim() || "";
|
|
574
|
+
}
|
|
575
|
+
const parentLabel = el.closest("label");
|
|
576
|
+
if (parentLabel) {
|
|
577
|
+
const clone = parentLabel.cloneNode(true);
|
|
578
|
+
clone.querySelectorAll("input,select,textarea").forEach((n) => n.remove());
|
|
579
|
+
const lt = clone.textContent?.trim();
|
|
580
|
+
if (lt) return lt;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const ph = el.getAttribute("placeholder");
|
|
584
|
+
if (ph) return ph.trim();
|
|
585
|
+
const title = el.getAttribute("title");
|
|
586
|
+
if (title) return title.trim();
|
|
587
|
+
return (el.textContent?.trim().replace(/\s+/g, " ") || "").slice(0, 100);
|
|
588
|
+
}
|
|
589
|
+
function getSelector(element) {
|
|
590
|
+
const tag = element.tagName.toLowerCase();
|
|
591
|
+
const text = element.textContent?.trim().replace(/\s+/g, " ");
|
|
592
|
+
if (text && text.length > 0 && text.length <= 50) {
|
|
593
|
+
const sameTagElements = document.querySelectorAll(tag);
|
|
594
|
+
let matchCount = 0;
|
|
595
|
+
sameTagElements.forEach((el) => {
|
|
596
|
+
if (el.textContent?.includes(text)) matchCount++;
|
|
597
|
+
});
|
|
598
|
+
if (matchCount === 1) return `${tag}*=${text}`;
|
|
599
|
+
}
|
|
600
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
601
|
+
if (ariaLabel && ariaLabel.length <= 80) return `aria/${ariaLabel}`;
|
|
602
|
+
const testId = element.getAttribute("data-testid");
|
|
603
|
+
if (testId) {
|
|
604
|
+
const sel = `[data-testid="${CSS.escape(testId)}"]`;
|
|
605
|
+
if (document.querySelectorAll(sel).length === 1) return sel;
|
|
606
|
+
}
|
|
607
|
+
if (element.id) return `#${CSS.escape(element.id)}`;
|
|
608
|
+
const nameAttr = element.getAttribute("name");
|
|
609
|
+
if (nameAttr) {
|
|
610
|
+
const sel = `${tag}[name="${CSS.escape(nameAttr)}"]`;
|
|
611
|
+
if (document.querySelectorAll(sel).length === 1) return sel;
|
|
555
612
|
}
|
|
556
613
|
if (element.className && typeof element.className === "string") {
|
|
557
614
|
const classes = element.className.trim().split(/\s+/).filter(Boolean);
|
|
558
|
-
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
615
|
+
for (const cls of classes) {
|
|
616
|
+
const sel = `${tag}.${CSS.escape(cls)}`;
|
|
617
|
+
if (document.querySelectorAll(sel).length === 1) return sel;
|
|
618
|
+
}
|
|
619
|
+
if (classes.length >= 2) {
|
|
620
|
+
const sel = `${tag}${classes.slice(0, 2).map((c) => `.${CSS.escape(c)}`).join("")}`;
|
|
621
|
+
if (document.querySelectorAll(sel).length === 1) return sel;
|
|
564
622
|
}
|
|
565
623
|
}
|
|
566
624
|
let current = element;
|
|
567
625
|
const path = [];
|
|
568
626
|
while (current && current !== document.documentElement) {
|
|
569
|
-
let
|
|
627
|
+
let seg = current.tagName.toLowerCase();
|
|
570
628
|
if (current.id) {
|
|
571
|
-
|
|
572
|
-
path.unshift(selector);
|
|
629
|
+
path.unshift(`#${CSS.escape(current.id)}`);
|
|
573
630
|
break;
|
|
574
631
|
}
|
|
575
632
|
const parent = current.parentElement;
|
|
576
633
|
if (parent) {
|
|
577
|
-
const siblings = Array.from(parent.children).filter(
|
|
578
|
-
|
|
579
|
-
);
|
|
580
|
-
if (siblings.length > 1) {
|
|
581
|
-
const index = siblings.indexOf(current) + 1;
|
|
582
|
-
selector += `:nth-child(${index})`;
|
|
583
|
-
}
|
|
634
|
+
const siblings = Array.from(parent.children).filter((c) => c.tagName === current.tagName);
|
|
635
|
+
if (siblings.length > 1) seg += `:nth-of-type(${siblings.indexOf(current) + 1})`;
|
|
584
636
|
}
|
|
585
|
-
path.unshift(
|
|
637
|
+
path.unshift(seg);
|
|
586
638
|
current = current.parentElement;
|
|
587
|
-
if (path.length >= 4)
|
|
588
|
-
break;
|
|
589
|
-
}
|
|
639
|
+
if (path.length >= 4) break;
|
|
590
640
|
}
|
|
591
641
|
return path.join(" > ");
|
|
592
642
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
id: el.id || "",
|
|
619
|
-
className: (typeof el.className === "string" ? el.className : "") || "",
|
|
620
|
-
textContent: el.textContent?.trim() || "",
|
|
621
|
-
value: inputEl.value || "",
|
|
622
|
-
placeholder: inputEl.placeholder || "",
|
|
623
|
-
href: el.getAttribute("href") || "",
|
|
624
|
-
ariaLabel: el.getAttribute("aria-label") || "",
|
|
625
|
-
role: el.getAttribute("role") || "",
|
|
626
|
-
src: el.getAttribute("src") || "",
|
|
627
|
-
alt: el.getAttribute("alt") || "",
|
|
628
|
-
cssSelector: getCssSelector(el),
|
|
629
|
-
isInViewport
|
|
643
|
+
const elements = [];
|
|
644
|
+
const seen = /* @__PURE__ */ new Set();
|
|
645
|
+
document.querySelectorAll(interactableSelectors).forEach((el) => {
|
|
646
|
+
if (seen.has(el)) return;
|
|
647
|
+
seen.add(el);
|
|
648
|
+
const htmlEl = el;
|
|
649
|
+
if (!isVisible(htmlEl)) return;
|
|
650
|
+
const inputEl = htmlEl;
|
|
651
|
+
const rect = htmlEl.getBoundingClientRect();
|
|
652
|
+
const isInViewport = rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth);
|
|
653
|
+
const entry = {
|
|
654
|
+
tagName: htmlEl.tagName.toLowerCase(),
|
|
655
|
+
name: getAccessibleName(htmlEl),
|
|
656
|
+
type: htmlEl.getAttribute("type") || "",
|
|
657
|
+
value: inputEl.value || "",
|
|
658
|
+
href: htmlEl.getAttribute("href") || "",
|
|
659
|
+
selector: getSelector(htmlEl),
|
|
660
|
+
isInViewport
|
|
661
|
+
};
|
|
662
|
+
if (includeBounds) {
|
|
663
|
+
entry.boundingBox = {
|
|
664
|
+
x: rect.x + window.scrollX,
|
|
665
|
+
y: rect.y + window.scrollY,
|
|
666
|
+
width: rect.width,
|
|
667
|
+
height: rect.height
|
|
630
668
|
};
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
return getElements();
|
|
669
|
+
}
|
|
670
|
+
elements.push(entry);
|
|
671
|
+
});
|
|
672
|
+
return elements;
|
|
636
673
|
})();
|
|
637
|
-
async function
|
|
638
|
-
const {
|
|
639
|
-
return browser.execute(elementsScript,
|
|
674
|
+
async function getInteractableBrowserElements(browser, options = {}) {
|
|
675
|
+
const { includeBounds = false } = options;
|
|
676
|
+
return browser.execute(elementsScript, includeBounds);
|
|
640
677
|
}
|
|
641
678
|
|
|
642
679
|
// src/locators/constants.ts
|
|
@@ -1604,20 +1641,11 @@ import { encode } from "@toon-format/toon";
|
|
|
1604
1641
|
import { z as z7 } from "zod";
|
|
1605
1642
|
var getVisibleElementsToolDefinition = {
|
|
1606
1643
|
name: "get_visible_elements",
|
|
1607
|
-
description:
|
|
1644
|
+
description: "Get interactable elements on the page (buttons, links, inputs). Use get_accessibility for page structure and non-interactable elements.",
|
|
1608
1645
|
inputSchema: {
|
|
1609
|
-
inViewportOnly: z7.boolean().optional().describe(
|
|
1610
|
-
|
|
1611
|
-
),
|
|
1612
|
-
includeContainers: z7.boolean().optional().describe(
|
|
1613
|
-
"Include layout containers (ViewGroup, FrameLayout, ScrollView, etc). Default: false. Set to true to see all elements including layouts."
|
|
1614
|
-
),
|
|
1615
|
-
includeBounds: z7.boolean().optional().describe(
|
|
1616
|
-
"Include element bounds/coordinates (x, y, width, height). Default: false. Set to true for coordinate-based interactions or layout debugging."
|
|
1617
|
-
),
|
|
1618
|
-
elementType: z7.enum(["interactable", "visual", "all"]).optional().describe(
|
|
1619
|
-
'Type of elements to return: "interactable" (default) for buttons/links/inputs, "visual" for images/SVGs, "all" for both.'
|
|
1620
|
-
),
|
|
1646
|
+
inViewportOnly: z7.boolean().optional().describe("Only return elements within the visible viewport. Default: true. Set to false to get ALL elements on the page."),
|
|
1647
|
+
includeContainers: z7.boolean().optional().describe("Mobile only: include layout containers. Default: false."),
|
|
1648
|
+
includeBounds: z7.boolean().optional().describe("Include element bounds/coordinates (x, y, width, height). Default: false."),
|
|
1621
1649
|
limit: z7.number().optional().describe("Maximum number of elements to return. Default: 0 (unlimited)."),
|
|
1622
1650
|
offset: z7.number().optional().describe("Number of elements to skip (for pagination). Default: 0.")
|
|
1623
1651
|
}
|
|
@@ -1629,7 +1657,6 @@ var getVisibleElementsTool = async (args) => {
|
|
|
1629
1657
|
inViewportOnly = true,
|
|
1630
1658
|
includeContainers = false,
|
|
1631
1659
|
includeBounds = false,
|
|
1632
|
-
elementType = "interactable",
|
|
1633
1660
|
limit = 0,
|
|
1634
1661
|
offset = 0
|
|
1635
1662
|
} = args || {};
|
|
@@ -1638,7 +1665,7 @@ var getVisibleElementsTool = async (args) => {
|
|
|
1638
1665
|
const platform = browser.isAndroid ? "android" : "ios";
|
|
1639
1666
|
elements = await getMobileVisibleElements(browser, platform, { includeContainers, includeBounds });
|
|
1640
1667
|
} else {
|
|
1641
|
-
elements = await
|
|
1668
|
+
elements = await getInteractableBrowserElements(browser, { includeBounds });
|
|
1642
1669
|
}
|
|
1643
1670
|
if (inViewportOnly) {
|
|
1644
1671
|
elements = elements.filter((el) => el.isInViewport !== false);
|
|
@@ -1667,8 +1694,327 @@ var getVisibleElementsTool = async (args) => {
|
|
|
1667
1694
|
}
|
|
1668
1695
|
};
|
|
1669
1696
|
|
|
1670
|
-
// src/
|
|
1697
|
+
// src/scripts/get-browser-accessibility-tree.ts
|
|
1698
|
+
var accessibilityTreeScript = () => (function() {
|
|
1699
|
+
const INPUT_TYPE_ROLES = {
|
|
1700
|
+
text: "textbox",
|
|
1701
|
+
search: "searchbox",
|
|
1702
|
+
email: "textbox",
|
|
1703
|
+
url: "textbox",
|
|
1704
|
+
tel: "textbox",
|
|
1705
|
+
password: "textbox",
|
|
1706
|
+
number: "spinbutton",
|
|
1707
|
+
checkbox: "checkbox",
|
|
1708
|
+
radio: "radio",
|
|
1709
|
+
range: "slider",
|
|
1710
|
+
submit: "button",
|
|
1711
|
+
reset: "button",
|
|
1712
|
+
image: "button",
|
|
1713
|
+
file: "button",
|
|
1714
|
+
color: "button"
|
|
1715
|
+
};
|
|
1716
|
+
const LANDMARK_ROLES = /* @__PURE__ */ new Set([
|
|
1717
|
+
"navigation",
|
|
1718
|
+
"main",
|
|
1719
|
+
"banner",
|
|
1720
|
+
"contentinfo",
|
|
1721
|
+
"complementary",
|
|
1722
|
+
"form",
|
|
1723
|
+
"dialog",
|
|
1724
|
+
"region"
|
|
1725
|
+
]);
|
|
1726
|
+
const CONTAINER_ROLES = /* @__PURE__ */ new Set([
|
|
1727
|
+
"navigation",
|
|
1728
|
+
"banner",
|
|
1729
|
+
"contentinfo",
|
|
1730
|
+
"complementary",
|
|
1731
|
+
"main",
|
|
1732
|
+
"form",
|
|
1733
|
+
"region",
|
|
1734
|
+
"group",
|
|
1735
|
+
"list",
|
|
1736
|
+
"listitem",
|
|
1737
|
+
"table",
|
|
1738
|
+
"row",
|
|
1739
|
+
"rowgroup",
|
|
1740
|
+
"generic"
|
|
1741
|
+
]);
|
|
1742
|
+
function getRole(el) {
|
|
1743
|
+
const explicit = el.getAttribute("role");
|
|
1744
|
+
if (explicit) return explicit.split(" ")[0];
|
|
1745
|
+
const tag = el.tagName.toLowerCase();
|
|
1746
|
+
switch (tag) {
|
|
1747
|
+
case "button":
|
|
1748
|
+
return "button";
|
|
1749
|
+
case "a":
|
|
1750
|
+
return el.hasAttribute("href") ? "link" : null;
|
|
1751
|
+
case "input": {
|
|
1752
|
+
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
1753
|
+
if (type === "hidden") return null;
|
|
1754
|
+
return INPUT_TYPE_ROLES[type] || "textbox";
|
|
1755
|
+
}
|
|
1756
|
+
case "select":
|
|
1757
|
+
return "combobox";
|
|
1758
|
+
case "textarea":
|
|
1759
|
+
return "textbox";
|
|
1760
|
+
case "h1":
|
|
1761
|
+
case "h2":
|
|
1762
|
+
case "h3":
|
|
1763
|
+
case "h4":
|
|
1764
|
+
case "h5":
|
|
1765
|
+
case "h6":
|
|
1766
|
+
return "heading";
|
|
1767
|
+
case "img":
|
|
1768
|
+
return "img";
|
|
1769
|
+
case "nav":
|
|
1770
|
+
return "navigation";
|
|
1771
|
+
case "main":
|
|
1772
|
+
return "main";
|
|
1773
|
+
case "header":
|
|
1774
|
+
return !el.closest("article,aside,main,nav,section") ? "banner" : null;
|
|
1775
|
+
case "footer":
|
|
1776
|
+
return !el.closest("article,aside,main,nav,section") ? "contentinfo" : null;
|
|
1777
|
+
case "aside":
|
|
1778
|
+
return "complementary";
|
|
1779
|
+
case "dialog":
|
|
1780
|
+
return "dialog";
|
|
1781
|
+
case "form":
|
|
1782
|
+
return "form";
|
|
1783
|
+
case "section":
|
|
1784
|
+
return el.hasAttribute("aria-label") || el.hasAttribute("aria-labelledby") ? "region" : null;
|
|
1785
|
+
case "summary":
|
|
1786
|
+
return "button";
|
|
1787
|
+
case "details":
|
|
1788
|
+
return "group";
|
|
1789
|
+
case "progress":
|
|
1790
|
+
return "progressbar";
|
|
1791
|
+
case "meter":
|
|
1792
|
+
return "meter";
|
|
1793
|
+
case "ul":
|
|
1794
|
+
case "ol":
|
|
1795
|
+
return "list";
|
|
1796
|
+
case "li":
|
|
1797
|
+
return "listitem";
|
|
1798
|
+
case "table":
|
|
1799
|
+
return "table";
|
|
1800
|
+
}
|
|
1801
|
+
if (el.contentEditable === "true") return "textbox";
|
|
1802
|
+
if (el.hasAttribute("tabindex") && parseInt(el.getAttribute("tabindex") || "-1", 10) >= 0) return "generic";
|
|
1803
|
+
return null;
|
|
1804
|
+
}
|
|
1805
|
+
function getAccessibleName(el, role) {
|
|
1806
|
+
const ariaLabel = el.getAttribute("aria-label");
|
|
1807
|
+
if (ariaLabel) return ariaLabel.trim();
|
|
1808
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
1809
|
+
if (labelledBy) {
|
|
1810
|
+
const texts = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim() || "").filter(Boolean);
|
|
1811
|
+
if (texts.length > 0) return texts.join(" ").slice(0, 100);
|
|
1812
|
+
}
|
|
1813
|
+
const tag = el.tagName.toLowerCase();
|
|
1814
|
+
if (tag === "img" || tag === "input" && el.getAttribute("type") === "image") {
|
|
1815
|
+
const alt = el.getAttribute("alt");
|
|
1816
|
+
if (alt !== null) return alt.trim();
|
|
1817
|
+
}
|
|
1818
|
+
if (["input", "select", "textarea"].includes(tag)) {
|
|
1819
|
+
const id = el.getAttribute("id");
|
|
1820
|
+
if (id) {
|
|
1821
|
+
const label = document.querySelector(`label[for="${CSS.escape(id)}"]`);
|
|
1822
|
+
if (label) return label.textContent?.trim() || "";
|
|
1823
|
+
}
|
|
1824
|
+
const parentLabel = el.closest("label");
|
|
1825
|
+
if (parentLabel) {
|
|
1826
|
+
const clone = parentLabel.cloneNode(true);
|
|
1827
|
+
clone.querySelectorAll("input,select,textarea").forEach((n) => n.remove());
|
|
1828
|
+
const lt = clone.textContent?.trim();
|
|
1829
|
+
if (lt) return lt;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
const ph = el.getAttribute("placeholder");
|
|
1833
|
+
if (ph) return ph.trim();
|
|
1834
|
+
const title = el.getAttribute("title");
|
|
1835
|
+
if (title) return title.trim();
|
|
1836
|
+
if (role && CONTAINER_ROLES.has(role)) return "";
|
|
1837
|
+
return (el.textContent?.trim().replace(/\s+/g, " ") || "").slice(0, 100);
|
|
1838
|
+
}
|
|
1839
|
+
function getSelector(element) {
|
|
1840
|
+
const tag = element.tagName.toLowerCase();
|
|
1841
|
+
const text = element.textContent?.trim().replace(/\s+/g, " ");
|
|
1842
|
+
if (text && text.length > 0 && text.length <= 50) {
|
|
1843
|
+
const sameTagElements = document.querySelectorAll(tag);
|
|
1844
|
+
let matchCount = 0;
|
|
1845
|
+
sameTagElements.forEach((el) => {
|
|
1846
|
+
if (el.textContent?.includes(text)) matchCount++;
|
|
1847
|
+
});
|
|
1848
|
+
if (matchCount === 1) return `${tag}*=${text}`;
|
|
1849
|
+
}
|
|
1850
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
1851
|
+
if (ariaLabel && ariaLabel.length <= 80) return `aria/${ariaLabel}`;
|
|
1852
|
+
const testId = element.getAttribute("data-testid");
|
|
1853
|
+
if (testId) {
|
|
1854
|
+
const sel = `[data-testid="${CSS.escape(testId)}"]`;
|
|
1855
|
+
if (document.querySelectorAll(sel).length === 1) return sel;
|
|
1856
|
+
}
|
|
1857
|
+
if (element.id) return `#${CSS.escape(element.id)}`;
|
|
1858
|
+
const nameAttr = element.getAttribute("name");
|
|
1859
|
+
if (nameAttr) {
|
|
1860
|
+
const sel = `${tag}[name="${CSS.escape(nameAttr)}"]`;
|
|
1861
|
+
if (document.querySelectorAll(sel).length === 1) return sel;
|
|
1862
|
+
}
|
|
1863
|
+
if (element.className && typeof element.className === "string") {
|
|
1864
|
+
const classes = element.className.trim().split(/\s+/).filter(Boolean);
|
|
1865
|
+
for (const cls of classes) {
|
|
1866
|
+
const sel = `${tag}.${CSS.escape(cls)}`;
|
|
1867
|
+
if (document.querySelectorAll(sel).length === 1) return sel;
|
|
1868
|
+
}
|
|
1869
|
+
if (classes.length >= 2) {
|
|
1870
|
+
const sel = `${tag}${classes.slice(0, 2).map((c) => `.${CSS.escape(c)}`).join("")}`;
|
|
1871
|
+
if (document.querySelectorAll(sel).length === 1) return sel;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
let current = element;
|
|
1875
|
+
const path = [];
|
|
1876
|
+
while (current && current !== document.documentElement) {
|
|
1877
|
+
let seg = current.tagName.toLowerCase();
|
|
1878
|
+
if (current.id) {
|
|
1879
|
+
path.unshift(`#${CSS.escape(current.id)}`);
|
|
1880
|
+
break;
|
|
1881
|
+
}
|
|
1882
|
+
const parent = current.parentElement;
|
|
1883
|
+
if (parent) {
|
|
1884
|
+
const siblings = Array.from(parent.children).filter((c) => c.tagName === current.tagName);
|
|
1885
|
+
if (siblings.length > 1) seg += `:nth-of-type(${siblings.indexOf(current) + 1})`;
|
|
1886
|
+
}
|
|
1887
|
+
path.unshift(seg);
|
|
1888
|
+
current = current.parentElement;
|
|
1889
|
+
if (path.length >= 4) break;
|
|
1890
|
+
}
|
|
1891
|
+
return path.join(" > ");
|
|
1892
|
+
}
|
|
1893
|
+
function isVisible(el) {
|
|
1894
|
+
if (typeof el.checkVisibility === "function") {
|
|
1895
|
+
return el.checkVisibility({ opacityProperty: true, visibilityProperty: true, contentVisibilityAuto: true });
|
|
1896
|
+
}
|
|
1897
|
+
const style = window.getComputedStyle(el);
|
|
1898
|
+
return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0" && el.offsetWidth > 0 && el.offsetHeight > 0;
|
|
1899
|
+
}
|
|
1900
|
+
function getLevel(el) {
|
|
1901
|
+
const m = el.tagName.toLowerCase().match(/^h([1-6])$/);
|
|
1902
|
+
if (m) return parseInt(m[1], 10);
|
|
1903
|
+
const ariaLevel = el.getAttribute("aria-level");
|
|
1904
|
+
if (ariaLevel) return parseInt(ariaLevel, 10);
|
|
1905
|
+
return void 0;
|
|
1906
|
+
}
|
|
1907
|
+
function getState2(el) {
|
|
1908
|
+
const inputEl = el;
|
|
1909
|
+
const isCheckable = ["input", "menuitemcheckbox", "menuitemradio"].includes(el.tagName.toLowerCase()) || ["checkbox", "radio", "switch"].includes(el.getAttribute("role") || "");
|
|
1910
|
+
return {
|
|
1911
|
+
disabled: el.getAttribute("aria-disabled") === "true" || inputEl.disabled ? "true" : "",
|
|
1912
|
+
checked: isCheckable && inputEl.checked ? "true" : el.getAttribute("aria-checked") || "",
|
|
1913
|
+
expanded: el.getAttribute("aria-expanded") || "",
|
|
1914
|
+
selected: el.getAttribute("aria-selected") || "",
|
|
1915
|
+
pressed: el.getAttribute("aria-pressed") || "",
|
|
1916
|
+
required: inputEl.required || el.getAttribute("aria-required") === "true" ? "true" : "",
|
|
1917
|
+
readonly: inputEl.readOnly || el.getAttribute("aria-readonly") === "true" ? "true" : ""
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
const result = [];
|
|
1921
|
+
function walk(el, depth = 0) {
|
|
1922
|
+
if (depth > 200) return;
|
|
1923
|
+
if (!isVisible(el)) return;
|
|
1924
|
+
const role = getRole(el);
|
|
1925
|
+
if (!role) {
|
|
1926
|
+
for (const child of Array.from(el.children)) {
|
|
1927
|
+
walk(child, depth + 1);
|
|
1928
|
+
}
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
const name = getAccessibleName(el, role);
|
|
1932
|
+
const isLandmark = LANDMARK_ROLES.has(role);
|
|
1933
|
+
const hasIdentity = !!(name || isLandmark);
|
|
1934
|
+
const selector = hasIdentity ? getSelector(el) : "";
|
|
1935
|
+
const node = { role, name, selector, level: getLevel(el) ?? "", ...getState2(el) };
|
|
1936
|
+
result.push(node);
|
|
1937
|
+
for (const child of Array.from(el.children)) {
|
|
1938
|
+
walk(child, depth + 1);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
for (const child of Array.from(document.body.children)) {
|
|
1942
|
+
walk(child, 0);
|
|
1943
|
+
}
|
|
1944
|
+
return result;
|
|
1945
|
+
})();
|
|
1946
|
+
async function getBrowserAccessibilityTree(browser) {
|
|
1947
|
+
return browser.execute(accessibilityTreeScript);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// src/tools/get-accessibility-tree.tool.ts
|
|
1951
|
+
import { encode as encode2 } from "@toon-format/toon";
|
|
1671
1952
|
import { z as z8 } from "zod";
|
|
1953
|
+
var getAccessibilityToolDefinition = {
|
|
1954
|
+
name: "get_accessibility",
|
|
1955
|
+
description: "Gets the accessibility tree: page structure with headings, landmarks, and semantic roles. Browser-only. Use to understand page layout and context around interactable elements.",
|
|
1956
|
+
inputSchema: {
|
|
1957
|
+
limit: z8.number().optional().describe("Maximum number of nodes to return. Default: 100. Use 0 for unlimited."),
|
|
1958
|
+
offset: z8.number().optional().describe("Number of nodes to skip (for pagination). Default: 0."),
|
|
1959
|
+
roles: z8.array(z8.string()).optional().describe('Filter to specific roles (e.g., ["heading", "navigation", "region"]). Default: all roles.')
|
|
1960
|
+
}
|
|
1961
|
+
};
|
|
1962
|
+
var getAccessibilityTreeTool = async (args) => {
|
|
1963
|
+
try {
|
|
1964
|
+
const browser = getBrowser();
|
|
1965
|
+
if (browser.isAndroid || browser.isIOS) {
|
|
1966
|
+
return {
|
|
1967
|
+
content: [{
|
|
1968
|
+
type: "text",
|
|
1969
|
+
text: "Error: get_accessibility is browser-only. For mobile apps, use get_visible_elements instead."
|
|
1970
|
+
}]
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
const { limit = 100, offset = 0, roles } = args || {};
|
|
1974
|
+
let nodes = await getBrowserAccessibilityTree(browser);
|
|
1975
|
+
if (nodes.length === 0) {
|
|
1976
|
+
return {
|
|
1977
|
+
content: [{ type: "text", text: "No accessibility tree available" }]
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
nodes = nodes.filter((n) => n.name && n.name.trim() !== "");
|
|
1981
|
+
if (roles && roles.length > 0) {
|
|
1982
|
+
const roleSet = new Set(roles.map((r) => r.toLowerCase()));
|
|
1983
|
+
nodes = nodes.filter((n) => n.role && roleSet.has(n.role.toLowerCase()));
|
|
1984
|
+
}
|
|
1985
|
+
const total = nodes.length;
|
|
1986
|
+
if (offset > 0) {
|
|
1987
|
+
nodes = nodes.slice(offset);
|
|
1988
|
+
}
|
|
1989
|
+
if (limit > 0) {
|
|
1990
|
+
nodes = nodes.slice(0, limit);
|
|
1991
|
+
}
|
|
1992
|
+
const stateKeys = ["level", "disabled", "checked", "expanded", "selected", "pressed", "required", "readonly"];
|
|
1993
|
+
const usedKeys = stateKeys.filter((k) => nodes.some((n) => n[k] !== ""));
|
|
1994
|
+
const trimmed = nodes.map(({ role, name, selector, ...state2 }) => {
|
|
1995
|
+
const node = { role, name, selector };
|
|
1996
|
+
for (const k of usedKeys) node[k] = state2[k];
|
|
1997
|
+
return node;
|
|
1998
|
+
});
|
|
1999
|
+
const result = {
|
|
2000
|
+
total,
|
|
2001
|
+
showing: trimmed.length,
|
|
2002
|
+
hasMore: offset + trimmed.length < total,
|
|
2003
|
+
nodes: trimmed
|
|
2004
|
+
};
|
|
2005
|
+
const toon = encode2(result).replace(/,""/g, ",").replace(/"",/g, ",");
|
|
2006
|
+
return {
|
|
2007
|
+
content: [{ type: "text", text: toon }]
|
|
2008
|
+
};
|
|
2009
|
+
} catch (e) {
|
|
2010
|
+
return {
|
|
2011
|
+
content: [{ type: "text", text: `Error getting accessibility tree: ${e}` }]
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
};
|
|
2015
|
+
|
|
2016
|
+
// src/tools/take-screenshot.tool.ts
|
|
2017
|
+
import { z as z9 } from "zod";
|
|
1672
2018
|
import sharp from "sharp";
|
|
1673
2019
|
var MAX_DIMENSION = 2e3;
|
|
1674
2020
|
var MAX_FILE_SIZE_BYTES = 1024 * 1024;
|
|
@@ -1676,7 +2022,7 @@ var takeScreenshotToolDefinition = {
|
|
|
1676
2022
|
name: "take_screenshot",
|
|
1677
2023
|
description: "captures a screenshot of the current page",
|
|
1678
2024
|
inputSchema: {
|
|
1679
|
-
outputPath:
|
|
2025
|
+
outputPath: z9.string().optional().describe("Optional path where to save the screenshot. If not provided, returns base64 data.")
|
|
1680
2026
|
}
|
|
1681
2027
|
};
|
|
1682
2028
|
async function processScreenshot(screenshotBase64) {
|
|
@@ -1728,12 +2074,12 @@ var takeScreenshotTool = async ({ outputPath }) => {
|
|
|
1728
2074
|
};
|
|
1729
2075
|
|
|
1730
2076
|
// src/tools/cookies.tool.ts
|
|
1731
|
-
import { z as
|
|
2077
|
+
import { z as z10 } from "zod";
|
|
1732
2078
|
var getCookiesToolDefinition = {
|
|
1733
2079
|
name: "get_cookies",
|
|
1734
2080
|
description: "gets all cookies or a specific cookie by name",
|
|
1735
2081
|
inputSchema: {
|
|
1736
|
-
name:
|
|
2082
|
+
name: z10.string().optional().describe("Optional cookie name to retrieve a specific cookie. If not provided, returns all cookies")
|
|
1737
2083
|
}
|
|
1738
2084
|
};
|
|
1739
2085
|
var getCookiesTool = async ({ name }) => {
|
|
@@ -1769,14 +2115,14 @@ var setCookieToolDefinition = {
|
|
|
1769
2115
|
name: "set_cookie",
|
|
1770
2116
|
description: "sets a cookie with specified name, value, and optional attributes",
|
|
1771
2117
|
inputSchema: {
|
|
1772
|
-
name:
|
|
1773
|
-
value:
|
|
1774
|
-
domain:
|
|
1775
|
-
path:
|
|
1776
|
-
expiry:
|
|
1777
|
-
httpOnly:
|
|
1778
|
-
secure:
|
|
1779
|
-
sameSite:
|
|
2118
|
+
name: z10.string().describe("Cookie name"),
|
|
2119
|
+
value: z10.string().describe("Cookie value"),
|
|
2120
|
+
domain: z10.string().optional().describe("Cookie domain (defaults to current domain)"),
|
|
2121
|
+
path: z10.string().optional().describe('Cookie path (defaults to "/")'),
|
|
2122
|
+
expiry: z10.number().optional().describe("Expiry date as Unix timestamp in seconds"),
|
|
2123
|
+
httpOnly: z10.boolean().optional().describe("HttpOnly flag"),
|
|
2124
|
+
secure: z10.boolean().optional().describe("Secure flag"),
|
|
2125
|
+
sameSite: z10.enum(["strict", "lax", "none"]).optional().describe("SameSite attribute")
|
|
1780
2126
|
}
|
|
1781
2127
|
};
|
|
1782
2128
|
var setCookieTool = async ({
|
|
@@ -1806,7 +2152,7 @@ var deleteCookiesToolDefinition = {
|
|
|
1806
2152
|
name: "delete_cookies",
|
|
1807
2153
|
description: "deletes all cookies or a specific cookie by name",
|
|
1808
2154
|
inputSchema: {
|
|
1809
|
-
name:
|
|
2155
|
+
name: z10.string().optional().describe("Optional cookie name to delete a specific cookie. If not provided, deletes all cookies")
|
|
1810
2156
|
}
|
|
1811
2157
|
};
|
|
1812
2158
|
var deleteCookiesTool = async ({ name }) => {
|
|
@@ -1829,124 +2175,6 @@ var deleteCookiesTool = async ({ name }) => {
|
|
|
1829
2175
|
}
|
|
1830
2176
|
};
|
|
1831
2177
|
|
|
1832
|
-
// src/scripts/get-browser-accessibility-tree.ts
|
|
1833
|
-
function flattenAccessibilityTree(node, result = []) {
|
|
1834
|
-
if (!node) return result;
|
|
1835
|
-
if (node.role !== "WebArea" || node.name) {
|
|
1836
|
-
const entry = {
|
|
1837
|
-
role: node.role || "",
|
|
1838
|
-
name: node.name || "",
|
|
1839
|
-
value: node.value ?? "",
|
|
1840
|
-
description: node.description || "",
|
|
1841
|
-
disabled: node.disabled ? "true" : "",
|
|
1842
|
-
focused: node.focused ? "true" : "",
|
|
1843
|
-
selected: node.selected ? "true" : "",
|
|
1844
|
-
checked: node.checked === true ? "true" : node.checked === false ? "false" : node.checked === "mixed" ? "mixed" : "",
|
|
1845
|
-
expanded: node.expanded === true ? "true" : node.expanded === false ? "false" : "",
|
|
1846
|
-
pressed: node.pressed === true ? "true" : node.pressed === false ? "false" : node.pressed === "mixed" ? "mixed" : "",
|
|
1847
|
-
readonly: node.readonly ? "true" : "",
|
|
1848
|
-
required: node.required ? "true" : "",
|
|
1849
|
-
level: node.level ?? "",
|
|
1850
|
-
valuemin: node.valuemin ?? "",
|
|
1851
|
-
valuemax: node.valuemax ?? "",
|
|
1852
|
-
autocomplete: node.autocomplete || "",
|
|
1853
|
-
haspopup: node.haspopup || "",
|
|
1854
|
-
invalid: node.invalid ? "true" : "",
|
|
1855
|
-
modal: node.modal ? "true" : "",
|
|
1856
|
-
multiline: node.multiline ? "true" : "",
|
|
1857
|
-
multiselectable: node.multiselectable ? "true" : "",
|
|
1858
|
-
orientation: node.orientation || "",
|
|
1859
|
-
keyshortcuts: node.keyshortcuts || "",
|
|
1860
|
-
roledescription: node.roledescription || "",
|
|
1861
|
-
valuetext: node.valuetext || ""
|
|
1862
|
-
};
|
|
1863
|
-
result.push(entry);
|
|
1864
|
-
}
|
|
1865
|
-
if (node.children && Array.isArray(node.children)) {
|
|
1866
|
-
for (const child of node.children) {
|
|
1867
|
-
flattenAccessibilityTree(child, result);
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
return result;
|
|
1871
|
-
}
|
|
1872
|
-
async function getBrowserAccessibilityTree(browser) {
|
|
1873
|
-
const puppeteer = await browser.getPuppeteer();
|
|
1874
|
-
const pages = await puppeteer.pages();
|
|
1875
|
-
if (pages.length === 0) {
|
|
1876
|
-
return [];
|
|
1877
|
-
}
|
|
1878
|
-
const page = pages[0];
|
|
1879
|
-
const snapshot = await page.accessibility.snapshot({
|
|
1880
|
-
interestingOnly: true
|
|
1881
|
-
});
|
|
1882
|
-
if (!snapshot) {
|
|
1883
|
-
return [];
|
|
1884
|
-
}
|
|
1885
|
-
return flattenAccessibilityTree(snapshot);
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
// src/tools/get-accessibility-tree.tool.ts
|
|
1889
|
-
import { encode as encode2 } from "@toon-format/toon";
|
|
1890
|
-
import { z as z10 } from "zod";
|
|
1891
|
-
var getAccessibilityToolDefinition = {
|
|
1892
|
-
name: "get_accessibility",
|
|
1893
|
-
description: "gets accessibility tree snapshot with semantic information about page elements (roles, names, states). Browser-only - use when get_visible_elements does not return expected elements.",
|
|
1894
|
-
inputSchema: {
|
|
1895
|
-
limit: z10.number().optional().describe("Maximum number of nodes to return. Default: 100. Use 0 for unlimited."),
|
|
1896
|
-
offset: z10.number().optional().describe("Number of nodes to skip (for pagination). Default: 0."),
|
|
1897
|
-
roles: z10.array(z10.string()).optional().describe('Filter to specific roles (e.g., ["button", "link", "textbox"]). Default: all roles.'),
|
|
1898
|
-
namedOnly: z10.boolean().optional().describe("Only return nodes with a name/label. Default: true. Filters out anonymous containers.")
|
|
1899
|
-
}
|
|
1900
|
-
};
|
|
1901
|
-
var getAccessibilityTreeTool = async (args) => {
|
|
1902
|
-
try {
|
|
1903
|
-
const browser = getBrowser();
|
|
1904
|
-
if (browser.isAndroid || browser.isIOS) {
|
|
1905
|
-
return {
|
|
1906
|
-
content: [{
|
|
1907
|
-
type: "text",
|
|
1908
|
-
text: "Error: get_accessibility is browser-only. For mobile apps, use get_visible_elements instead."
|
|
1909
|
-
}]
|
|
1910
|
-
};
|
|
1911
|
-
}
|
|
1912
|
-
const { limit = 100, offset = 0, roles, namedOnly = true } = args || {};
|
|
1913
|
-
let nodes = await getBrowserAccessibilityTree(browser);
|
|
1914
|
-
if (nodes.length === 0) {
|
|
1915
|
-
return {
|
|
1916
|
-
content: [{ type: "text", text: "No accessibility tree available" }]
|
|
1917
|
-
};
|
|
1918
|
-
}
|
|
1919
|
-
if (namedOnly) {
|
|
1920
|
-
nodes = nodes.filter((n) => n.name && n.name.trim() !== "");
|
|
1921
|
-
}
|
|
1922
|
-
if (roles && roles.length > 0) {
|
|
1923
|
-
const roleSet = new Set(roles.map((r) => r.toLowerCase()));
|
|
1924
|
-
nodes = nodes.filter((n) => n.role && roleSet.has(n.role.toLowerCase()));
|
|
1925
|
-
}
|
|
1926
|
-
const total = nodes.length;
|
|
1927
|
-
if (offset > 0) {
|
|
1928
|
-
nodes = nodes.slice(offset);
|
|
1929
|
-
}
|
|
1930
|
-
if (limit > 0) {
|
|
1931
|
-
nodes = nodes.slice(0, limit);
|
|
1932
|
-
}
|
|
1933
|
-
const result = {
|
|
1934
|
-
total,
|
|
1935
|
-
showing: nodes.length,
|
|
1936
|
-
hasMore: offset + nodes.length < total,
|
|
1937
|
-
nodes
|
|
1938
|
-
};
|
|
1939
|
-
const toon = encode2(result).replace(/,""/g, ",").replace(/"",/g, ",");
|
|
1940
|
-
return {
|
|
1941
|
-
content: [{ type: "text", text: toon }]
|
|
1942
|
-
};
|
|
1943
|
-
} catch (e) {
|
|
1944
|
-
return {
|
|
1945
|
-
content: [{ type: "text", text: `Error getting accessibility tree: ${e}` }]
|
|
1946
|
-
};
|
|
1947
|
-
}
|
|
1948
|
-
};
|
|
1949
|
-
|
|
1950
2178
|
// src/tools/gestures.tool.ts
|
|
1951
2179
|
import { z as z11 } from "zod";
|
|
1952
2180
|
var tapElementToolDefinition = {
|
|
@@ -2282,6 +2510,8 @@ var executeScriptToolDefinition = {
|
|
|
2282
2510
|
name: "execute_script",
|
|
2283
2511
|
description: `Executes JavaScript in browser or mobile commands via Appium.
|
|
2284
2512
|
|
|
2513
|
+
**Option B for browser interaction** \u2014 prefer get_visible_elements or click_element/set_value with a selector instead. Use execute_script only when no dedicated tool covers the action (e.g. reading computed values, triggering custom events, scrolling to a position).
|
|
2514
|
+
|
|
2285
2515
|
**Browser:** Runs JavaScript in page context. Use 'return' to get values back.
|
|
2286
2516
|
- Example: execute_script({ script: "return document.title" })
|
|
2287
2517
|
- Example: execute_script({ script: "return window.scrollY" })
|
|
@@ -2347,7 +2577,7 @@ var package_default = {
|
|
|
2347
2577
|
type: "git",
|
|
2348
2578
|
url: "git://github.com/webdriverio/mcp.git"
|
|
2349
2579
|
},
|
|
2350
|
-
version: "2.
|
|
2580
|
+
version: "2.3.0",
|
|
2351
2581
|
description: "MCP server with WebdriverIO for browser and mobile app automation (iOS/Android via Appium)",
|
|
2352
2582
|
main: "./lib/server.js",
|
|
2353
2583
|
module: "./lib/server.js",
|
|
@@ -2379,19 +2609,21 @@ var package_default = {
|
|
|
2379
2609
|
bundle: "tsup && shx chmod +x lib/server.js",
|
|
2380
2610
|
postbundle: "npm pack",
|
|
2381
2611
|
lint: "eslint src/ --fix && tsc --noEmit",
|
|
2612
|
+
"lint:tests": "eslint tests/ --fix && tsc -p tsconfig.test.json --noEmit",
|
|
2382
2613
|
start: "node lib/server.js",
|
|
2383
2614
|
dev: "tsx --watch src/server.ts",
|
|
2384
|
-
prepare: "husky"
|
|
2615
|
+
prepare: "husky",
|
|
2616
|
+
test: "vitest run"
|
|
2385
2617
|
},
|
|
2386
2618
|
dependencies: {
|
|
2387
|
-
"@modelcontextprotocol/sdk": "1.
|
|
2388
|
-
xpath: "^0.0.34",
|
|
2619
|
+
"@modelcontextprotocol/sdk": "1.27",
|
|
2389
2620
|
"@toon-format/toon": "^2.1.0",
|
|
2390
2621
|
"@wdio/protocols": "^9.16.2",
|
|
2391
2622
|
"@xmldom/xmldom": "^0.8.11",
|
|
2392
2623
|
"puppeteer-core": "^24.35.0",
|
|
2393
2624
|
sharp: "^0.34.5",
|
|
2394
|
-
webdriverio: "9.
|
|
2625
|
+
webdriverio: "9.24",
|
|
2626
|
+
xpath: "^0.0.34",
|
|
2395
2627
|
zod: "^4.3.5"
|
|
2396
2628
|
},
|
|
2397
2629
|
devDependencies: {
|
|
@@ -2400,13 +2632,15 @@ var package_default = {
|
|
|
2400
2632
|
"@wdio/eslint": "^0.1.3",
|
|
2401
2633
|
"@wdio/types": "^9.20.0",
|
|
2402
2634
|
eslint: "^9.39.2",
|
|
2635
|
+
"happy-dom": "^20.7.0",
|
|
2403
2636
|
husky: "^9.1.7",
|
|
2404
2637
|
"release-it": "^19.2.3",
|
|
2405
2638
|
rimraf: "^6.1.2",
|
|
2406
2639
|
shx: "^0.4.0",
|
|
2407
2640
|
tsup: "^8.5.1",
|
|
2408
2641
|
tsx: "^4.21.0",
|
|
2409
|
-
typescript: "5.9"
|
|
2642
|
+
typescript: "5.9",
|
|
2643
|
+
vitest: "^4.0.18"
|
|
2410
2644
|
},
|
|
2411
2645
|
packageManager: "pnpm@10.12.4"
|
|
2412
2646
|
};
|