@hypothesi/tauri-mcp-server 0.11.2 → 0.13.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.
@@ -1,26 +1,139 @@
1
1
  /**
2
- * Find an element using various selector strategies
2
+ * Find elements using various selector strategies and return structured data
3
3
  *
4
4
  * @param {Object} params
5
5
  * @param {string} params.selector - Element selector, ref ID (e.g., "ref=e3"), or text
6
6
  * @param {string} params.strategy - Selector strategy: 'css', 'xpath', or 'text'
7
+ * @param {number|null} params.nth - Zero-based index to pick one of several matching elements
8
+ * @param {number} params.limit - Maximum number of matches to return
9
+ * @param {string[]} params.properties - Computed CSS properties to include
10
+ * @param {boolean} params.visibleOnly - Return only rendered elements
11
+ * @param {boolean} params.includeHtml - Include outerHTML for each match
7
12
  */
8
13
  (function(params) {
9
- const { selector, strategy } = params;
10
-
11
- var element = window.__MCP__.resolveRef(selector, strategy);
12
-
13
- if (element) {
14
- const outerHTML = element.outerHTML;
15
- // Truncate very long HTML to avoid overwhelming output
16
- const truncated = outerHTML.length > 5000
17
- ? outerHTML.substring(0, 5000) + '...'
18
- : outerHTML;
19
- var msg = 'Found element: ' + truncated;
20
- var count = window.__MCP__.countAll(selector, strategy);
21
- if (count > 1) msg += '\n(+' + (count - 1) + ' more match' + (count - 1 === 1 ? '' : 'es') + ')';
22
- return msg;
14
+ const { selector, strategy, nth, limit, properties, visibleOnly, includeHtml } = params;
15
+
16
+ function getAccessibleName(element) {
17
+ try {
18
+ if (window.ariaApi && typeof window.ariaApi.getName === 'function') {
19
+ return window.ariaApi.getName(element) || null;
20
+ }
21
+ } catch (e) {
22
+ // Ignore aria-api errors
23
+ }
24
+ return null;
25
+ }
26
+
27
+ function getAttributes(element) {
28
+ const attrs = {
29
+ id: element.id || null,
30
+ class: element.className || null,
31
+ role: element.getAttribute('role') || null,
32
+ 'aria-label': element.getAttribute('aria-label') || null,
33
+ 'data-testid': element.getAttribute('data-testid') || null,
34
+ href: element.getAttribute('href') || null,
35
+ type: element.getAttribute('type') || null,
36
+ disabled: element.hasAttribute('disabled') ? '' : null,
37
+ };
38
+ // Remove null values
39
+ const result = {};
40
+ for (const key in attrs) {
41
+ if (attrs[key] !== null) {
42
+ result[key] = attrs[key];
43
+ }
44
+ }
45
+ return result;
46
+ }
47
+
48
+ function getStyles(element) {
49
+ if (!properties || properties.length === 0) return null;
50
+ const computed = window.getComputedStyle(element);
51
+ const result = {};
52
+ for (let i = 0; i < properties.length; i++) {
53
+ const prop = properties[i];
54
+ result[prop] = computed.getPropertyValue(prop);
55
+ }
56
+ return result;
57
+ }
58
+
59
+ function isInteractive(element) {
60
+ return element.matches && element.matches(window.__MCP__.INTERACTIVE_SELECTOR);
61
+ }
62
+
63
+ // Get all candidates first
64
+ var allMatches = window.__MCP__.resolveAll(selector, strategy);
65
+
66
+ // Filter by visibility if requested
67
+ var matches = visibleOnly
68
+ ? allMatches.filter(function(el) { return window.__MCP__.isRendered(el); })
69
+ : allMatches;
70
+
71
+ const totalMatches = matches.length;
72
+
73
+ // Apply nth if specified
74
+ if (typeof nth === 'number') {
75
+ if (!matches[nth]) {
76
+ return JSON.stringify({
77
+ selector: selector,
78
+ strategy: strategy,
79
+ totalMatches: totalMatches,
80
+ returned: 0,
81
+ truncated: false,
82
+ matches: [],
83
+ error: 'nth=' + nth + ' is out of range, ' + totalMatches + ' matches for ' + selector
84
+ });
85
+ }
86
+ matches = [matches[nth]];
23
87
  }
24
88
 
25
- return 'Element not found';
89
+ // Apply limit
90
+ const truncated = matches.length > limit;
91
+ if (truncated) {
92
+ matches = matches.slice(0, limit);
93
+ }
94
+
95
+ const returned = matches.length;
96
+
97
+ const results = matches.map(function(element, index) {
98
+ const rect = element.getBoundingClientRect();
99
+ const ref = window.__MCP__.reverseRefs && window.__MCP__.reverseRefs.has(element)
100
+ ? Array.from(window.__MCP__.reverseRefs.entries()).find(([k, v]) => v === element)[0]
101
+ : null;
102
+
103
+ const match = {
104
+ nth: typeof nth === 'number' ? nth : (index + (typeof nth !== 'number' ? 0 : 0)),
105
+ ref: ref,
106
+ tag: element.tagName.toLowerCase(),
107
+ text: (element.textContent || '').trim(),
108
+ accessibleName: getAccessibleName(element),
109
+ attributes: getAttributes(element),
110
+ rect: {
111
+ x: Math.round(rect.left),
112
+ y: Math.round(rect.top),
113
+ width: Math.round(rect.width),
114
+ height: Math.round(rect.height),
115
+ },
116
+ visible: window.__MCP__.isRendered(element),
117
+ interactive: isInteractive(element),
118
+ };
119
+
120
+ const styles = getStyles(element);
121
+ if (styles) match.styles = styles;
122
+
123
+ if (includeHtml) {
124
+ const html = element.outerHTML;
125
+ match.html = html.length > 2000 ? html.substring(0, 2000) + '…' : html;
126
+ }
127
+
128
+ return match;
129
+ });
130
+
131
+ return JSON.stringify({
132
+ selector: selector,
133
+ strategy: strategy,
134
+ totalMatches: totalMatches,
135
+ returned: returned,
136
+ truncated: truncated,
137
+ matches: results,
138
+ });
26
139
  })
@@ -4,15 +4,42 @@
4
4
  * @param {Object} params
5
5
  * @param {string} params.selector - CSS selector, XPath, text, or ref ID (e.g., "ref=e3") for element to focus
6
6
  * @param {string} params.strategy - Selector strategy: 'css', 'xpath', or 'text'
7
+ * @param {number|null} params.nth - Zero-based index to pick one of several matching elements
7
8
  */
8
9
  (function(params) {
9
- const { selector, strategy } = params;
10
+ const { selector, strategy, nth } = params;
10
11
 
11
12
  function resolveElement(selectorOrRef) {
12
13
  if (!selectorOrRef) return null;
13
- var el = window.__MCP__.resolveRef(selectorOrRef, strategy);
14
- if (!el) throw new Error('Element not found: ' + selectorOrRef);
15
- return el;
14
+ var matches = window.__MCP__.resolveAll(selectorOrRef, strategy);
15
+
16
+ if (matches.length === 0) throw new Error('Element not found: ' + selectorOrRef);
17
+ if (typeof nth === 'number') {
18
+ if (!matches[nth]) {
19
+ throw new Error('nth=' + nth + ' is out of range, ' + matches.length + ' matches for ' + selectorOrRef);
20
+ }
21
+ return matches[nth];
22
+ }
23
+ if (matches.length > 1 && strategy === 'text') {
24
+ throw new Error(describeAmbiguity(selectorOrRef, matches));
25
+ }
26
+ return matches[0];
27
+ }
28
+
29
+ function describeAmbiguity(selectorOrRef, matches) {
30
+ var lines = ['Ambiguous text selector "' + selectorOrRef + '" matched ' + matches.length + ' visible elements. Pass nth, or use a CSS selector or ref.'];
31
+ for (var i = 0; i < Math.min(matches.length, 5); i++) {
32
+ var el = matches[i];
33
+ var rect = el.getBoundingClientRect();
34
+ var text = (el.textContent || '').trim().substring(0, 50);
35
+ var tag = el.tagName.toLowerCase();
36
+ var className = el.className || '';
37
+ lines.push(' nth=' + i + ' ' + tag + (className ? '.' + className.split(' ')[0] : '') + ' "' + text + '" rect ' + Math.round(rect.left) + ',' + Math.round(rect.top) + ' ' + Math.round(rect.width) + 'x' + Math.round(rect.height));
38
+ }
39
+ if (matches.length > 5) {
40
+ lines.push(' ... and ' + (matches.length - 5) + ' more');
41
+ }
42
+ return lines.join('\n');
16
43
  }
17
44
 
18
45
  const element = resolveElement(selector);
@@ -58,14 +58,32 @@ export function buildScreenshotCaptureScript(format, quality) {
58
58
  return `
59
59
  ${HTML2CANVAS_RESOLVER_SCRIPT}
60
60
 
61
- // Capture the entire document
61
+ // Render the document, but crop the output to the visible viewport. Without
62
+ // explicit bounds html2canvas allocates a canvas for the full scrollable
63
+ // document, which is both inconsistent with the native implementations and
64
+ // can time out on long pages.
62
65
  const element = document.documentElement;
63
66
  if (!element) {
64
67
  throw new Error('document.documentElement is null');
65
68
  }
66
69
 
70
+ const viewportWidth = Math.max(element.clientWidth, window.innerWidth || 0);
71
+ const viewportHeight = Math.max(element.clientHeight, window.innerHeight || 0);
72
+ const viewportX = window.scrollX || window.pageXOffset || 0;
73
+ const viewportY = window.scrollY || window.pageYOffset || 0;
74
+
67
75
  // Capture the webview
68
- const canvas = await html2canvasFn(element, ${HTML2CANVAS_OPTIONS_SCRIPT});
76
+ const canvas = await html2canvasFn(element, {
77
+ ...${HTML2CANVAS_OPTIONS_SCRIPT},
78
+ x: viewportX,
79
+ y: viewportY,
80
+ width: viewportWidth,
81
+ height: viewportHeight,
82
+ windowWidth: viewportWidth,
83
+ windowHeight: viewportHeight,
84
+ scrollX: viewportX,
85
+ scrollY: viewportY,
86
+ });
69
87
  if (!canvas) {
70
88
  throw new Error('html2canvas returned null canvas');
71
89
  }
@@ -8,9 +8,7 @@ export declare const SCRIPTS: {
8
8
  readonly resolveRef: string;
9
9
  readonly interact: string;
10
10
  readonly swipe: string;
11
- readonly keyboard: string;
12
11
  readonly waitFor: string;
13
- readonly getStyles: string;
14
12
  readonly focus: string;
15
13
  readonly findElement: string;
16
14
  readonly domSnapshot: string;
@@ -28,9 +26,9 @@ export declare function getResolveRefSource(): string;
28
26
  */
29
27
  export declare function buildScript(script: string, params: Record<string, unknown>): string;
30
28
  /**
31
- * Build a script for typing text (uses the keyboard script's typeText function)
29
+ * Build a script for typing text
32
30
  */
33
- export declare function buildTypeScript(selector: string, text: string, strategy?: string): string;
31
+ export declare function buildTypeScript(selector: string, text: string, strategy?: string, nth?: number | null): string;
34
32
  /**
35
33
  * Build a script for key events (press, down, up)
36
34
  */
@@ -16,9 +16,7 @@ export const SCRIPTS = {
16
16
  resolveRef: loadScript('resolve-ref'),
17
17
  interact: loadScript('interact'),
18
18
  swipe: loadScript('swipe'),
19
- keyboard: loadScript('keyboard'),
20
19
  waitFor: loadScript('wait-for'),
21
- getStyles: loadScript('get-styles'),
22
20
  focus: loadScript('focus'),
23
21
  findElement: loadScript('find-element'),
24
22
  domSnapshot: loadScript('dom-snapshot'),
@@ -40,21 +38,52 @@ export function buildScript(script, params) {
40
38
  return `(${script})(${JSON.stringify(params)})`;
41
39
  }
42
40
  /**
43
- * Build a script for typing text (uses the keyboard script's typeText function)
41
+ * Build a script for typing text
44
42
  */
45
- export function buildTypeScript(selector, text, strategy) {
46
- const escapedText = text.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
47
- const escapedSelector = selector.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
43
+ export function buildTypeScript(selector, text, strategy, nth) {
48
44
  const strat = strategy || 'css';
45
+ const nthValue = nth ?? null;
49
46
  return `
50
47
  (function() {
51
- const selector = '${escapedSelector}';
52
- const strategy = '${strat}';
53
- const text = '${escapedText}';
48
+ const selector = ${JSON.stringify(selector)};
49
+ const strategy = ${JSON.stringify(strat)};
50
+ const text = ${JSON.stringify(text)};
51
+ const nth = ${JSON.stringify(nthValue)};
54
52
 
55
- var element = window.__MCP__.resolveRef(selector, strategy);
56
- if (!element) throw new Error('Element not found: ' + selector);
53
+ function resolveElement(selectorOrRef) {
54
+ if (!selectorOrRef) return null;
55
+ var matches = window.__MCP__.resolveAll(selectorOrRef, strategy);
57
56
 
57
+ if (matches.length === 0) throw new Error('Element not found: ' + selectorOrRef);
58
+ if (typeof nth === 'number') {
59
+ if (!matches[nth]) {
60
+ throw new Error('nth=' + nth + ' is out of range, ' + matches.length + ' matches for ' + selectorOrRef);
61
+ }
62
+ return matches[nth];
63
+ }
64
+ if (matches.length > 1 && strategy === 'text') {
65
+ throw new Error(describeAmbiguity(selectorOrRef, matches));
66
+ }
67
+ return matches[0];
68
+ }
69
+
70
+ function describeAmbiguity(selectorOrRef, matches) {
71
+ var lines = ['Ambiguous text selector "' + selectorOrRef + '" matched ' + matches.length + ' visible elements. Pass nth, or use a CSS selector or ref.'];
72
+ for (var i = 0; i < Math.min(matches.length, 5); i++) {
73
+ var el = matches[i];
74
+ var rect = el.getBoundingClientRect();
75
+ var text = (el.textContent || '').trim().substring(0, 50);
76
+ var tag = el.tagName.toLowerCase();
77
+ var className = el.className || '';
78
+ lines.push(' nth=' + i + ' ' + tag + (className ? '.' + className.split(' ')[0] : '') + ' "' + text + '" rect ' + Math.round(rect.left) + ',' + Math.round(rect.top) + ' ' + Math.round(rect.width) + 'x' + Math.round(rect.height));
79
+ }
80
+ if (matches.length > 5) {
81
+ lines.push(' ... and ' + (matches.length - 5) + ' more');
82
+ }
83
+ return lines.join('\\n');
84
+ }
85
+
86
+ var element = resolveElement(selector);
58
87
  element.focus();
59
88
 
60
89
  // Use native prototype setter to bypass React's value tracker
@@ -1,11 +1,12 @@
1
1
  /**
2
- * Webview interaction script - handles click, double-click, long-press, and scroll actions
2
+ * Webview interaction script - handles click, double-click, long-press, scroll, swipe, hover, and right-click actions
3
3
  * This script is injected into the webview and executed with parameters.
4
4
  *
5
5
  * @param {Object} params
6
6
  * @param {string} params.action - The action to perform
7
7
  * @param {string|null} params.selector - CSS selector, XPath, text, or ref ID (e.g., "ref=e3") for the element
8
8
  * @param {string} params.strategy - Selector strategy: 'css', 'xpath', or 'text'
9
+ * @param {number|null} params.nth - Zero-based index to pick one of several matching elements
9
10
  * @param {number|null} params.x - X coordinate
10
11
  * @param {number|null} params.y - Y coordinate
11
12
  * @param {number} params.duration - Duration for long-press
@@ -13,13 +14,39 @@
13
14
  * @param {number} params.scrollY - Vertical scroll amount
14
15
  */
15
16
  (function(params) {
16
- const { action, selector, strategy, x, y, duration, scrollX, scrollY } = params;
17
+ const { action, selector, strategy, nth, x, y, duration, scrollX, scrollY } = params;
17
18
 
18
19
  function resolveElement(selectorOrRef) {
19
20
  if (!selectorOrRef) return null;
20
- var el = window.__MCP__.resolveRef(selectorOrRef, strategy);
21
- if (!el) throw new Error('Element not found: ' + selectorOrRef);
22
- return el;
21
+ var matches = window.__MCP__.resolveAll(selectorOrRef, strategy);
22
+
23
+ if (matches.length === 0) throw new Error('Element not found: ' + selectorOrRef);
24
+ if (typeof nth === 'number') {
25
+ if (!matches[nth]) {
26
+ throw new Error('nth=' + nth + ' is out of range, ' + matches.length + ' matches for ' + selectorOrRef);
27
+ }
28
+ return matches[nth];
29
+ }
30
+ if (matches.length > 1 && strategy === 'text') {
31
+ throw new Error(describeAmbiguity(selectorOrRef, matches));
32
+ }
33
+ return matches[0];
34
+ }
35
+
36
+ function describeAmbiguity(selectorOrRef, matches) {
37
+ var lines = ['Ambiguous text selector "' + selectorOrRef + '" matched ' + matches.length + ' visible elements. Pass nth, or use a CSS selector or ref.'];
38
+ for (var i = 0; i < Math.min(matches.length, 5); i++) {
39
+ var el = matches[i];
40
+ var rect = el.getBoundingClientRect();
41
+ var text = (el.textContent || '').trim().substring(0, 50);
42
+ var tag = el.tagName.toLowerCase();
43
+ var className = el.className || '';
44
+ lines.push(' nth=' + i + ' ' + tag + (className ? '.' + className.split(' ')[0] : '') + ' "' + text + '" rect ' + Math.round(rect.left) + ',' + Math.round(rect.top) + ' ' + Math.round(rect.width) + 'x' + Math.round(rect.height));
45
+ }
46
+ if (matches.length > 5) {
47
+ lines.push(' ... and ' + (matches.length - 5) + ' more');
48
+ }
49
+ return lines.join('\n');
23
50
  }
24
51
 
25
52
  function matchHint() {
@@ -62,6 +89,19 @@
62
89
  clientY: targetY,
63
90
  };
64
91
 
92
+ const pointerOptions = {
93
+ ...eventOptions,
94
+ pointerId: 1,
95
+ pointerType: 'mouse',
96
+ isPrimary: true,
97
+ };
98
+
99
+ const rightButtonOptions = {
100
+ ...eventOptions,
101
+ button: 2,
102
+ buttons: 2,
103
+ };
104
+
65
105
  if (action === 'click') {
66
106
  if (element) {
67
107
  element.dispatchEvent(new MouseEvent('mousedown', eventOptions));
@@ -94,6 +134,27 @@
94
134
  return `Long-pressed at (${targetX}, ${targetY}) for ${duration}ms` + matchHint();
95
135
  }
96
136
 
137
+ if (action === 'hover') {
138
+ if (element) {
139
+ element.dispatchEvent(new PointerEvent('pointerover', pointerOptions));
140
+ element.dispatchEvent(new PointerEvent('pointerenter', pointerOptions));
141
+ element.dispatchEvent(new MouseEvent('mouseover', eventOptions));
142
+ element.dispatchEvent(new MouseEvent('mouseenter', eventOptions));
143
+ element.dispatchEvent(new PointerEvent('pointermove', pointerOptions));
144
+ element.dispatchEvent(new MouseEvent('mousemove', eventOptions));
145
+ }
146
+ return `Hovered at (${targetX}, ${targetY})` + matchHint();
147
+ }
148
+
149
+ if (action === 'right-click') {
150
+ if (element) {
151
+ element.dispatchEvent(new MouseEvent('mousedown', rightButtonOptions));
152
+ element.dispatchEvent(new MouseEvent('mouseup', rightButtonOptions));
153
+ element.dispatchEvent(new MouseEvent('contextmenu', rightButtonOptions));
154
+ }
155
+ return `Right-clicked at (${targetX}, ${targetY})` + matchHint();
156
+ }
157
+
97
158
  if (action === 'scroll') {
98
159
  const scrollTarget = element || window;
99
160
  if (scrollX !== 0 || scrollY !== 0) {
@@ -15,6 +15,84 @@
15
15
 
16
16
  var REF_PATTERN = /^\[?(?:ref=)?(e\d+)\]?$/;
17
17
 
18
+ var NON_RENDERED_TAGS = {
19
+ SCRIPT: 1, STYLE: 1, TITLE: 1, HEAD: 1, META: 1,
20
+ LINK: 1, TEMPLATE: 1, NOSCRIPT: 1, BASE: 1,
21
+ };
22
+
23
+ var INTERACTIVE_SELECTOR = 'a,button,input,select,textarea,summary,' +
24
+ '[role=button],[role=link],[role=tab],[role=menuitem],[role=option],[tabindex]';
25
+
26
+ function isRendered(element) {
27
+ if (!element || element.nodeType !== 1) return false;
28
+ if (NON_RENDERED_TAGS[element.tagName]) return false;
29
+ if (element.hidden) return false;
30
+ if (element.getClientRects().length === 0) return false;
31
+ try {
32
+ var style = window.getComputedStyle(element);
33
+ if (style.display === 'none') return false;
34
+ if (style.visibility === 'hidden' || style.visibility === 'collapse') return false;
35
+ } catch (e) {
36
+ return false;
37
+ }
38
+ return true;
39
+ }
40
+
41
+ function hasMatchingDescendant(element, needle) {
42
+ var descendants = element.querySelectorAll('*');
43
+ for (var i = 0; i < descendants.length; i++) {
44
+ var el = descendants[i];
45
+ if (!isRendered(el)) continue;
46
+ var content = (el.textContent || '').trim();
47
+ if (content.indexOf(needle) !== -1) return true;
48
+ }
49
+ return false;
50
+ }
51
+
52
+ function preferInteractive(el) {
53
+ if (el.matches(INTERACTIVE_SELECTOR)) return el;
54
+ var host = el.closest(INTERACTIVE_SELECTOR);
55
+ return host && isRendered(host) ? host : el;
56
+ }
57
+
58
+ function dedupe(arr) {
59
+ var seen = new Set();
60
+ var result = [];
61
+ for (var i = 0; i < arr.length; i++) {
62
+ var el = arr[i];
63
+ if (!seen.has(el)) {
64
+ seen.add(el);
65
+ result.push(el);
66
+ }
67
+ }
68
+ return result;
69
+ }
70
+
71
+ function textCandidates(needle) {
72
+ var all = document.body ? document.body.querySelectorAll('*') : [],
73
+ exact = [], partial = [];
74
+
75
+ for (var i = 0; i < all.length; i++) {
76
+ var el = all[i],
77
+ content = (el.textContent || '').trim(),
78
+ attributeText = [
79
+ el.getAttribute('placeholder'),
80
+ el.getAttribute('aria-label'),
81
+ el.getAttribute('title'),
82
+ ].filter(Boolean),
83
+ exactAttribute = attributeText.indexOf(needle) !== -1,
84
+ partialAttribute = attributeText.some(function(value) { return value.indexOf(needle) !== -1; }),
85
+ textMatch = content.indexOf(needle) !== -1;
86
+
87
+ if (!textMatch && !partialAttribute) continue;
88
+ if (!isRendered(el)) continue;
89
+ if (textMatch && el.querySelector('*') && hasMatchingDescendant(el, needle)) continue;
90
+ (content === needle || exactAttribute ? exact : partial).push(preferInteractive(el));
91
+ }
92
+
93
+ return dedupe(exact.length > 0 ? exact : partial);
94
+ }
95
+
18
96
  function xpathForText(text) {
19
97
  // Escape single quotes for XPath by splitting on ' and using concat()
20
98
  if (text.indexOf("'") === -1) {
@@ -47,39 +125,26 @@
47
125
  }
48
126
 
49
127
  if (strategy === 'text') {
50
- // First try: match element text content
51
- var xpath = xpathForText(selectorOrRef);
52
- var result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
53
- if (result.singleNodeValue) return result.singleNodeValue;
54
-
55
- // Fallback: search placeholder, aria-label, and title attributes
56
- var attrSelectors = [
57
- '[placeholder*="' + selectorOrRef.replace(/"/g, '\\"') + '"]',
58
- '[aria-label*="' + selectorOrRef.replace(/"/g, '\\"') + '"]',
59
- '[title*="' + selectorOrRef.replace(/"/g, '\\"') + '"]',
60
- ];
61
- for (var i = 0; i < attrSelectors.length; i++) {
62
- var el = document.querySelector(attrSelectors[i]);
63
- if (el) return el;
64
- }
65
- return null;
128
+ var candidates = textCandidates(selectorOrRef);
129
+ return candidates[0] || null;
66
130
  }
67
131
 
68
132
  if (strategy === 'xpath') {
69
133
  var result = document.evaluate(selectorOrRef, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
70
- return result.singleNodeValue;
134
+ var node = result.singleNodeValue;
135
+ return (node && isRendered(node)) ? node : null;
71
136
  }
72
137
 
73
138
  // Default: CSS selector
74
139
  return document.querySelector(selectorOrRef);
75
140
  };
76
141
 
77
- /**
78
- * Resolve all matching elements as an Array.
79
- * @param {string} selector - Selector, XPath, or text
80
- * @param {string} [strategy] - 'css' (default), 'xpath', or 'text'
81
- * @returns {Element[]}
82
- */
142
+ /**
143
+ * Resolve all matching elements as an Array.
144
+ * @param {string} selector - Selector, XPath, or text
145
+ * @param {string} [strategy] - 'css' (default), 'xpath', or 'text'
146
+ * @returns {Element[]}
147
+ */
83
148
  window.__MCP__.resolveAll = function(selector, strategy) {
84
149
  if (!selector) return [];
85
150
 
@@ -91,33 +156,15 @@
91
156
  }
92
157
 
93
158
  if (strategy === 'text') {
94
- // First try: match element text content
95
- var xpath = xpathForText(selector);
96
- var snapshot = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
97
- var results = [];
98
- for (var i = 0; i < snapshot.snapshotLength; i++) {
99
- results.push(snapshot.snapshotItem(i));
100
- }
101
- if (results.length > 0) return results;
102
-
103
- // Fallback: search placeholder, aria-label, and title attributes
104
- var attrSelectors = [
105
- '[placeholder*="' + selector.replace(/"/g, '\\"') + '"]',
106
- '[aria-label*="' + selector.replace(/"/g, '\\"') + '"]',
107
- '[title*="' + selector.replace(/"/g, '\\"') + '"]',
108
- ];
109
- for (var i = 0; i < attrSelectors.length; i++) {
110
- var found = Array.from(document.querySelectorAll(attrSelectors[i]));
111
- if (found.length > 0) return results.concat(found);
112
- }
113
- return results;
159
+ return textCandidates(selector);
114
160
  }
115
161
 
116
162
  if (strategy === 'xpath') {
117
163
  var snapshot = document.evaluate(selector, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
118
164
  var results = [];
119
165
  for (var i = 0; i < snapshot.snapshotLength; i++) {
120
- results.push(snapshot.snapshotItem(i));
166
+ var node = snapshot.snapshotItem(i);
167
+ if (node && isRendered(node)) results.push(node);
121
168
  }
122
169
  return results;
123
170
  }
@@ -126,13 +173,17 @@
126
173
  return Array.from(document.querySelectorAll(selector));
127
174
  };
128
175
 
129
- /**
130
- * Count all matching elements.
131
- * @param {string} selector - Selector, XPath, or text
132
- * @param {string} [strategy] - 'css' (default), 'xpath', or 'text'
133
- * @returns {number}
134
- */
176
+ /**
177
+ * Count all matching elements.
178
+ * @param {string} selector - Selector, XPath, or text
179
+ * @param {string} [strategy] - 'css' (default), 'xpath', or 'text'
180
+ * @returns {number}
181
+ */
135
182
  window.__MCP__.countAll = function(selector, strategy) {
136
183
  return window.__MCP__.resolveAll(selector, strategy).length;
137
184
  };
185
+
186
+ // Export for reuse by other scripts
187
+ window.__MCP__.isRendered = isRendered;
188
+ window.__MCP__.INTERACTIVE_SELECTOR = INTERACTIVE_SELECTOR;
138
189
  })();