@wdio/mcp 2.1.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
  };
@@ -592,95 +634,12 @@ var elementsScript = (elementType = "interactable") => (function() {
592
634
  }
593
635
  return getElements();
594
636
  })();
595
- var get_interactable_browser_elements_default = elementsScript;
596
-
597
- // src/locators/source-parsing.ts
598
- import { DOMParser } from "@xmldom/xmldom";
599
- function childNodesOf(node) {
600
- const children = [];
601
- if (node.childNodes) {
602
- for (let i = 0; i < node.childNodes.length; i++) {
603
- const child = node.childNodes.item(i);
604
- if (child?.nodeType === 1) {
605
- children.push(child);
606
- }
607
- }
608
- }
609
- return children;
610
- }
611
- function translateRecursively(domNode, parentPath = "", index = null) {
612
- const attributes = {};
613
- const element = domNode;
614
- if (element.attributes) {
615
- for (let attrIdx = 0; attrIdx < element.attributes.length; attrIdx++) {
616
- const attr = element.attributes.item(attrIdx);
617
- if (attr) {
618
- attributes[attr.name] = attr.value.replace(/(\n)/gm, "\\n");
619
- }
620
- }
621
- }
622
- const path = index === null ? "" : `${parentPath ? parentPath + "." : ""}${index}`;
623
- return {
624
- children: childNodesOf(domNode).map(
625
- (childNode, childIndex) => translateRecursively(childNode, path, childIndex)
626
- ),
627
- tagName: domNode.nodeName,
628
- attributes,
629
- path
630
- };
631
- }
632
- function xmlToJSON(sourceXML) {
633
- try {
634
- const parser = new DOMParser();
635
- const sourceDoc = parser.parseFromString(sourceXML, "text/xml");
636
- const parseErrors = sourceDoc.getElementsByTagName("parsererror");
637
- if (parseErrors.length > 0) {
638
- console.error("[xmlToJSON] XML parsing error:", parseErrors[0].textContent);
639
- return null;
640
- }
641
- const children = childNodesOf(sourceDoc);
642
- const firstChild = children[0] || (sourceDoc.documentElement ? childNodesOf(sourceDoc.documentElement)[0] : null);
643
- return firstChild ? translateRecursively(firstChild) : { children: [], tagName: "", attributes: {}, path: "" };
644
- } catch (e) {
645
- console.error("[xmlToJSON] Failed to parse XML:", e);
646
- return null;
647
- }
648
- }
649
- function parseAndroidBounds(bounds) {
650
- const match = bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
651
- if (!match) {
652
- return { x: 0, y: 0, width: 0, height: 0 };
653
- }
654
- const x1 = parseInt(match[1], 10);
655
- const y1 = parseInt(match[2], 10);
656
- const x2 = parseInt(match[3], 10);
657
- const y2 = parseInt(match[4], 10);
658
- return {
659
- x: x1,
660
- y: y1,
661
- width: x2 - x1,
662
- height: y2 - y1
663
- };
664
- }
665
- function parseIOSBounds(attributes) {
666
- return {
667
- x: parseInt(attributes.x || "0", 10),
668
- y: parseInt(attributes.y || "0", 10),
669
- width: parseInt(attributes.width || "0", 10),
670
- height: parseInt(attributes.height || "0", 10)
671
- };
672
- }
673
- function countAttributeOccurrences(sourceXML, attribute, value) {
674
- const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
675
- const pattern = new RegExp(`${attribute}=["']${escapedValue}["']`, "g");
676
- const matches = sourceXML.match(pattern);
677
- return matches ? matches.length : 0;
678
- }
679
- function isAttributeUnique(sourceXML, attribute, value) {
680
- return countAttributeOccurrences(sourceXML, attribute, value) === 1;
637
+ async function getBrowserInteractableElements(browser, options = {}) {
638
+ const { elementType = "interactable" } = options;
639
+ return browser.execute(elementsScript, elementType);
681
640
  }
682
641
 
683
- // src/locators/element-filter.ts
642
+ // src/locators/constants.ts
684
643
  var ANDROID_INTERACTABLE_TAGS = [
685
644
  // Input elements
686
645
  "android.widget.EditText",
@@ -715,43 +674,6 @@ var ANDROID_INTERACTABLE_TAGS = [
715
674
  // List/grid items
716
675
  "android.widget.AdapterView"
717
676
  ];
718
- var IOS_INTERACTABLE_TAGS = [
719
- // Input elements
720
- "XCUIElementTypeTextField",
721
- "XCUIElementTypeSecureTextField",
722
- "XCUIElementTypeTextView",
723
- "XCUIElementTypeSearchField",
724
- // Button-like elements
725
- "XCUIElementTypeButton",
726
- "XCUIElementTypeLink",
727
- // Text elements (often tappable)
728
- "XCUIElementTypeStaticText",
729
- // Image elements
730
- "XCUIElementTypeImage",
731
- "XCUIElementTypeIcon",
732
- // Selection elements
733
- "XCUIElementTypeSwitch",
734
- "XCUIElementTypeSlider",
735
- "XCUIElementTypeStepper",
736
- "XCUIElementTypeSegmentedControl",
737
- "XCUIElementTypePicker",
738
- "XCUIElementTypePickerWheel",
739
- "XCUIElementTypeDatePicker",
740
- "XCUIElementTypePageIndicator",
741
- // Table/list items
742
- "XCUIElementTypeCell",
743
- "XCUIElementTypeMenuItem",
744
- "XCUIElementTypeMenuBarItem",
745
- // Toggle elements
746
- "XCUIElementTypeCheckBox",
747
- "XCUIElementTypeRadioButton",
748
- "XCUIElementTypeToggle",
749
- // Other interactive
750
- "XCUIElementTypeKey",
751
- "XCUIElementTypeKeyboard",
752
- "XCUIElementTypeAlert",
753
- "XCUIElementTypeSheet"
754
- ];
755
677
  var ANDROID_LAYOUT_CONTAINERS = [
756
678
  // Core ViewGroup classes
757
679
  "android.view.ViewGroup",
@@ -790,6 +712,43 @@ var ANDROID_LAYOUT_CONTAINERS = [
790
712
  "com.android.internal.policy.DecorView",
791
713
  "android.widget.DecorView"
792
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
+ ];
793
752
  var IOS_LAYOUT_CONTAINERS = [
794
753
  // Generic containers
795
754
  "XCUIElementTypeOther",
@@ -822,6 +781,188 @@ var IOS_LAYOUT_CONTAINERS = [
822
781
  // Application root
823
782
  "XCUIElementTypeApplication"
824
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
825
966
  function matchesTagList(tagName, tagList) {
826
967
  if (tagList.includes(tagName)) {
827
968
  return true;
@@ -902,7 +1043,6 @@ function shouldIncludeElement(element, filters, isNative, automationName) {
902
1043
  const {
903
1044
  includeTagNames = [],
904
1045
  excludeTagNames = ["hierarchy"],
905
- // Always exclude root hierarchy node
906
1046
  requireAttributes = [],
907
1047
  minAttributeCount = 0,
908
1048
  fetchableOnly = false,
@@ -949,38 +1089,144 @@ function isValidValue(value) {
949
1089
  function escapeText(text) {
950
1090
  return text.replace(/"/g, '\\"').replace(/\n/g, "\\n");
951
1091
  }
952
- function getSimpleSuggestedLocators(element, sourceXML, isNative, automationName) {
953
- const results = [];
954
- const isAndroid = automationName.toLowerCase().includes("uiautomator");
955
- const attrs = element.attributes;
956
- if (isAndroid) {
957
- const resourceId = attrs["resource-id"];
958
- if (isValidValue(resourceId) && isAttributeUnique(sourceXML, "resource-id", resourceId)) {
959
- 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;
960
1108
  }
961
- const contentDesc = attrs["content-desc"];
962
- if (isValidValue(contentDesc) && isAttributeUnique(sourceXML, "content-desc", contentDesc)) {
963
- 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
+ }
964
1167
  }
965
- const text = attrs.text;
966
- if (isValidValue(text) && text.length < 100 && isAttributeUnique(sourceXML, "text", text)) {
967
- 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
+ }
968
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)]);
969
1211
  } else {
970
- const name = attrs.name;
971
- if (isValidValue(name) && isAttributeUnique(sourceXML, "name", name)) {
972
- results.push(["accessibility-id", `~${name}`]);
973
- }
974
- const label = attrs.label;
975
- if (isValidValue(label) && label !== name && isAttributeUnique(sourceXML, "label", label)) {
976
- results.push(["predicate-string", `-ios predicate string:label == "${escapeText(label)}"`]);
977
- }
978
- const value = attrs.value;
979
- if (isValidValue(value) && isAttributeUnique(sourceXML, "value", value)) {
980
- 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
+ }
981
1217
  }
1218
+ results.push(["xpath", xpath2]);
982
1219
  }
983
- 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;
984
1230
  }
985
1231
  function buildUiAutomatorSelector(element) {
986
1232
  const attrs = element.attributes;
@@ -1063,19 +1309,86 @@ function buildXPath(element, sourceXML, isAndroid) {
1063
1309
  }
1064
1310
  return `//${tagName}[${conditions.join(" and ")}]`;
1065
1311
  }
1066
- 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) {
1067
1377
  const results = [];
1068
1378
  const isAndroid = automationName.toLowerCase().includes("uiautomator");
1379
+ const inUiAutomatorScope = isAndroid ? isInUiAutomatorScope(element, ctx.parsedDOM) : true;
1069
1380
  if (isAndroid) {
1070
- const uiAutomator = buildUiAutomatorSelector(element);
1071
- if (uiAutomator) {
1072
- results.push(["uiautomator", uiAutomator]);
1381
+ if (inUiAutomatorScope) {
1382
+ const uiAutomator = buildUiAutomatorSelector(element);
1383
+ if (uiAutomator) {
1384
+ results.push(["uiautomator", uiAutomator]);
1385
+ }
1073
1386
  }
1074
- const xpath = buildXPath(element, sourceXML, true);
1075
- if (xpath) {
1076
- results.push(["xpath", xpath]);
1387
+ const xpath2 = buildXPath(element, ctx.sourceXML, true);
1388
+ if (xpath2) {
1389
+ addXPathLocator(results, xpath2, ctx, targetNode);
1077
1390
  }
1078
- if (isValidValue(element.attributes.class)) {
1391
+ if (inUiAutomatorScope && isValidValue(element.attributes.class)) {
1079
1392
  results.push([
1080
1393
  "class-name",
1081
1394
  `android=new UiSelector().className("${element.attributes.class}")`
@@ -1090,9 +1403,9 @@ function getComplexSuggestedLocators(element, sourceXML, isNative, automationNam
1090
1403
  if (classChain) {
1091
1404
  results.push(["class-chain", classChain]);
1092
1405
  }
1093
- const xpath = buildXPath(element, sourceXML, false);
1094
- if (xpath) {
1095
- results.push(["xpath", xpath]);
1406
+ const xpath2 = buildXPath(element, ctx.sourceXML, false);
1407
+ if (xpath2) {
1408
+ addXPathLocator(results, xpath2, ctx, targetNode);
1096
1409
  }
1097
1410
  const type = element.tagName;
1098
1411
  if (type.startsWith("XCUIElementType")) {
@@ -1101,9 +1414,14 @@ function getComplexSuggestedLocators(element, sourceXML, isNative, automationNam
1101
1414
  }
1102
1415
  return results;
1103
1416
  }
1104
- function getSuggestedLocators(element, sourceXML, isNative, automationName) {
1105
- const simpleLocators = getSimpleSuggestedLocators(element, sourceXML, isNative, automationName);
1106
- 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);
1107
1425
  const seen = /* @__PURE__ */ new Set();
1108
1426
  const results = [];
1109
1427
  for (const locator of [...simpleLocators, ...complexLocators]) {
@@ -1124,7 +1442,7 @@ function locatorsToObject(locators) {
1124
1442
  return result;
1125
1443
  }
1126
1444
 
1127
- // src/locators/generate-all-locators.ts
1445
+ // src/locators/index.ts
1128
1446
  function parseBounds(element, platform) {
1129
1447
  return platform === "android" ? parseAndroidBounds(element.attributes.bounds || "") : parseIOSBounds(element.attributes);
1130
1448
  }
@@ -1160,7 +1478,14 @@ function shouldProcess(element, ctx) {
1160
1478
  function processElement(element, ctx) {
1161
1479
  if (!shouldProcess(element, ctx)) return;
1162
1480
  try {
1163
- 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
+ );
1164
1489
  if (locators.length === 0) return;
1165
1490
  const transformed = transformElement(element, locators, ctx);
1166
1491
  if (Object.keys(transformed.locators).length === 0) return;
@@ -1182,6 +1507,7 @@ function generateAllElementLocators(sourceXML, options) {
1182
1507
  console.error("[generateAllElementLocators] Failed to parse page source XML");
1183
1508
  return [];
1184
1509
  }
1510
+ const parsedDOM = xmlToDOM(sourceXML);
1185
1511
  const ctx = {
1186
1512
  sourceXML,
1187
1513
  platform: options.platform,
@@ -1189,7 +1515,8 @@ function generateAllElementLocators(sourceXML, options) {
1189
1515
  isNative: options.isNative ?? true,
1190
1516
  viewportSize: options.viewportSize ?? { width: 9999, height: 9999 },
1191
1517
  filters: options.filters ?? {},
1192
- results: []
1518
+ results: [],
1519
+ parsedDOM
1193
1520
  };
1194
1521
  traverseTree(sourceJSON, ctx);
1195
1522
  return ctx.results;
@@ -1240,7 +1567,8 @@ function toMobileElementInfo(element, includeBounds) {
1240
1567
  resourceId: element.resourceId || "",
1241
1568
  accessibilityId: accessId || "",
1242
1569
  isEnabled: element.enabled !== false,
1243
- alternativeSelectors: selectedLocators.length > 1 ? selectedLocators.slice(1) : []
1570
+ altSelector: selectedLocators[1] || ""
1571
+ // Single alternative (flattened for tabular)
1244
1572
  };
1245
1573
  if (includeBounds) {
1246
1574
  info.bounds = element.bounds;
@@ -1310,7 +1638,7 @@ var getVisibleElementsTool = async (args) => {
1310
1638
  const platform = browser.isAndroid ? "android" : "ios";
1311
1639
  elements = await getMobileVisibleElements(browser, platform, { includeContainers, includeBounds });
1312
1640
  } else {
1313
- elements = await browser.execute(get_interactable_browser_elements_default, elementType);
1641
+ elements = await getBrowserInteractableElements(browser, { elementType });
1314
1642
  }
1315
1643
  if (inViewportOnly) {
1316
1644
  elements = elements.filter((el) => el.isInViewport !== false);
@@ -1501,29 +1829,15 @@ var deleteCookiesTool = async ({ name }) => {
1501
1829
  }
1502
1830
  };
1503
1831
 
1504
- // src/tools/get-accessibility-tree.tool.ts
1505
- import { encode as encode2 } from "@toon-format/toon";
1506
- import { z as z10 } from "zod";
1507
- var getAccessibilityToolDefinition = {
1508
- name: "get_accessibility",
1509
- 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.",
1510
- inputSchema: {
1511
- limit: z10.number().optional().describe("Maximum number of nodes to return. Default: 100. Use 0 for unlimited."),
1512
- offset: z10.number().optional().describe("Number of nodes to skip (for pagination). Default: 0."),
1513
- roles: z10.array(z10.string()).optional().describe('Filter to specific roles (e.g., ["button", "link", "textbox"]). Default: all roles.'),
1514
- namedOnly: z10.boolean().optional().describe("Only return nodes with a name/label. Default: true. Filters out anonymous containers.")
1515
- }
1516
- };
1832
+ // src/scripts/get-browser-accessibility-tree.ts
1517
1833
  function flattenAccessibilityTree(node, result = []) {
1518
1834
  if (!node) return result;
1519
1835
  if (node.role !== "WebArea" || node.name) {
1520
1836
  const entry = {
1521
- // Primary identifiers (most useful)
1522
1837
  role: node.role || "",
1523
1838
  name: node.name || "",
1524
1839
  value: node.value ?? "",
1525
1840
  description: node.description || "",
1526
- // Boolean states (empty string = not applicable/false)
1527
1841
  disabled: node.disabled ? "true" : "",
1528
1842
  focused: node.focused ? "true" : "",
1529
1843
  selected: node.selected ? "true" : "",
@@ -1532,7 +1846,6 @@ function flattenAccessibilityTree(node, result = []) {
1532
1846
  pressed: node.pressed === true ? "true" : node.pressed === false ? "false" : node.pressed === "mixed" ? "mixed" : "",
1533
1847
  readonly: node.readonly ? "true" : "",
1534
1848
  required: node.required ? "true" : "",
1535
- // Less common properties
1536
1849
  level: node.level ?? "",
1537
1850
  valuemin: node.valuemin ?? "",
1538
1851
  valuemax: node.valuemax ?? "",
@@ -1556,6 +1869,35 @@ function flattenAccessibilityTree(node, result = []) {
1556
1869
  }
1557
1870
  return result;
1558
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
+ };
1559
1901
  var getAccessibilityTreeTool = async (args) => {
1560
1902
  try {
1561
1903
  const browser = getBrowser();
@@ -1568,24 +1910,12 @@ var getAccessibilityTreeTool = async (args) => {
1568
1910
  };
1569
1911
  }
1570
1912
  const { limit = 100, offset = 0, roles, namedOnly = true } = args || {};
1571
- const puppeteer = await browser.getPuppeteer();
1572
- const pages = await puppeteer.pages();
1573
- if (pages.length === 0) {
1574
- return {
1575
- content: [{ type: "text", text: "No active pages found" }]
1576
- };
1577
- }
1578
- const page = pages[0];
1579
- const snapshot = await page.accessibility.snapshot({
1580
- interestingOnly: true
1581
- // Filter to only interesting/semantic nodes
1582
- });
1583
- if (!snapshot) {
1913
+ let nodes = await getBrowserAccessibilityTree(browser);
1914
+ if (nodes.length === 0) {
1584
1915
  return {
1585
1916
  content: [{ type: "text", text: "No accessibility tree available" }]
1586
1917
  };
1587
1918
  }
1588
- let nodes = flattenAccessibilityTree(snapshot);
1589
1919
  if (namedOnly) {
1590
1920
  nodes = nodes.filter((n) => n.name && n.name.trim() !== "");
1591
1921
  }
@@ -2017,11 +2347,21 @@ var package_default = {
2017
2347
  type: "git",
2018
2348
  url: "git://github.com/webdriverio/mcp.git"
2019
2349
  },
2020
- version: "2.0.0",
2350
+ version: "2.1.0",
2021
2351
  description: "MCP server with WebdriverIO for browser and mobile app automation (iOS/Android via Appium)",
2022
2352
  main: "./lib/server.js",
2023
2353
  module: "./lib/server.js",
2024
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
+ },
2025
2365
  bin: {
2026
2366
  "wdio-mcp": "lib/server.js"
2027
2367
  },
@@ -2045,6 +2385,7 @@ var package_default = {
2045
2385
  },
2046
2386
  dependencies: {
2047
2387
  "@modelcontextprotocol/sdk": "1.25",
2388
+ xpath: "^0.0.34",
2048
2389
  "@toon-format/toon": "^2.1.0",
2049
2390
  "@wdio/protocols": "^9.16.2",
2050
2391
  "@xmldom/xmldom": "^0.8.11",
@@ -2082,7 +2423,7 @@ var server = new McpServer({
2082
2423
  description: package_default.description,
2083
2424
  websiteUrl: "https://github.com/webdriverio/mcp"
2084
2425
  }, {
2085
- 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.",
2086
2427
  capabilities: {
2087
2428
  tools: {}
2088
2429
  }