@wdio/mcp 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/server.js CHANGED
@@ -7,10 +7,13 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
7
7
  // src/tools/browser.tool.ts
8
8
  import { remote } from "webdriverio";
9
9
  import { z } from "zod";
10
+ var supportedBrowsers = ["chrome", "firefox", "edge", "safari"];
11
+ var browserSchema = z.enum(supportedBrowsers).default("chrome");
10
12
  var startBrowserToolDefinition = {
11
13
  name: "start_browser",
12
- description: "starts a browser session and sets it to the current state",
14
+ description: "starts a browser session (Chrome, Firefox, Edge, Safari) and sets it to the current state",
13
15
  inputSchema: {
16
+ browser: browserSchema.describe("Browser to launch: chrome, firefox, edge, safari (default: chrome)"),
14
17
  headless: z.boolean().optional(),
15
18
  windowWidth: z.number().min(400).max(3840).optional().default(1920),
16
19
  windowHeight: z.number().min(400).max(2160).optional().default(1080),
@@ -38,12 +41,22 @@ var getBrowser = () => {
38
41
  };
39
42
  getBrowser.__state = state;
40
43
  var startBrowserTool = async ({
44
+ browser = "chrome",
41
45
  headless = false,
42
46
  windowWidth = 1920,
43
47
  windowHeight = 1080,
44
48
  navigationUrl
45
49
  }) => {
46
- const chromeArgs = [
50
+ const browserDisplayNames = {
51
+ chrome: "Chrome",
52
+ firefox: "Firefox",
53
+ edge: "Edge",
54
+ safari: "Safari"
55
+ };
56
+ const selectedBrowser = browser;
57
+ const headlessSupported = selectedBrowser !== "safari";
58
+ const effectiveHeadless = headless && headlessSupported;
59
+ const chromiumArgs = [
47
60
  `--window-size=${windowWidth},${windowHeight}`,
48
61
  "--no-sandbox",
49
62
  "--disable-search-engine-choice-screen",
@@ -54,37 +67,66 @@ var startBrowserTool = async ({
54
67
  "--disable-web-security",
55
68
  "--allow-running-insecure-content"
56
69
  ];
57
- if (headless) {
58
- chromeArgs.push("--headless=new");
59
- chromeArgs.push("--disable-gpu");
60
- chromeArgs.push("--disable-dev-shm-usage");
61
- }
62
- const browser = await remote({
63
- capabilities: {
64
- browserName: "chrome",
65
- "goog:chromeOptions": {
66
- args: chromeArgs
67
- },
68
- acceptInsecureCerts: true
69
- }
70
+ if (effectiveHeadless) {
71
+ chromiumArgs.push("--headless=new");
72
+ chromiumArgs.push("--disable-gpu");
73
+ chromiumArgs.push("--disable-dev-shm-usage");
74
+ }
75
+ const firefoxArgs = [];
76
+ if (effectiveHeadless && selectedBrowser === "firefox") {
77
+ firefoxArgs.push("-headless");
78
+ }
79
+ const capabilities = {
80
+ acceptInsecureCerts: true
81
+ };
82
+ switch (selectedBrowser) {
83
+ case "chrome":
84
+ capabilities.browserName = "chrome";
85
+ capabilities["goog:chromeOptions"] = { args: chromiumArgs };
86
+ break;
87
+ case "edge":
88
+ capabilities.browserName = "msedge";
89
+ capabilities["ms:edgeOptions"] = { args: chromiumArgs };
90
+ break;
91
+ case "firefox":
92
+ capabilities.browserName = "firefox";
93
+ if (firefoxArgs.length > 0) {
94
+ capabilities["moz:firefoxOptions"] = { args: firefoxArgs };
95
+ }
96
+ break;
97
+ case "safari":
98
+ capabilities.browserName = "safari";
99
+ break;
100
+ }
101
+ const wdioBrowser = await remote({
102
+ capabilities
70
103
  });
71
- const { sessionId } = browser;
72
- state.browsers.set(sessionId, browser);
104
+ const { sessionId } = wdioBrowser;
105
+ state.browsers.set(sessionId, wdioBrowser);
73
106
  state.currentSession = sessionId;
74
107
  state.sessionMetadata.set(sessionId, {
75
108
  type: "browser",
76
- capabilities: browser.capabilities,
109
+ capabilities: wdioBrowser.capabilities,
77
110
  isAttached: false
78
111
  });
112
+ let sizeNote = "";
113
+ try {
114
+ await wdioBrowser.setWindowSize(windowWidth, windowHeight);
115
+ } catch (e) {
116
+ sizeNote = `
117
+ Note: Unable to set window size (${windowWidth}x${windowHeight}). ${e}`;
118
+ }
79
119
  if (navigationUrl) {
80
- await browser.url(navigationUrl);
120
+ await wdioBrowser.url(navigationUrl);
81
121
  }
82
- const modeText = headless ? "headless" : "headed";
122
+ const modeText = effectiveHeadless ? "headless" : "headed";
123
+ const browserText = browserDisplayNames[selectedBrowser];
83
124
  const urlText = navigationUrl ? ` and navigated to ${navigationUrl}` : "";
125
+ const headlessNote = headless && !headlessSupported ? "\nNote: Safari does not support headless mode. Started in headed mode." : "";
84
126
  return {
85
127
  content: [{
86
128
  type: "text",
87
- text: `Browser started in ${modeText} mode with sessionId: ${sessionId} (${windowWidth}x${windowHeight})${urlText}`
129
+ text: `${browserText} browser started in ${modeText} mode with sessionId: ${sessionId} (${windowWidth}x${windowHeight})${urlText}${headlessNote}${sizeNote}`
88
130
  }]
89
131
  };
90
132
  };
@@ -572,130 +614,32 @@ var elementsScript = (elementType = "interactable") => (function() {
572
614
  const isInViewport = rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth);
573
615
  const info = {
574
616
  tagName: el.tagName.toLowerCase(),
617
+ type: el.getAttribute("type") || "",
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") || "",
575
628
  cssSelector: getCssSelector(el),
576
629
  isInViewport
577
630
  };
578
- const type = el.getAttribute("type");
579
- if (type) info.type = type;
580
- const id = el.id;
581
- if (id) info.id = id;
582
- const className = el.className;
583
- if (className && typeof className === "string") info.className = className;
584
- const textContent = el.textContent?.trim();
585
- if (textContent) info.textContent = textContent;
586
- const value = inputEl.value;
587
- if (value) info.value = value;
588
- const placeholder = inputEl.placeholder;
589
- if (placeholder) info.placeholder = placeholder;
590
- const href = el.getAttribute("href");
591
- if (href) info.href = href;
592
- const ariaLabel = el.getAttribute("aria-label");
593
- if (ariaLabel) info.ariaLabel = ariaLabel;
594
- const role = el.getAttribute("role");
595
- if (role) info.role = role;
596
- const src = el.getAttribute("src");
597
- if (src) info.src = src;
598
- const alt = el.getAttribute("alt");
599
- if (alt) info.alt = alt;
600
- if (elementType === "visual" || elementType === "all") {
601
- const bgImage = window.getComputedStyle(el).backgroundImage;
602
- if (bgImage && bgImage !== "none") info.backgroundImage = bgImage;
603
- }
604
631
  return info;
605
632
  });
606
633
  return elementInfos;
607
634
  }
608
635
  return getElements();
609
636
  })();
610
- var get_interactable_browser_elements_default = elementsScript;
611
-
612
- // src/locators/source-parsing.ts
613
- import { DOMParser } from "@xmldom/xmldom";
614
- function childNodesOf(node) {
615
- const children = [];
616
- if (node.childNodes) {
617
- for (let i = 0; i < node.childNodes.length; i++) {
618
- const child = node.childNodes.item(i);
619
- if (child?.nodeType === 1) {
620
- children.push(child);
621
- }
622
- }
623
- }
624
- return children;
625
- }
626
- function translateRecursively(domNode, parentPath = "", index = null) {
627
- const attributes = {};
628
- const element = domNode;
629
- if (element.attributes) {
630
- for (let attrIdx = 0; attrIdx < element.attributes.length; attrIdx++) {
631
- const attr = element.attributes.item(attrIdx);
632
- if (attr) {
633
- attributes[attr.name] = attr.value.replace(/(\n)/gm, "\\n");
634
- }
635
- }
636
- }
637
- const path = index === null ? "" : `${parentPath ? parentPath + "." : ""}${index}`;
638
- return {
639
- children: childNodesOf(domNode).map(
640
- (childNode, childIndex) => translateRecursively(childNode, path, childIndex)
641
- ),
642
- tagName: domNode.nodeName,
643
- attributes,
644
- path
645
- };
646
- }
647
- function xmlToJSON(sourceXML) {
648
- try {
649
- const parser = new DOMParser();
650
- const sourceDoc = parser.parseFromString(sourceXML, "text/xml");
651
- const parseErrors = sourceDoc.getElementsByTagName("parsererror");
652
- if (parseErrors.length > 0) {
653
- console.error("[xmlToJSON] XML parsing error:", parseErrors[0].textContent);
654
- return null;
655
- }
656
- const children = childNodesOf(sourceDoc);
657
- const firstChild = children[0] || (sourceDoc.documentElement ? childNodesOf(sourceDoc.documentElement)[0] : null);
658
- return firstChild ? translateRecursively(firstChild) : { children: [], tagName: "", attributes: {}, path: "" };
659
- } catch (e) {
660
- console.error("[xmlToJSON] Failed to parse XML:", e);
661
- return null;
662
- }
663
- }
664
- function parseAndroidBounds(bounds) {
665
- const match = bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
666
- if (!match) {
667
- return { x: 0, y: 0, width: 0, height: 0 };
668
- }
669
- const x1 = parseInt(match[1], 10);
670
- const y1 = parseInt(match[2], 10);
671
- const x2 = parseInt(match[3], 10);
672
- const y2 = parseInt(match[4], 10);
673
- return {
674
- x: x1,
675
- y: y1,
676
- width: x2 - x1,
677
- height: y2 - y1
678
- };
679
- }
680
- function parseIOSBounds(attributes) {
681
- return {
682
- x: parseInt(attributes.x || "0", 10),
683
- y: parseInt(attributes.y || "0", 10),
684
- width: parseInt(attributes.width || "0", 10),
685
- height: parseInt(attributes.height || "0", 10)
686
- };
687
- }
688
- function countAttributeOccurrences(sourceXML, attribute, value) {
689
- const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
690
- const pattern = new RegExp(`${attribute}=["']${escapedValue}["']`, "g");
691
- const matches = sourceXML.match(pattern);
692
- return matches ? matches.length : 0;
693
- }
694
- function isAttributeUnique(sourceXML, attribute, value) {
695
- return countAttributeOccurrences(sourceXML, attribute, value) === 1;
637
+ async function getBrowserInteractableElements(browser, options = {}) {
638
+ const { elementType = "interactable" } = options;
639
+ return browser.execute(elementsScript, elementType);
696
640
  }
697
641
 
698
- // src/locators/element-filter.ts
642
+ // src/locators/constants.ts
699
643
  var ANDROID_INTERACTABLE_TAGS = [
700
644
  // Input elements
701
645
  "android.widget.EditText",
@@ -730,43 +674,6 @@ var ANDROID_INTERACTABLE_TAGS = [
730
674
  // List/grid items
731
675
  "android.widget.AdapterView"
732
676
  ];
733
- var IOS_INTERACTABLE_TAGS = [
734
- // Input elements
735
- "XCUIElementTypeTextField",
736
- "XCUIElementTypeSecureTextField",
737
- "XCUIElementTypeTextView",
738
- "XCUIElementTypeSearchField",
739
- // Button-like elements
740
- "XCUIElementTypeButton",
741
- "XCUIElementTypeLink",
742
- // Text elements (often tappable)
743
- "XCUIElementTypeStaticText",
744
- // Image elements
745
- "XCUIElementTypeImage",
746
- "XCUIElementTypeIcon",
747
- // Selection elements
748
- "XCUIElementTypeSwitch",
749
- "XCUIElementTypeSlider",
750
- "XCUIElementTypeStepper",
751
- "XCUIElementTypeSegmentedControl",
752
- "XCUIElementTypePicker",
753
- "XCUIElementTypePickerWheel",
754
- "XCUIElementTypeDatePicker",
755
- "XCUIElementTypePageIndicator",
756
- // Table/list items
757
- "XCUIElementTypeCell",
758
- "XCUIElementTypeMenuItem",
759
- "XCUIElementTypeMenuBarItem",
760
- // Toggle elements
761
- "XCUIElementTypeCheckBox",
762
- "XCUIElementTypeRadioButton",
763
- "XCUIElementTypeToggle",
764
- // Other interactive
765
- "XCUIElementTypeKey",
766
- "XCUIElementTypeKeyboard",
767
- "XCUIElementTypeAlert",
768
- "XCUIElementTypeSheet"
769
- ];
770
677
  var ANDROID_LAYOUT_CONTAINERS = [
771
678
  // Core ViewGroup classes
772
679
  "android.view.ViewGroup",
@@ -805,6 +712,43 @@ var ANDROID_LAYOUT_CONTAINERS = [
805
712
  "com.android.internal.policy.DecorView",
806
713
  "android.widget.DecorView"
807
714
  ];
715
+ var IOS_INTERACTABLE_TAGS = [
716
+ // Input elements
717
+ "XCUIElementTypeTextField",
718
+ "XCUIElementTypeSecureTextField",
719
+ "XCUIElementTypeTextView",
720
+ "XCUIElementTypeSearchField",
721
+ // Button-like elements
722
+ "XCUIElementTypeButton",
723
+ "XCUIElementTypeLink",
724
+ // Text elements (often tappable)
725
+ "XCUIElementTypeStaticText",
726
+ // Image elements
727
+ "XCUIElementTypeImage",
728
+ "XCUIElementTypeIcon",
729
+ // Selection elements
730
+ "XCUIElementTypeSwitch",
731
+ "XCUIElementTypeSlider",
732
+ "XCUIElementTypeStepper",
733
+ "XCUIElementTypeSegmentedControl",
734
+ "XCUIElementTypePicker",
735
+ "XCUIElementTypePickerWheel",
736
+ "XCUIElementTypeDatePicker",
737
+ "XCUIElementTypePageIndicator",
738
+ // Table/list items
739
+ "XCUIElementTypeCell",
740
+ "XCUIElementTypeMenuItem",
741
+ "XCUIElementTypeMenuBarItem",
742
+ // Toggle elements
743
+ "XCUIElementTypeCheckBox",
744
+ "XCUIElementTypeRadioButton",
745
+ "XCUIElementTypeToggle",
746
+ // Other interactive
747
+ "XCUIElementTypeKey",
748
+ "XCUIElementTypeKeyboard",
749
+ "XCUIElementTypeAlert",
750
+ "XCUIElementTypeSheet"
751
+ ];
808
752
  var IOS_LAYOUT_CONTAINERS = [
809
753
  // Generic containers
810
754
  "XCUIElementTypeOther",
@@ -837,6 +781,188 @@ var IOS_LAYOUT_CONTAINERS = [
837
781
  // Application root
838
782
  "XCUIElementTypeApplication"
839
783
  ];
784
+
785
+ // src/locators/xml-parsing.ts
786
+ import { DOMParser } from "@xmldom/xmldom";
787
+ import xpath from "xpath";
788
+ function childNodesOf(node) {
789
+ const children = [];
790
+ if (node.childNodes) {
791
+ for (let i = 0; i < node.childNodes.length; i++) {
792
+ const child = node.childNodes.item(i);
793
+ if (child?.nodeType === 1) {
794
+ children.push(child);
795
+ }
796
+ }
797
+ }
798
+ return children;
799
+ }
800
+ function translateRecursively(domNode, parentPath = "", index = null) {
801
+ const attributes = {};
802
+ const element = domNode;
803
+ if (element.attributes) {
804
+ for (let attrIdx = 0; attrIdx < element.attributes.length; attrIdx++) {
805
+ const attr = element.attributes.item(attrIdx);
806
+ if (attr) {
807
+ attributes[attr.name] = attr.value.replace(/(\n)/gm, "\\n");
808
+ }
809
+ }
810
+ }
811
+ const path = index === null ? "" : `${parentPath ? parentPath + "." : ""}${index}`;
812
+ return {
813
+ children: childNodesOf(domNode).map(
814
+ (childNode, childIndex) => translateRecursively(childNode, path, childIndex)
815
+ ),
816
+ tagName: domNode.nodeName,
817
+ attributes,
818
+ path
819
+ };
820
+ }
821
+ function isSameElement(node1, node2) {
822
+ if (node1.nodeType !== 1 || node2.nodeType !== 1) return false;
823
+ const el1 = node1;
824
+ const el2 = node2;
825
+ if (el1.nodeName !== el2.nodeName) return false;
826
+ const bounds1 = el1.getAttribute("bounds");
827
+ const bounds2 = el2.getAttribute("bounds");
828
+ if (bounds1 && bounds2) {
829
+ return bounds1 === bounds2;
830
+ }
831
+ const x1 = el1.getAttribute("x");
832
+ const y1 = el1.getAttribute("y");
833
+ const x2 = el2.getAttribute("x");
834
+ const y2 = el2.getAttribute("y");
835
+ if (x1 && y1 && x2 && y2) {
836
+ return x1 === x2 && y1 === y2 && el1.getAttribute("width") === el2.getAttribute("width") && el1.getAttribute("height") === el2.getAttribute("height");
837
+ }
838
+ return false;
839
+ }
840
+ function xmlToJSON(sourceXML) {
841
+ try {
842
+ const parser = new DOMParser();
843
+ const sourceDoc = parser.parseFromString(sourceXML, "text/xml");
844
+ const parseErrors = sourceDoc.getElementsByTagName("parsererror");
845
+ if (parseErrors.length > 0) {
846
+ console.error("[xmlToJSON] XML parsing error:", parseErrors[0].textContent);
847
+ return null;
848
+ }
849
+ const children = childNodesOf(sourceDoc);
850
+ const firstChild = children[0] || (sourceDoc.documentElement ? childNodesOf(sourceDoc.documentElement)[0] : null);
851
+ return firstChild ? translateRecursively(firstChild) : { children: [], tagName: "", attributes: {}, path: "" };
852
+ } catch (e) {
853
+ console.error("[xmlToJSON] Failed to parse XML:", e);
854
+ return null;
855
+ }
856
+ }
857
+ function xmlToDOM(sourceXML) {
858
+ try {
859
+ const parser = new DOMParser();
860
+ const doc = parser.parseFromString(sourceXML, "text/xml");
861
+ const parseErrors = doc.getElementsByTagName("parsererror");
862
+ if (parseErrors.length > 0) {
863
+ console.error("[xmlToDOM] XML parsing error:", parseErrors[0].textContent);
864
+ return null;
865
+ }
866
+ return doc;
867
+ } catch (e) {
868
+ console.error("[xmlToDOM] Failed to parse XML:", e);
869
+ return null;
870
+ }
871
+ }
872
+ function evaluateXPath(doc, xpathExpr) {
873
+ try {
874
+ const nodes = xpath.select(xpathExpr, doc);
875
+ if (Array.isArray(nodes)) {
876
+ return nodes;
877
+ }
878
+ return [];
879
+ } catch (e) {
880
+ console.error(`[evaluateXPath] Failed to evaluate "${xpathExpr}":`, e);
881
+ return [];
882
+ }
883
+ }
884
+ function checkXPathUniqueness(doc, xpathExpr, targetNode) {
885
+ try {
886
+ const nodes = evaluateXPath(doc, xpathExpr);
887
+ const totalMatches = nodes.length;
888
+ if (totalMatches === 0) {
889
+ return { isUnique: false };
890
+ }
891
+ if (totalMatches === 1) {
892
+ return { isUnique: true };
893
+ }
894
+ if (targetNode) {
895
+ for (let i = 0; i < nodes.length; i++) {
896
+ if (nodes[i].isSameNode(targetNode) || isSameElement(nodes[i], targetNode)) {
897
+ return {
898
+ isUnique: false,
899
+ index: i + 1,
900
+ // 1-based index for XPath
901
+ totalMatches
902
+ };
903
+ }
904
+ }
905
+ }
906
+ return { isUnique: false, totalMatches };
907
+ } catch (e) {
908
+ console.error(`[checkXPathUniqueness] Error checking "${xpathExpr}":`, e);
909
+ return { isUnique: false };
910
+ }
911
+ }
912
+ function findDOMNodeByPath(doc, path) {
913
+ if (!path) return doc.documentElement;
914
+ const indices = path.split(".").map(Number);
915
+ let current = doc.documentElement;
916
+ for (const index of indices) {
917
+ if (!current) return null;
918
+ const children = [];
919
+ if (current.childNodes) {
920
+ for (let i = 0; i < current.childNodes.length; i++) {
921
+ const child = current.childNodes.item(i);
922
+ if (child?.nodeType === 1) {
923
+ children.push(child);
924
+ }
925
+ }
926
+ }
927
+ current = children[index] || null;
928
+ }
929
+ return current;
930
+ }
931
+ function parseAndroidBounds(bounds) {
932
+ const match = bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
933
+ if (!match) {
934
+ return { x: 0, y: 0, width: 0, height: 0 };
935
+ }
936
+ const x1 = parseInt(match[1], 10);
937
+ const y1 = parseInt(match[2], 10);
938
+ const x2 = parseInt(match[3], 10);
939
+ const y2 = parseInt(match[4], 10);
940
+ return {
941
+ x: x1,
942
+ y: y1,
943
+ width: x2 - x1,
944
+ height: y2 - y1
945
+ };
946
+ }
947
+ function parseIOSBounds(attributes) {
948
+ return {
949
+ x: parseInt(attributes.x || "0", 10),
950
+ y: parseInt(attributes.y || "0", 10),
951
+ width: parseInt(attributes.width || "0", 10),
952
+ height: parseInt(attributes.height || "0", 10)
953
+ };
954
+ }
955
+ function countAttributeOccurrences(sourceXML, attribute, value) {
956
+ const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
957
+ const pattern = new RegExp(`${attribute}=["']${escapedValue}["']`, "g");
958
+ const matches = sourceXML.match(pattern);
959
+ return matches ? matches.length : 0;
960
+ }
961
+ function isAttributeUnique(sourceXML, attribute, value) {
962
+ return countAttributeOccurrences(sourceXML, attribute, value) === 1;
963
+ }
964
+
965
+ // src/locators/element-filter.ts
840
966
  function matchesTagList(tagName, tagList) {
841
967
  if (tagList.includes(tagName)) {
842
968
  return true;
@@ -917,7 +1043,6 @@ function shouldIncludeElement(element, filters, isNative, automationName) {
917
1043
  const {
918
1044
  includeTagNames = [],
919
1045
  excludeTagNames = ["hierarchy"],
920
- // Always exclude root hierarchy node
921
1046
  requireAttributes = [],
922
1047
  minAttributeCount = 0,
923
1048
  fetchableOnly = false,
@@ -964,38 +1089,144 @@ function isValidValue(value) {
964
1089
  function escapeText(text) {
965
1090
  return text.replace(/"/g, '\\"').replace(/\n/g, "\\n");
966
1091
  }
967
- function getSimpleSuggestedLocators(element, sourceXML, isNative, automationName) {
968
- const results = [];
969
- const isAndroid = automationName.toLowerCase().includes("uiautomator");
970
- const attrs = element.attributes;
971
- if (isAndroid) {
972
- const resourceId = attrs["resource-id"];
973
- if (isValidValue(resourceId) && isAttributeUnique(sourceXML, "resource-id", resourceId)) {
974
- results.push(["id", `android=new UiSelector().resourceId("${resourceId}")`]);
1092
+ function escapeXPathValue(value) {
1093
+ if (!value.includes("'")) {
1094
+ return `'${value}'`;
1095
+ }
1096
+ if (!value.includes('"')) {
1097
+ return `"${value}"`;
1098
+ }
1099
+ const parts = [];
1100
+ let current = "";
1101
+ for (const char of value) {
1102
+ if (char === "'") {
1103
+ if (current) parts.push(`'${current}'`);
1104
+ parts.push(`"'"`);
1105
+ current = "";
1106
+ } else {
1107
+ current += char;
975
1108
  }
976
- const contentDesc = attrs["content-desc"];
977
- if (isValidValue(contentDesc) && isAttributeUnique(sourceXML, "content-desc", contentDesc)) {
978
- results.push(["accessibility-id", `~${contentDesc}`]);
1109
+ }
1110
+ if (current) parts.push(`'${current}'`);
1111
+ return `concat(${parts.join(",")})`;
1112
+ }
1113
+ function generateIndexedXPath(baseXPath, index) {
1114
+ return `(${baseXPath})[${index}]`;
1115
+ }
1116
+ function generateIndexedUiAutomator(baseSelector, index) {
1117
+ return `${baseSelector}.instance(${index - 1})`;
1118
+ }
1119
+ function checkUniqueness(ctx, xpath2, targetNode) {
1120
+ if (ctx.parsedDOM) {
1121
+ return checkXPathUniqueness(ctx.parsedDOM, xpath2, targetNode);
1122
+ }
1123
+ const match = xpath2.match(/\/\/\*\[@([^=]+)="([^"]+)"\]/);
1124
+ if (match) {
1125
+ const [, attr, value] = match;
1126
+ return { isUnique: isAttributeUnique(ctx.sourceXML, attr, value) };
1127
+ }
1128
+ return { isUnique: false };
1129
+ }
1130
+ function getSiblingIndex(element) {
1131
+ const parent = element.parentNode;
1132
+ if (!parent) return 1;
1133
+ const tagName = element.nodeName;
1134
+ let index = 0;
1135
+ for (let i = 0; i < parent.childNodes.length; i++) {
1136
+ const child = parent.childNodes.item(i);
1137
+ if (child?.nodeType === 1 && child.nodeName === tagName) {
1138
+ index++;
1139
+ if (child === element) return index;
1140
+ }
1141
+ }
1142
+ return 1;
1143
+ }
1144
+ function countSiblings(element) {
1145
+ const parent = element.parentNode;
1146
+ if (!parent) return 1;
1147
+ const tagName = element.nodeName;
1148
+ let count = 0;
1149
+ for (let i = 0; i < parent.childNodes.length; i++) {
1150
+ const child = parent.childNodes.item(i);
1151
+ if (child?.nodeType === 1 && child.nodeName === tagName) {
1152
+ count++;
1153
+ }
1154
+ }
1155
+ return count;
1156
+ }
1157
+ function findUniqueAttribute(element, ctx) {
1158
+ const attrs = ctx.isAndroid ? ["resource-id", "content-desc", "text"] : ["name", "label", "value"];
1159
+ for (const attr of attrs) {
1160
+ const value = element.getAttribute(attr);
1161
+ if (value && value.trim()) {
1162
+ const xpath2 = `//*[@${attr}=${escapeXPathValue(value)}]`;
1163
+ const result = ctx.parsedDOM ? checkXPathUniqueness(ctx.parsedDOM, xpath2) : { isUnique: isAttributeUnique(ctx.sourceXML, attr, value) };
1164
+ if (result.isUnique) {
1165
+ return `@${attr}=${escapeXPathValue(value)}`;
1166
+ }
979
1167
  }
980
- const text = attrs.text;
981
- if (isValidValue(text) && text.length < 100 && isAttributeUnique(sourceXML, "text", text)) {
982
- results.push(["text", `android=new UiSelector().text("${escapeText(text)}")`]);
1168
+ }
1169
+ return null;
1170
+ }
1171
+ function buildHierarchicalXPath(ctx, element, maxDepth = 3) {
1172
+ if (!ctx.parsedDOM) return null;
1173
+ const pathParts = [];
1174
+ let current = element;
1175
+ let depth = 0;
1176
+ while (current && depth < maxDepth) {
1177
+ const tagName = current.nodeName;
1178
+ const uniqueAttr = findUniqueAttribute(current, ctx);
1179
+ if (uniqueAttr) {
1180
+ pathParts.unshift(`//${tagName}[${uniqueAttr}]`);
1181
+ break;
1182
+ } else {
1183
+ const siblingIndex = getSiblingIndex(current);
1184
+ const siblingCount = countSiblings(current);
1185
+ if (siblingCount > 1) {
1186
+ pathParts.unshift(`${tagName}[${siblingIndex}]`);
1187
+ } else {
1188
+ pathParts.unshift(tagName);
1189
+ }
983
1190
  }
1191
+ const parent = current.parentNode;
1192
+ current = parent && parent.nodeType === 1 ? parent : null;
1193
+ depth++;
1194
+ }
1195
+ if (pathParts.length === 0) return null;
1196
+ let result = pathParts[0];
1197
+ for (let i = 1; i < pathParts.length; i++) {
1198
+ result += "/" + pathParts[i];
1199
+ }
1200
+ if (!result.startsWith("//")) {
1201
+ result = "//" + result;
1202
+ }
1203
+ return result;
1204
+ }
1205
+ function addXPathLocator(results, xpath2, ctx, targetNode) {
1206
+ const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
1207
+ if (uniqueness.isUnique) {
1208
+ results.push(["xpath", xpath2]);
1209
+ } else if (uniqueness.index) {
1210
+ results.push(["xpath", generateIndexedXPath(xpath2, uniqueness.index)]);
984
1211
  } else {
985
- const name = attrs.name;
986
- if (isValidValue(name) && isAttributeUnique(sourceXML, "name", name)) {
987
- results.push(["accessibility-id", `~${name}`]);
988
- }
989
- const label = attrs.label;
990
- if (isValidValue(label) && label !== name && isAttributeUnique(sourceXML, "label", label)) {
991
- results.push(["predicate-string", `-ios predicate string:label == "${escapeText(label)}"`]);
992
- }
993
- const value = attrs.value;
994
- if (isValidValue(value) && isAttributeUnique(sourceXML, "value", value)) {
995
- results.push(["predicate-string", `-ios predicate string:value == "${escapeText(value)}"`]);
1212
+ if (targetNode && ctx.parsedDOM) {
1213
+ const hierarchical = buildHierarchicalXPath(ctx, targetNode);
1214
+ if (hierarchical) {
1215
+ results.push(["xpath", hierarchical]);
1216
+ }
996
1217
  }
1218
+ results.push(["xpath", xpath2]);
997
1219
  }
998
- return results;
1220
+ }
1221
+ function isInUiAutomatorScope(element, doc) {
1222
+ if (!doc) return true;
1223
+ const hierarchyNodes = evaluateXPath(doc, "/hierarchy/*");
1224
+ if (hierarchyNodes.length === 0) return true;
1225
+ const lastIndex = hierarchyNodes.length;
1226
+ const pathParts = element.path.split(".");
1227
+ if (pathParts.length === 0 || pathParts[0] === "") return true;
1228
+ const firstIndex = parseInt(pathParts[0], 10);
1229
+ return firstIndex === lastIndex - 1;
999
1230
  }
1000
1231
  function buildUiAutomatorSelector(element) {
1001
1232
  const attrs = element.attributes;
@@ -1078,19 +1309,86 @@ function buildXPath(element, sourceXML, isAndroid) {
1078
1309
  }
1079
1310
  return `//${tagName}[${conditions.join(" and ")}]`;
1080
1311
  }
1081
- function getComplexSuggestedLocators(element, sourceXML, isNative, automationName) {
1312
+ function getSimpleSuggestedLocators(element, ctx, automationName, targetNode) {
1313
+ const results = [];
1314
+ const isAndroid = automationName.toLowerCase().includes("uiautomator");
1315
+ const attrs = element.attributes;
1316
+ const inUiAutomatorScope = isAndroid ? isInUiAutomatorScope(element, ctx.parsedDOM) : true;
1317
+ if (isAndroid) {
1318
+ const resourceId = attrs["resource-id"];
1319
+ if (isValidValue(resourceId)) {
1320
+ const xpath2 = `//*[@resource-id="${resourceId}"]`;
1321
+ const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
1322
+ if (uniqueness.isUnique && inUiAutomatorScope) {
1323
+ results.push(["id", `android=new UiSelector().resourceId("${resourceId}")`]);
1324
+ } else if (uniqueness.index && inUiAutomatorScope) {
1325
+ const base = `android=new UiSelector().resourceId("${resourceId}")`;
1326
+ results.push(["id", generateIndexedUiAutomator(base, uniqueness.index)]);
1327
+ }
1328
+ }
1329
+ const contentDesc = attrs["content-desc"];
1330
+ if (isValidValue(contentDesc)) {
1331
+ const xpath2 = `//*[@content-desc="${contentDesc}"]`;
1332
+ const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
1333
+ if (uniqueness.isUnique) {
1334
+ results.push(["accessibility-id", `~${contentDesc}`]);
1335
+ }
1336
+ }
1337
+ const text = attrs.text;
1338
+ if (isValidValue(text) && text.length < 100) {
1339
+ const xpath2 = `//*[@text="${escapeText(text)}"]`;
1340
+ const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
1341
+ if (uniqueness.isUnique && inUiAutomatorScope) {
1342
+ results.push(["text", `android=new UiSelector().text("${escapeText(text)}")`]);
1343
+ } else if (uniqueness.index && inUiAutomatorScope) {
1344
+ const base = `android=new UiSelector().text("${escapeText(text)}")`;
1345
+ results.push(["text", generateIndexedUiAutomator(base, uniqueness.index)]);
1346
+ }
1347
+ }
1348
+ } else {
1349
+ const name = attrs.name;
1350
+ if (isValidValue(name)) {
1351
+ const xpath2 = `//*[@name="${name}"]`;
1352
+ const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
1353
+ if (uniqueness.isUnique) {
1354
+ results.push(["accessibility-id", `~${name}`]);
1355
+ }
1356
+ }
1357
+ const label = attrs.label;
1358
+ if (isValidValue(label) && label !== attrs.name) {
1359
+ const xpath2 = `//*[@label="${escapeText(label)}"]`;
1360
+ const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
1361
+ if (uniqueness.isUnique) {
1362
+ results.push(["predicate-string", `-ios predicate string:label == "${escapeText(label)}"`]);
1363
+ }
1364
+ }
1365
+ const value = attrs.value;
1366
+ if (isValidValue(value)) {
1367
+ const xpath2 = `//*[@value="${escapeText(value)}"]`;
1368
+ const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
1369
+ if (uniqueness.isUnique) {
1370
+ results.push(["predicate-string", `-ios predicate string:value == "${escapeText(value)}"`]);
1371
+ }
1372
+ }
1373
+ }
1374
+ return results;
1375
+ }
1376
+ function getComplexSuggestedLocators(element, ctx, automationName, targetNode) {
1082
1377
  const results = [];
1083
1378
  const isAndroid = automationName.toLowerCase().includes("uiautomator");
1379
+ const inUiAutomatorScope = isAndroid ? isInUiAutomatorScope(element, ctx.parsedDOM) : true;
1084
1380
  if (isAndroid) {
1085
- const uiAutomator = buildUiAutomatorSelector(element);
1086
- if (uiAutomator) {
1087
- results.push(["uiautomator", uiAutomator]);
1381
+ if (inUiAutomatorScope) {
1382
+ const uiAutomator = buildUiAutomatorSelector(element);
1383
+ if (uiAutomator) {
1384
+ results.push(["uiautomator", uiAutomator]);
1385
+ }
1088
1386
  }
1089
- const xpath = buildXPath(element, sourceXML, true);
1090
- if (xpath) {
1091
- results.push(["xpath", xpath]);
1387
+ const xpath2 = buildXPath(element, ctx.sourceXML, true);
1388
+ if (xpath2) {
1389
+ addXPathLocator(results, xpath2, ctx, targetNode);
1092
1390
  }
1093
- if (isValidValue(element.attributes.class)) {
1391
+ if (inUiAutomatorScope && isValidValue(element.attributes.class)) {
1094
1392
  results.push([
1095
1393
  "class-name",
1096
1394
  `android=new UiSelector().className("${element.attributes.class}")`
@@ -1105,9 +1403,9 @@ function getComplexSuggestedLocators(element, sourceXML, isNative, automationNam
1105
1403
  if (classChain) {
1106
1404
  results.push(["class-chain", classChain]);
1107
1405
  }
1108
- const xpath = buildXPath(element, sourceXML, false);
1109
- if (xpath) {
1110
- results.push(["xpath", xpath]);
1406
+ const xpath2 = buildXPath(element, ctx.sourceXML, false);
1407
+ if (xpath2) {
1408
+ addXPathLocator(results, xpath2, ctx, targetNode);
1111
1409
  }
1112
1410
  const type = element.tagName;
1113
1411
  if (type.startsWith("XCUIElementType")) {
@@ -1116,9 +1414,14 @@ function getComplexSuggestedLocators(element, sourceXML, isNative, automationNam
1116
1414
  }
1117
1415
  return results;
1118
1416
  }
1119
- function getSuggestedLocators(element, sourceXML, isNative, automationName) {
1120
- const simpleLocators = getSimpleSuggestedLocators(element, sourceXML, isNative, automationName);
1121
- const complexLocators = getComplexSuggestedLocators(element, sourceXML, isNative, automationName);
1417
+ function getSuggestedLocators(element, sourceXML, automationName, ctx, targetNode) {
1418
+ const locatorCtx = ctx ?? {
1419
+ sourceXML,
1420
+ parsedDOM: null,
1421
+ isAndroid: automationName.toLowerCase().includes("uiautomator")
1422
+ };
1423
+ const simpleLocators = getSimpleSuggestedLocators(element, locatorCtx, automationName, targetNode);
1424
+ const complexLocators = getComplexSuggestedLocators(element, locatorCtx, automationName, targetNode);
1122
1425
  const seen = /* @__PURE__ */ new Set();
1123
1426
  const results = [];
1124
1427
  for (const locator of [...simpleLocators, ...complexLocators]) {
@@ -1139,7 +1442,7 @@ function locatorsToObject(locators) {
1139
1442
  return result;
1140
1443
  }
1141
1444
 
1142
- // src/locators/generate-all-locators.ts
1445
+ // src/locators/index.ts
1143
1446
  function parseBounds(element, platform) {
1144
1447
  return platform === "android" ? parseAndroidBounds(element.attributes.bounds || "") : parseIOSBounds(element.attributes);
1145
1448
  }
@@ -1175,7 +1478,14 @@ function shouldProcess(element, ctx) {
1175
1478
  function processElement(element, ctx) {
1176
1479
  if (!shouldProcess(element, ctx)) return;
1177
1480
  try {
1178
- const locators = getSuggestedLocators(element, ctx.sourceXML, ctx.isNative, ctx.automationName);
1481
+ const targetNode = ctx.parsedDOM ? findDOMNodeByPath(ctx.parsedDOM, element.path) : void 0;
1482
+ const locators = getSuggestedLocators(
1483
+ element,
1484
+ ctx.sourceXML,
1485
+ ctx.automationName,
1486
+ { sourceXML: ctx.sourceXML, parsedDOM: ctx.parsedDOM, isAndroid: ctx.platform === "android" },
1487
+ targetNode || void 0
1488
+ );
1179
1489
  if (locators.length === 0) return;
1180
1490
  const transformed = transformElement(element, locators, ctx);
1181
1491
  if (Object.keys(transformed.locators).length === 0) return;
@@ -1197,6 +1507,7 @@ function generateAllElementLocators(sourceXML, options) {
1197
1507
  console.error("[generateAllElementLocators] Failed to parse page source XML");
1198
1508
  return [];
1199
1509
  }
1510
+ const parsedDOM = xmlToDOM(sourceXML);
1200
1511
  const ctx = {
1201
1512
  sourceXML,
1202
1513
  platform: options.platform,
@@ -1204,7 +1515,8 @@ function generateAllElementLocators(sourceXML, options) {
1204
1515
  isNative: options.isNative ?? true,
1205
1516
  viewportSize: options.viewportSize ?? { width: 9999, height: 9999 },
1206
1517
  filters: options.filters ?? {},
1207
- results: []
1518
+ results: [],
1519
+ parsedDOM
1208
1520
  };
1209
1521
  traverseTree(sourceJSON, ctx);
1210
1522
  return ctx.results;
@@ -1246,27 +1558,18 @@ function selectBestLocators(locators) {
1246
1558
  }
1247
1559
  function toMobileElementInfo(element, includeBounds) {
1248
1560
  const selectedLocators = selectBestLocators(element.locators);
1561
+ const accessId = element.accessibilityId || element.contentDesc;
1249
1562
  const info = {
1250
1563
  selector: selectedLocators[0] || "",
1251
1564
  tagName: element.tagName,
1252
- isInViewport: element.isInViewport
1565
+ isInViewport: element.isInViewport,
1566
+ text: element.text || "",
1567
+ resourceId: element.resourceId || "",
1568
+ accessibilityId: accessId || "",
1569
+ isEnabled: element.enabled !== false,
1570
+ altSelector: selectedLocators[1] || ""
1571
+ // Single alternative (flattened for tabular)
1253
1572
  };
1254
- if (element.text) {
1255
- info.text = element.text;
1256
- }
1257
- if (element.resourceId) {
1258
- info.resourceId = element.resourceId;
1259
- }
1260
- const accessId = element.accessibilityId || element.contentDesc;
1261
- if (accessId) {
1262
- info.accessibilityId = accessId;
1263
- }
1264
- if (!element.enabled) {
1265
- info.isEnabled = false;
1266
- }
1267
- if (selectedLocators.length > 1) {
1268
- info.alternativeSelectors = selectedLocators.slice(1);
1269
- }
1270
1573
  if (includeBounds) {
1271
1574
  info.bounds = element.bounds;
1272
1575
  }
@@ -1299,18 +1602,6 @@ async function getMobileVisibleElements(browser, platform, options = {}) {
1299
1602
  // src/tools/get-visible-elements.tool.ts
1300
1603
  import { encode } from "@toon-format/toon";
1301
1604
  import { z as z7 } from "zod";
1302
-
1303
- // src/utils/strip-undefined.ts
1304
- function stripUndefined(obj) {
1305
- return Object.fromEntries(
1306
- Object.entries(obj).filter(([_, v]) => v !== void 0 && v !== null && v !== "")
1307
- );
1308
- }
1309
- function stripUndefinedFromArray(arr) {
1310
- return arr.map(stripUndefined);
1311
- }
1312
-
1313
- // src/tools/get-visible-elements.tool.ts
1314
1605
  var getVisibleElementsToolDefinition = {
1315
1606
  name: "get_visible_elements",
1316
1607
  description: 'get a list of visible (in viewport & displayed) interactable elements on the page (buttons, links, inputs). Use elementType="visual" for images/SVGs. Must prefer this to take_screenshot for interactions',
@@ -1347,8 +1638,7 @@ var getVisibleElementsTool = async (args) => {
1347
1638
  const platform = browser.isAndroid ? "android" : "ios";
1348
1639
  elements = await getMobileVisibleElements(browser, platform, { includeContainers, includeBounds });
1349
1640
  } else {
1350
- const raw = await browser.execute(get_interactable_browser_elements_default, elementType);
1351
- elements = stripUndefinedFromArray(raw);
1641
+ elements = await getBrowserInteractableElements(browser, { elementType });
1352
1642
  }
1353
1643
  if (inViewportOnly) {
1354
1644
  elements = elements.filter((el) => el.isInViewport !== false);
@@ -1366,8 +1656,9 @@ var getVisibleElementsTool = async (args) => {
1366
1656
  hasMore: offset + elements.length < total,
1367
1657
  elements
1368
1658
  };
1659
+ const toon = encode(result).replace(/,""/g, ",").replace(/"",/g, ",");
1369
1660
  return {
1370
- content: [{ type: "text", text: encode(result) }]
1661
+ content: [{ type: "text", text: toon }]
1371
1662
  };
1372
1663
  } catch (e) {
1373
1664
  return {
@@ -1538,6 +1829,62 @@ var deleteCookiesTool = async ({ name }) => {
1538
1829
  }
1539
1830
  };
1540
1831
 
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
+
1541
1888
  // src/tools/get-accessibility-tree.tool.ts
1542
1889
  import { encode as encode2 } from "@toon-format/toon";
1543
1890
  import { z as z10 } from "zod";
@@ -1551,44 +1898,6 @@ var getAccessibilityToolDefinition = {
1551
1898
  namedOnly: z10.boolean().optional().describe("Only return nodes with a name/label. Default: true. Filters out anonymous containers.")
1552
1899
  }
1553
1900
  };
1554
- function flattenAccessibilityTree(node, result = []) {
1555
- if (!node) return result;
1556
- if (node.role !== "WebArea" || node.name) {
1557
- const entry = {};
1558
- if (node.role) entry.role = node.role;
1559
- if (node.name) entry.name = node.name;
1560
- if (node.value !== void 0 && node.value !== "") entry.value = node.value;
1561
- if (node.description) entry.description = node.description;
1562
- if (node.keyshortcuts) entry.keyshortcuts = node.keyshortcuts;
1563
- if (node.roledescription) entry.roledescription = node.roledescription;
1564
- if (node.valuetext) entry.valuetext = node.valuetext;
1565
- if (node.disabled) entry.disabled = node.disabled;
1566
- if (node.expanded !== void 0) entry.expanded = node.expanded;
1567
- if (node.focused) entry.focused = node.focused;
1568
- if (node.modal) entry.modal = node.modal;
1569
- if (node.multiline) entry.multiline = node.multiline;
1570
- if (node.multiselectable) entry.multiselectable = node.multiselectable;
1571
- if (node.readonly) entry.readonly = node.readonly;
1572
- if (node.required) entry.required = node.required;
1573
- if (node.selected) entry.selected = node.selected;
1574
- if (node.checked !== void 0) entry.checked = node.checked;
1575
- if (node.pressed !== void 0) entry.pressed = node.pressed;
1576
- if (node.level !== void 0) entry.level = node.level;
1577
- if (node.valuemin !== void 0) entry.valuemin = node.valuemin;
1578
- if (node.valuemax !== void 0) entry.valuemax = node.valuemax;
1579
- if (node.autocomplete) entry.autocomplete = node.autocomplete;
1580
- if (node.haspopup) entry.haspopup = node.haspopup;
1581
- if (node.invalid) entry.invalid = node.invalid;
1582
- if (node.orientation) entry.orientation = node.orientation;
1583
- result.push(entry);
1584
- }
1585
- if (node.children && Array.isArray(node.children)) {
1586
- for (const child of node.children) {
1587
- flattenAccessibilityTree(child, result);
1588
- }
1589
- }
1590
- return result;
1591
- }
1592
1901
  var getAccessibilityTreeTool = async (args) => {
1593
1902
  try {
1594
1903
  const browser = getBrowser();
@@ -1601,24 +1910,12 @@ var getAccessibilityTreeTool = async (args) => {
1601
1910
  };
1602
1911
  }
1603
1912
  const { limit = 100, offset = 0, roles, namedOnly = true } = args || {};
1604
- const puppeteer = await browser.getPuppeteer();
1605
- const pages = await puppeteer.pages();
1606
- if (pages.length === 0) {
1607
- return {
1608
- content: [{ type: "text", text: "No active pages found" }]
1609
- };
1610
- }
1611
- const page = pages[0];
1612
- const snapshot = await page.accessibility.snapshot({
1613
- interestingOnly: true
1614
- // Filter to only interesting/semantic nodes
1615
- });
1616
- if (!snapshot) {
1913
+ let nodes = await getBrowserAccessibilityTree(browser);
1914
+ if (nodes.length === 0) {
1617
1915
  return {
1618
1916
  content: [{ type: "text", text: "No accessibility tree available" }]
1619
1917
  };
1620
1918
  }
1621
- let nodes = flattenAccessibilityTree(snapshot);
1622
1919
  if (namedOnly) {
1623
1920
  nodes = nodes.filter((n) => n.name && n.name.trim() !== "");
1624
1921
  }
@@ -1639,8 +1936,9 @@ var getAccessibilityTreeTool = async (args) => {
1639
1936
  hasMore: offset + nodes.length < total,
1640
1937
  nodes
1641
1938
  };
1939
+ const toon = encode2(result).replace(/,""/g, ",").replace(/"",/g, ",");
1642
1940
  return {
1643
- content: [{ type: "text", text: encode2(result) }]
1941
+ content: [{ type: "text", text: toon }]
1644
1942
  };
1645
1943
  } catch (e) {
1646
1944
  return {
@@ -2049,11 +2347,21 @@ var package_default = {
2049
2347
  type: "git",
2050
2348
  url: "git://github.com/webdriverio/mcp.git"
2051
2349
  },
2052
- version: "1.6.1",
2350
+ version: "2.1.0",
2053
2351
  description: "MCP server with WebdriverIO for browser and mobile app automation (iOS/Android via Appium)",
2054
2352
  main: "./lib/server.js",
2055
2353
  module: "./lib/server.js",
2056
2354
  types: "./lib/server.d.ts",
2355
+ exports: {
2356
+ ".": {
2357
+ import: "./lib/server.js",
2358
+ types: "./lib/server.d.ts"
2359
+ },
2360
+ "./snapshot": {
2361
+ import: "./lib/snapshot.js",
2362
+ types: "./lib/snapshot.d.ts"
2363
+ }
2364
+ },
2057
2365
  bin: {
2058
2366
  "wdio-mcp": "lib/server.js"
2059
2367
  },
@@ -2077,6 +2385,7 @@ var package_default = {
2077
2385
  },
2078
2386
  dependencies: {
2079
2387
  "@modelcontextprotocol/sdk": "1.25",
2388
+ xpath: "^0.0.34",
2080
2389
  "@toon-format/toon": "^2.1.0",
2081
2390
  "@wdio/protocols": "^9.16.2",
2082
2391
  "@xmldom/xmldom": "^0.8.11",
@@ -2114,7 +2423,7 @@ var server = new McpServer({
2114
2423
  description: package_default.description,
2115
2424
  websiteUrl: "https://github.com/webdriverio/mcp"
2116
2425
  }, {
2117
- instructions: "MCP server for browser and mobile app automation using WebDriverIO. Supports Chrome browser control (headed/headless) and iOS/Android native app testing via Appium.",
2426
+ instructions: "MCP server for browser and mobile app automation using WebDriverIO. Supports Chrome, Firefox, Edge, and Safari browser control plus iOS/Android native app testing via Appium.",
2118
2427
  capabilities: {
2119
2428
  tools: {}
2120
2429
  }