@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.
- package/README.md +4 -2
- package/dist/driver/element-picker.js +272 -0
- package/dist/driver/scripts/dom-snapshot.js +13 -14
- package/dist/driver/scripts/element-picker.js +395 -0
- package/dist/driver/scripts/find-element.js +5 -30
- package/dist/driver/scripts/focus.js +8 -4
- package/dist/driver/scripts/get-styles.js +13 -16
- package/dist/driver/scripts/html2canvas-loader.js +5 -1
- package/dist/driver/scripts/index.js +11 -4
- package/dist/driver/scripts/interact.js +15 -7
- package/dist/driver/scripts/resolve-ref.js +92 -3
- package/dist/driver/scripts/wait-for.js +12 -8
- package/dist/driver/webview-interactions.js +35 -17
- package/dist/prompts-registry.js +46 -0
- package/dist/tools-registry.js +71 -110
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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(
|
|
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 = '${
|
|
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
|
-
|
|
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
|
}
|