@hypothesi/tauri-mcp-server 0.8.2 → 0.9.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.
@@ -0,0 +1,395 @@
1
+ /**
2
+ * Element picker overlay for MCP Server Tauri.
3
+ *
4
+ * Activated by the agent via webview_select_element. Displays a hover highlight,
5
+ * context tooltip, and cancel bar. On click (desktop) or two-tap (mobile) the
6
+ * selected element's metadata is emitted as a Tauri event so the MCP server can
7
+ * retrieve it asynchronously.
8
+ *
9
+ * @param {Object} params
10
+ * @param {string} params.mode - 'pick' (agent-initiated picker)
11
+ * @param {string} params.pickerId - Unique identifier for this picker session
12
+ */
13
+ (function(params) {
14
+ var mode = params.mode;
15
+ var pickerId = params.pickerId;
16
+
17
+ // Duplicate-activation guard
18
+ if (window.__MCP_PICKER_ACTIVE__) {
19
+ // Cancel the previous picker
20
+ var prevId = window.__MCP_PICKER_ACTIVE__;
21
+ cleanup();
22
+ if (window.__TAURI__ && window.__TAURI__.event && window.__TAURI__.event.emit) {
23
+ window.__TAURI__.event.emit('__element_picked', { pickerId: prevId, cancelled: true });
24
+ }
25
+ }
26
+ window.__MCP_PICKER_ACTIVE__ = pickerId;
27
+
28
+ var isTouch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
29
+ var highlight = null;
30
+ var tooltip = null;
31
+ var cancelBar = null;
32
+ var trackedElement = null; // element currently being highlighted
33
+ var rafId = null; // requestAnimationFrame handle for cyclic repositioning
34
+
35
+ // ── Velocity-based hover throttling state ──────────────────────────────
36
+ var lastMouseX = 0;
37
+ var lastMouseY = 0;
38
+ var lastMouseTime = 0;
39
+ var mouseVelocity = 0;
40
+ var hoverUpdateTimer = null;
41
+
42
+ // ── Cancel bar ─────────────────────────────────────────────────────────
43
+ cancelBar = document.createElement('div');
44
+ cancelBar.setAttribute('data-mcp-picker', 'cancel-bar');
45
+ var cancelBarAtTop = true;
46
+ cancelBar.style.cssText =
47
+ 'position:fixed;top:0;left:0;right:0;z-index:2147483647;height:40px;' +
48
+ 'background:rgba(30,41,59,0.95);display:flex;align-items:center;' +
49
+ 'justify-content:space-between;padding:0 12px;font:13px/1 system-ui,sans-serif;' +
50
+ 'color:#E2E8F0;box-sizing:border-box;' +
51
+ 'transition:top 0.2s ease,bottom 0.2s ease;';
52
+
53
+ var cancelText = document.createElement('span');
54
+ cancelText.textContent = 'MCP Element Picker \u2014 Click to select | ESC or tap X to cancel';
55
+ cancelText.style.cssText = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;';
56
+
57
+ // Move button — toggles bar between top and bottom
58
+ var moveBtn = document.createElement('button');
59
+ moveBtn.setAttribute('data-mcp-picker', 'move-btn');
60
+ moveBtn.textContent = '\u2193'; // ↓
61
+ moveBtn.title = 'Move bar to bottom';
62
+ moveBtn.style.cssText =
63
+ 'background:none;border:none;color:#94A3B8;font-size:16px;cursor:pointer;' +
64
+ 'width:32px;height:40px;display:flex;align-items:center;justify-content:center;' +
65
+ 'flex-shrink:0;';
66
+ function toggleCancelBarPosition(e) {
67
+ e.stopPropagation();
68
+ e.preventDefault();
69
+ cancelBarAtTop = !cancelBarAtTop;
70
+ cancelBar.style.top = cancelBarAtTop ? '0' : 'auto';
71
+ cancelBar.style.bottom = cancelBarAtTop ? 'auto' : '0';
72
+ moveBtn.textContent = cancelBarAtTop ? '\u2193' : '\u2191'; // ↓ or ↑
73
+ moveBtn.title = cancelBarAtTop ? 'Move bar to bottom' : 'Move bar to top';
74
+ }
75
+ moveBtn.addEventListener('click', toggleCancelBarPosition);
76
+ moveBtn.addEventListener('touchend', toggleCancelBarPosition);
77
+
78
+ var cancelBtn = document.createElement('button');
79
+ cancelBtn.setAttribute('data-mcp-picker', 'cancel-btn');
80
+ cancelBtn.textContent = '\u2715';
81
+ cancelBtn.style.cssText =
82
+ 'background:none;border:none;color:#E2E8F0;font-size:20px;cursor:pointer;' +
83
+ 'width:40px;height:40px;display:flex;align-items:center;justify-content:center;' +
84
+ 'flex-shrink:0;';
85
+ cancelBtn.addEventListener('click', function(e) {
86
+ e.stopPropagation();
87
+ cancelPicker();
88
+ });
89
+ cancelBtn.addEventListener('touchend', function(e) {
90
+ e.stopPropagation();
91
+ e.preventDefault();
92
+ cancelPicker();
93
+ });
94
+
95
+ cancelBar.appendChild(cancelText);
96
+ cancelBar.appendChild(moveBtn);
97
+ cancelBar.appendChild(cancelBtn);
98
+ document.body.appendChild(cancelBar);
99
+
100
+ // ── Highlight helpers ──────────────────────────────────────────────────
101
+ function createHighlight() {
102
+ var el = document.createElement('div');
103
+ el.setAttribute('data-mcp-picker', 'highlight');
104
+ el.style.cssText =
105
+ 'position:fixed;z-index:2147483645;pointer-events:none;' +
106
+ 'background:rgba(59,130,246,0.15);border:2px solid #3B82F6;';
107
+ document.body.appendChild(el);
108
+ return el;
109
+ }
110
+
111
+ function positionHighlight(el, target) {
112
+ var rect = target.getBoundingClientRect();
113
+ el.style.top = rect.top + 'px';
114
+ el.style.left = rect.left + 'px';
115
+ el.style.width = rect.width + 'px';
116
+ el.style.height = rect.height + 'px';
117
+ }
118
+
119
+ // ── Cyclic highlight repositioning via rAF ─────────────────────────────
120
+ // Keeps the highlight tracking animated or repositioned elements at ~60fps.
121
+ function startTracking() {
122
+ if (rafId) return;
123
+ function tick() {
124
+ if (trackedElement && highlight) {
125
+ positionHighlight(highlight, trackedElement);
126
+ }
127
+ rafId = requestAnimationFrame(tick);
128
+ }
129
+ rafId = requestAnimationFrame(tick);
130
+ }
131
+
132
+ function stopTracking() {
133
+ if (rafId) {
134
+ cancelAnimationFrame(rafId);
135
+ rafId = null;
136
+ }
137
+ }
138
+
139
+ // ── Tooltip helpers ────────────────────────────────────────────────────
140
+ function showTooltip(target) {
141
+ if (!tooltip) {
142
+ tooltip = document.createElement('div');
143
+ tooltip.setAttribute('data-mcp-picker', 'tooltip');
144
+ tooltip.style.cssText =
145
+ 'position:fixed;z-index:2147483646;pointer-events:none;' +
146
+ 'background:#1E293B;color:#E2E8F0;font:12px/1.4 monospace;' +
147
+ 'padding:4px 8px;border-radius:4px;white-space:nowrap;max-width:300px;' +
148
+ 'overflow:hidden;text-overflow:ellipsis;';
149
+ document.body.appendChild(tooltip);
150
+ }
151
+
152
+ var rect = target.getBoundingClientRect();
153
+ var tag = target.tagName.toLowerCase();
154
+ var id = target.id ? '#' + target.id : '';
155
+ var cls = target.className && typeof target.className === 'string'
156
+ ? '.' + target.className.trim().split(/\s+/).join('.')
157
+ : '';
158
+ var label = (tag + id + cls);
159
+ if (label.length > 60) label = label.substring(0, 57) + '...';
160
+ label += ' (' + Math.round(rect.width) + '\u00d7' + Math.round(rect.height) + ')';
161
+ tooltip.textContent = label;
162
+
163
+ // Position above or below the element
164
+ var tooltipTop = rect.top - 28;
165
+ if (tooltipTop < 44) { // below the cancel bar (40px + 4px padding)
166
+ tooltipTop = rect.bottom + 4;
167
+ }
168
+ tooltip.style.top = tooltipTop + 'px';
169
+ tooltip.style.left = Math.max(4, rect.left) + 'px';
170
+ }
171
+
172
+ function hideTooltip() {
173
+ if (tooltip && tooltip.parentNode) {
174
+ tooltip.parentNode.removeChild(tooltip);
175
+ tooltip = null;
176
+ }
177
+ }
178
+
179
+ // ── Element detection ──────────────────────────────────────────────────
180
+ // Uses the shared elementsFromPoint helper from bridge.js when available,
181
+ // falls back to document.elementFromPoint.
182
+ function findElementAt(x, y) {
183
+ if (window.__MCP_GET_ELEMENT_AT_POINT__) {
184
+ return window.__MCP_GET_ELEMENT_AT_POINT__(x, y);
185
+ }
186
+ // Fallback: hide highlight, use single elementFromPoint
187
+ if (highlight) highlight.style.display = 'none';
188
+ var el = document.elementFromPoint(x, y);
189
+ if (highlight) highlight.style.display = '';
190
+ return el;
191
+ }
192
+
193
+ // ── Picker element detection guard (fallback for findElementAt) ────────
194
+ function isPickerUI(el) {
195
+ while (el) {
196
+ if (el.getAttribute && el.getAttribute('data-mcp-picker')) {
197
+ return true;
198
+ }
199
+ el = el.parentElement;
200
+ }
201
+ return false;
202
+ }
203
+
204
+ // ── Selection ──────────────────────────────────────────────────────────
205
+ function selectElement(target) {
206
+ var metadata;
207
+ if (window.__MCP_COLLECT_ELEMENT_METADATA__) {
208
+ metadata = window.__MCP_COLLECT_ELEMENT_METADATA__(target);
209
+ } else {
210
+ metadata = { tag: target.tagName.toLowerCase(), cssSelector: '' };
211
+ }
212
+
213
+ // Store in window for later retrieval
214
+ window.__MCP_PICKED_ELEMENT__ = metadata;
215
+
216
+ // Remove overlay + tooltip but keep highlight for screenshot
217
+ removeCancelBar();
218
+ hideTooltip();
219
+ stopTracking();
220
+ removeListeners();
221
+
222
+ // Emit Tauri event
223
+ if (window.__TAURI__ && window.__TAURI__.event && window.__TAURI__.event.emit) {
224
+ window.__TAURI__.event.emit('__element_picked', { pickerId: pickerId, element: metadata });
225
+ }
226
+
227
+ window.__MCP_PICKER_ACTIVE__ = null;
228
+ return 'Element selected: ' + metadata.tag + (metadata.id ? '#' + metadata.id : '');
229
+ }
230
+
231
+ // ── Cancellation ──────────────────────────────────────────────────────
232
+ function cancelPicker() {
233
+ cleanup();
234
+ if (window.__TAURI__ && window.__TAURI__.event && window.__TAURI__.event.emit) {
235
+ window.__TAURI__.event.emit('__element_picked', { pickerId: pickerId, cancelled: true });
236
+ }
237
+ window.__MCP_PICKER_ACTIVE__ = null;
238
+ }
239
+
240
+ // ── Cleanup ────────────────────────────────────────────────────────────
241
+ function removeCancelBar() {
242
+ if (cancelBar && cancelBar.parentNode) {
243
+ cancelBar.parentNode.removeChild(cancelBar);
244
+ cancelBar = null;
245
+ }
246
+ }
247
+
248
+ function removeHighlight() {
249
+ if (highlight && highlight.parentNode) {
250
+ highlight.parentNode.removeChild(highlight);
251
+ highlight = null;
252
+ }
253
+ }
254
+
255
+ function cleanup() {
256
+ removeCancelBar();
257
+ removeHighlight();
258
+ hideTooltip();
259
+ stopTracking();
260
+ removeListeners();
261
+ trackedElement = null;
262
+ if (hoverUpdateTimer) {
263
+ clearTimeout(hoverUpdateTimer);
264
+ hoverUpdateTimer = null;
265
+ }
266
+ window.__MCP_PICKER_ACTIVE__ = null;
267
+ }
268
+
269
+ // ── Core hover-update logic (shared by throttled and immediate paths) ──
270
+ function updateHoveredElement() {
271
+ var el = findElementAt(lastMouseX, lastMouseY);
272
+
273
+ if (!el || isPickerUI(el)) return;
274
+
275
+ if (el === trackedElement) return; // no change
276
+ trackedElement = el;
277
+
278
+ if (!highlight) {
279
+ highlight = createHighlight();
280
+ startTracking();
281
+ }
282
+ positionHighlight(highlight, el);
283
+ showTooltip(el);
284
+ }
285
+
286
+ // ── Desktop: hover + click with velocity throttling ────────────────────
287
+ function onMouseMove(e) {
288
+ var now = performance.now();
289
+ var dx = e.clientX - lastMouseX;
290
+ var dy = e.clientY - lastMouseY;
291
+ var dt = now - lastMouseTime;
292
+ var distance = Math.sqrt(dx * dx + dy * dy);
293
+
294
+ lastMouseX = e.clientX;
295
+ lastMouseY = e.clientY;
296
+
297
+ // Calculate velocity in pixels per second
298
+ mouseVelocity = dt > 0 ? (distance / dt) * 1000 : 0;
299
+ lastMouseTime = now;
300
+
301
+ // If moving fast (>600 px/s), throttle updates to ~28fps
302
+ if (mouseVelocity > 600) {
303
+ if (hoverUpdateTimer) {
304
+ clearTimeout(hoverUpdateTimer);
305
+ }
306
+ hoverUpdateTimer = setTimeout(updateHoveredElement, 36); // ~28fps
307
+ } else {
308
+ if (hoverUpdateTimer) {
309
+ clearTimeout(hoverUpdateTimer);
310
+ hoverUpdateTimer = null;
311
+ }
312
+ updateHoveredElement();
313
+ }
314
+ }
315
+
316
+ function onClick(e) {
317
+ // Let clicks on picker UI (cancel, move buttons) pass through to their handlers
318
+ if (isPickerUI(e.target)) return;
319
+
320
+ e.preventDefault();
321
+ e.stopPropagation();
322
+
323
+ var el = findElementAt(e.clientX, e.clientY);
324
+
325
+ if (!el || isPickerUI(el)) return;
326
+
327
+ selectElement(el);
328
+ }
329
+
330
+ // ── Mobile: two-tap ────────────────────────────────────────────────────
331
+ var lastTapTarget = null;
332
+
333
+ function onTouchEnd(e) {
334
+ var touch = e.changedTouches[0];
335
+ if (!touch) return;
336
+
337
+ var el = findElementAt(touch.clientX, touch.clientY);
338
+
339
+ // Let taps on picker UI (cancel, move buttons) pass through to their handlers
340
+ if (!el || isPickerUI(el)) return;
341
+
342
+ e.preventDefault();
343
+ e.stopPropagation();
344
+
345
+ if (lastTapTarget === el) {
346
+ // Second tap on same element -> confirm
347
+ selectElement(el);
348
+ } else {
349
+ // First tap (or different element) -> highlight
350
+ lastTapTarget = el;
351
+ trackedElement = el;
352
+
353
+ if (!highlight) {
354
+ highlight = createHighlight();
355
+ startTracking();
356
+ }
357
+ positionHighlight(highlight, el);
358
+ showTooltip(el);
359
+
360
+ if (cancelText) {
361
+ cancelText.textContent = 'Tap element again to send | X Cancel';
362
+ }
363
+ }
364
+ }
365
+
366
+ // ── Keyboard escape ────────────────────────────────────────────────────
367
+ function onKeyDown(e) {
368
+ if (e.key === 'Escape') {
369
+ e.preventDefault();
370
+ e.stopPropagation();
371
+ cancelPicker();
372
+ }
373
+ }
374
+
375
+ // ── Listener management ────────────────────────────────────────────────
376
+ function removeListeners() {
377
+ document.removeEventListener('mousemove', onMouseMove, true);
378
+ document.removeEventListener('click', onClick, true);
379
+ document.removeEventListener('touchend', onTouchEnd, true);
380
+ document.removeEventListener('keydown', onKeyDown, true);
381
+ }
382
+
383
+ // Attach appropriate listeners based on device
384
+ document.addEventListener('keydown', onKeyDown, true);
385
+
386
+ if (isTouch) {
387
+ document.addEventListener('touchend', onTouchEnd, true);
388
+ }
389
+
390
+ // Always attach mouse listeners (hybrid devices like touch laptops)
391
+ document.addEventListener('mousemove', onMouseMove, true);
392
+ document.addEventListener('click', onClick, true);
393
+
394
+ return 'Picker activated (id: ' + pickerId + ')';
395
+ })
@@ -7,36 +7,8 @@
7
7
  */
8
8
  (function(params) {
9
9
  const { selector, strategy } = params;
10
- let element;
11
10
 
12
- // Check if it's a ref ID first (works with any strategy)
13
- if (/^\[?(?:ref=)?(e\d+)\]?$/.test(selector)) {
14
- element = window.__MCP__.resolveRef(selector);
15
- } else if (strategy === 'text') {
16
- // Find element containing text
17
- const xpath = "//*[contains(text(), '" + selector + "')]";
18
- const result = document.evaluate(
19
- xpath,
20
- document,
21
- null,
22
- XPathResult.FIRST_ORDERED_NODE_TYPE,
23
- null
24
- );
25
- element = result.singleNodeValue;
26
- } else if (strategy === 'xpath') {
27
- // XPath selector
28
- const result = document.evaluate(
29
- selector,
30
- document,
31
- null,
32
- XPathResult.FIRST_ORDERED_NODE_TYPE,
33
- null
34
- );
35
- element = result.singleNodeValue;
36
- } else {
37
- // CSS selector (default)
38
- element = window.__MCP__.resolveRef(selector);
39
- }
11
+ var element = window.__MCP__.resolveRef(selector, strategy);
40
12
 
41
13
  if (element) {
42
14
  const outerHTML = element.outerHTML;
@@ -44,7 +16,10 @@
44
16
  const truncated = outerHTML.length > 5000
45
17
  ? outerHTML.substring(0, 5000) + '...'
46
18
  : outerHTML;
47
- return 'Found element: ' + truncated;
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;
48
23
  }
49
24
 
50
25
  return 'Element not found';
@@ -2,19 +2,23 @@
2
2
  * Focus an element
3
3
  *
4
4
  * @param {Object} params
5
- * @param {string} params.selector - CSS selector or ref ID (e.g., "ref=e3") for element to focus
5
+ * @param {string} params.selector - CSS selector, XPath, text, or ref ID (e.g., "ref=e3") for element to focus
6
+ * @param {string} params.strategy - Selector strategy: 'css', 'xpath', or 'text'
6
7
  */
7
8
  (function(params) {
8
- const { selector } = params;
9
+ const { selector, strategy } = params;
9
10
 
10
11
  function resolveElement(selectorOrRef) {
11
12
  if (!selectorOrRef) return null;
12
- var el = window.__MCP__.resolveRef(selectorOrRef);
13
+ var el = window.__MCP__.resolveRef(selectorOrRef, strategy);
13
14
  if (!el) throw new Error('Element not found: ' + selectorOrRef);
14
15
  return el;
15
16
  }
16
17
 
17
18
  const element = resolveElement(selector);
18
19
  element.focus();
19
- return `Focused element: ${selector}`;
20
+ var msg = 'Focused element: ' + selector;
21
+ var count = window.__MCP__.countAll(selector, strategy);
22
+ if (count > 1) msg += ' (+' + (count - 1) + ' more match' + (count - 1 === 1 ? '' : 'es') + ')';
23
+ return msg;
20
24
  })
@@ -2,36 +2,33 @@
2
2
  * Get computed CSS styles for elements
3
3
  *
4
4
  * @param {Object} params
5
- * @param {string} params.selector - CSS selector or ref ID (e.g., "ref=e3") for element(s)
5
+ * @param {string} params.selector - CSS selector, XPath, text, or ref ID (e.g., "ref=e3") for element(s)
6
+ * @param {string} params.strategy - Selector strategy: 'css', 'xpath', or 'text'
6
7
  * @param {string[]} params.properties - Specific CSS properties to retrieve
7
8
  * @param {boolean} params.multiple - Whether to get styles for all matching elements
8
9
  */
9
10
  (function(params) {
10
- const { selector, properties, multiple } = params;
11
+ const { selector, strategy, properties, multiple } = params;
11
12
 
12
- function resolveElement(selectorOrRef) {
13
- if (!selectorOrRef) return null;
14
- var el = window.__MCP__.resolveRef(selectorOrRef);
15
- if (!el) throw new Error('Element not found: ' + selectorOrRef);
16
- return el;
17
- }
13
+ var elements;
18
14
 
19
- // Check if selector is a ref ID - if so, multiple doesn't apply
20
- const isRef = /^\[?(?:ref=)?(e\d+)\]?$/.test(selector);
21
- const elements = isRef
22
- ? [resolveElement(selector)]
23
- : (multiple ? Array.from(document.querySelectorAll(selector)) : [document.querySelector(selector)]);
15
+ if (multiple) {
16
+ elements = window.__MCP__.resolveAll(selector, strategy);
17
+ } else {
18
+ var el = window.__MCP__.resolveRef(selector, strategy);
19
+ elements = el ? [el] : [];
20
+ }
24
21
 
25
22
  if (!elements[0]) {
26
- throw new Error(`Element not found: ${selector}`);
23
+ throw new Error('Element not found: ' + selector);
27
24
  }
28
25
 
29
- const results = elements.map(element => {
26
+ const results = elements.map(function(element) {
30
27
  const styles = window.getComputedStyle(element);
31
28
 
32
29
  if (properties.length > 0) {
33
30
  const result = {};
34
- properties.forEach(prop => {
31
+ properties.forEach(function(prop) {
35
32
  result[prop] = styles.getPropertyValue(prop);
36
33
  });
37
34
  return result;
@@ -22,7 +22,11 @@ export function getHtml2CanvasSource() {
22
22
  // Resolve the path to html2canvas-pro.js (UMD build)
23
23
  // Note: We use the main entry point since the minified version isn't exported
24
24
  const html2canvasProPath = require.resolve('html2canvas-pro');
25
- html2canvasProSource = readFileSync(html2canvasProPath, 'utf-8');
25
+ html2canvasProSource = readFileSync(html2canvasProPath, 'utf-8')
26
+ // Strip sourceMappingURL to prevent the browser from trying to fetch the
27
+ // .map file relative to the page's base URL (which fails when the app is
28
+ // served under a sub-path like '/some/path/').
29
+ .replace(/\/\/[#@]\s*sourceMappingURL=.*/g, '');
26
30
  }
27
31
  return html2canvasProSource;
28
32
  }
@@ -22,6 +22,7 @@ export const SCRIPTS = {
22
22
  focus: loadScript('focus'),
23
23
  findElement: loadScript('find-element'),
24
24
  domSnapshot: loadScript('dom-snapshot'),
25
+ elementPicker: loadScript('element-picker'),
25
26
  };
26
27
  /** Script ID used for resolve-ref in the script registry. */
27
28
  export const RESOLVE_REF_SCRIPT_ID = '__mcp_resolve_ref__';
@@ -41,14 +42,17 @@ export function buildScript(script, params) {
41
42
  /**
42
43
  * Build a script for typing text (uses the keyboard script's typeText function)
43
44
  */
44
- export function buildTypeScript(selector, text) {
45
+ export function buildTypeScript(selector, text, strategy) {
45
46
  const escapedText = text.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
47
+ const escapedSelector = selector.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
48
+ const strat = strategy || 'css';
46
49
  return `
47
50
  (function() {
48
- const selector = '${selector}';
51
+ const selector = '${escapedSelector}';
52
+ const strategy = '${strat}';
49
53
  const text = '${escapedText}';
50
54
 
51
- var element = window.__MCP__.resolveRef(selector);
55
+ var element = window.__MCP__.resolveRef(selector, strategy);
52
56
  if (!element) throw new Error('Element not found: ' + selector);
53
57
 
54
58
  element.focus();
@@ -56,7 +60,10 @@ export function buildTypeScript(selector, text) {
56
60
  element.dispatchEvent(new Event('input', { bubbles: true }));
57
61
  element.dispatchEvent(new Event('change', { bubbles: true }));
58
62
 
59
- return 'Typed "' + text + '" into ' + selector;
63
+ var msg = 'Typed "' + text + '" into ' + selector;
64
+ var count = window.__MCP__.countAll(selector, strategy);
65
+ if (count > 1) msg += ' (+' + (count - 1) + ' more match' + (count - 1 === 1 ? '' : 'es') + ')';
66
+ return msg;
60
67
  })()
61
68
  `;
62
69
  }
@@ -4,7 +4,8 @@
4
4
  *
5
5
  * @param {Object} params
6
6
  * @param {string} params.action - The action to perform
7
- * @param {string|null} params.selector - CSS selector or ref ID (e.g., "ref=e3") for the element
7
+ * @param {string|null} params.selector - CSS selector, XPath, text, or ref ID (e.g., "ref=e3") for the element
8
+ * @param {string} params.strategy - Selector strategy: 'css', 'xpath', or 'text'
8
9
  * @param {number|null} params.x - X coordinate
9
10
  * @param {number|null} params.y - Y coordinate
10
11
  * @param {number} params.duration - Duration for long-press
@@ -12,15 +13,22 @@
12
13
  * @param {number} params.scrollY - Vertical scroll amount
13
14
  */
14
15
  (function(params) {
15
- const { action, selector, x, y, duration, scrollX, scrollY } = params;
16
+ const { action, selector, strategy, x, y, duration, scrollX, scrollY } = params;
16
17
 
17
18
  function resolveElement(selectorOrRef) {
18
19
  if (!selectorOrRef) return null;
19
- var el = window.__MCP__.resolveRef(selectorOrRef);
20
+ var el = window.__MCP__.resolveRef(selectorOrRef, strategy);
20
21
  if (!el) throw new Error('Element not found: ' + selectorOrRef);
21
22
  return el;
22
23
  }
23
24
 
25
+ function matchHint() {
26
+ if (!selector) return '';
27
+ var count = window.__MCP__.countAll(selector, strategy);
28
+ if (count > 1) return ' (+' + (count - 1) + ' more match' + (count - 1 === 1 ? '' : 'es') + ')';
29
+ return '';
30
+ }
31
+
24
32
  let element = null;
25
33
  let targetX, targetY;
26
34
 
@@ -60,7 +68,7 @@
60
68
  element.dispatchEvent(new MouseEvent('mouseup', eventOptions));
61
69
  element.dispatchEvent(new MouseEvent('click', eventOptions));
62
70
  }
63
- return `Clicked at (${targetX}, ${targetY})`;
71
+ return `Clicked at (${targetX}, ${targetY})` + matchHint();
64
72
  }
65
73
 
66
74
  if (action === 'double-click') {
@@ -73,7 +81,7 @@
73
81
  element.dispatchEvent(new MouseEvent('click', eventOptions));
74
82
  element.dispatchEvent(new MouseEvent('dblclick', eventOptions));
75
83
  }
76
- return `Double-clicked at (${targetX}, ${targetY})`;
84
+ return `Double-clicked at (${targetX}, ${targetY})` + matchHint();
77
85
  }
78
86
 
79
87
  if (action === 'long-press') {
@@ -83,7 +91,7 @@
83
91
  element.dispatchEvent(new MouseEvent('mouseup', eventOptions));
84
92
  }, duration);
85
93
  }
86
- return `Long-pressed at (${targetX}, ${targetY}) for ${duration}ms`;
94
+ return `Long-pressed at (${targetX}, ${targetY}) for ${duration}ms` + matchHint();
87
95
  }
88
96
 
89
97
  if (action === 'scroll') {
@@ -95,7 +103,7 @@
95
103
  scrollTarget.scrollLeft += scrollX;
96
104
  scrollTarget.scrollTop += scrollY;
97
105
  }
98
- return `Scrolled by (${scrollX}, ${scrollY}) pixels`;
106
+ return `Scrolled by (${scrollX}, ${scrollY}) pixels` + matchHint();
99
107
  }
100
108
  return 'No scroll performed (scrollX and scrollY are both 0)';
101
109
  }