@hypothesi/tauri-mcp-server 0.12.0 → 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.
- package/dist/driver/app-discovery.d.ts +4 -2
- package/dist/driver/app-discovery.js +23 -3
- package/dist/driver/plugin-client.d.ts +23 -1
- package/dist/driver/plugin-client.js +127 -2
- package/dist/driver/plugin-commands.d.ts +8 -4
- package/dist/driver/plugin-commands.js +71 -16
- package/dist/driver/scripts/find-element.js +129 -16
- package/dist/driver/scripts/focus.js +31 -4
- package/dist/driver/scripts/html2canvas-loader.js +20 -2
- package/dist/driver/scripts/index.d.ts +2 -4
- package/dist/driver/scripts/index.js +40 -11
- package/dist/driver/scripts/interact.js +66 -5
- package/dist/driver/scripts/resolve-ref.js +101 -50
- package/dist/driver/session-manager.d.ts +1 -0
- package/dist/driver/session-manager.js +53 -6
- package/dist/driver/webview-executor.d.ts +20 -8
- package/dist/driver/webview-executor.js +159 -58
- package/dist/driver/webview-interactions.d.ts +56 -41
- package/dist/driver/webview-interactions.js +62 -40
- package/dist/monitor/logs.d.ts +14 -2
- package/dist/monitor/logs.js +37 -5
- package/dist/tools-registry.js +35 -33
- package/dist/version.d.ts +5 -0
- package/dist/version.js +16 -0
- package/package.json +1 -1
- package/dist/driver/scripts/get-styles.js +0 -47
- package/dist/driver/scripts/keyboard.js +0 -76
|
@@ -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
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
//
|
|
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,
|
|
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
|
|
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
|
|
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 =
|
|
52
|
-
const strategy =
|
|
53
|
-
const text =
|
|
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
|
-
|
|
56
|
-
|
|
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
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
})();
|
|
@@ -4,6 +4,7 @@ import { AppDiscovery } from './app-discovery.js';
|
|
|
4
4
|
import { PluginClient } from './plugin-client.js';
|
|
5
5
|
import { resetInitialization } from './webview-executor.js';
|
|
6
6
|
import { createMcpLogger } from '../logger.js';
|
|
7
|
+
import { PLUGIN_VERSION_FULL, describeVersionSkew } from '../version.js';
|
|
7
8
|
const sessionLogger = createMcpLogger('SESSION');
|
|
8
9
|
/**
|
|
9
10
|
* Session Manager - Native IPC-based session management
|
|
@@ -211,6 +212,30 @@ function promoteNextDefault() {
|
|
|
211
212
|
defaultPort = null;
|
|
212
213
|
}
|
|
213
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Return whether a cached session is responsive, removing it when stale.
|
|
217
|
+
*/
|
|
218
|
+
async function keepResponsiveSession(port) {
|
|
219
|
+
const session = activeSessions.get(port);
|
|
220
|
+
if (!session) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
if (await session.client.checkConnection()) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Treat probe errors like missed pongs and replace the stale client below.
|
|
230
|
+
}
|
|
231
|
+
session.client.disconnect();
|
|
232
|
+
activeSessions.delete(port);
|
|
233
|
+
if (port === defaultPort) {
|
|
234
|
+
promoteNextDefault();
|
|
235
|
+
}
|
|
236
|
+
sessionLogger.info(`Removed stale session: ${session.name} (${session.host}:${session.port})`);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
214
239
|
async function handleStatusAction() {
|
|
215
240
|
if (activeSessions.size === 0) {
|
|
216
241
|
return JSON.stringify({
|
|
@@ -219,6 +244,9 @@ async function handleStatusAction() {
|
|
|
219
244
|
identifier: null,
|
|
220
245
|
host: null,
|
|
221
246
|
port: null,
|
|
247
|
+
pluginVersion: null,
|
|
248
|
+
serverVersion: PLUGIN_VERSION_FULL,
|
|
249
|
+
versionWarning: describeVersionSkew(null),
|
|
222
250
|
});
|
|
223
251
|
}
|
|
224
252
|
if (activeSessions.size === 1) {
|
|
@@ -230,8 +258,12 @@ async function handleStatusAction() {
|
|
|
230
258
|
identifier: null,
|
|
231
259
|
host: null,
|
|
232
260
|
port: null,
|
|
261
|
+
pluginVersion: null,
|
|
262
|
+
serverVersion: PLUGIN_VERSION_FULL,
|
|
263
|
+
versionWarning: describeVersionSkew(null),
|
|
233
264
|
});
|
|
234
265
|
}
|
|
266
|
+
const versionWarning = describeVersionSkew(session.pluginVersion);
|
|
235
267
|
return JSON.stringify({
|
|
236
268
|
connected: true,
|
|
237
269
|
app: session.name,
|
|
@@ -239,15 +271,22 @@ async function handleStatusAction() {
|
|
|
239
271
|
cwd: session.cwd,
|
|
240
272
|
host: session.host,
|
|
241
273
|
port: session.port,
|
|
274
|
+
pluginVersion: session.pluginVersion,
|
|
275
|
+
serverVersion: PLUGIN_VERSION_FULL,
|
|
276
|
+
versionWarning,
|
|
242
277
|
});
|
|
243
278
|
}
|
|
244
279
|
const apps = Array.from(activeSessions.values()).map((session) => {
|
|
280
|
+
const versionWarning = describeVersionSkew(session.pluginVersion);
|
|
245
281
|
return {
|
|
246
282
|
name: session.name,
|
|
247
283
|
identifier: session.identifier,
|
|
248
284
|
cwd: session.cwd,
|
|
249
285
|
host: session.host,
|
|
250
286
|
port: session.port,
|
|
287
|
+
pluginVersion: session.pluginVersion,
|
|
288
|
+
serverVersion: PLUGIN_VERSION_FULL,
|
|
289
|
+
versionWarning,
|
|
251
290
|
isDefault: session.port === defaultPort,
|
|
252
291
|
};
|
|
253
292
|
});
|
|
@@ -256,12 +295,13 @@ async function handleStatusAction() {
|
|
|
256
295
|
apps,
|
|
257
296
|
totalCount: apps.length,
|
|
258
297
|
defaultPort,
|
|
298
|
+
serverVersion: PLUGIN_VERSION_FULL,
|
|
259
299
|
});
|
|
260
300
|
}
|
|
261
301
|
async function handleStartAction(host, port) {
|
|
262
302
|
const configuredHost = host ?? getDefaultHost();
|
|
263
303
|
const configuredPort = port ?? getDefaultPort();
|
|
264
|
-
if (
|
|
304
|
+
if (await keepResponsiveSession(configuredPort)) {
|
|
265
305
|
return `Already connected to app on port ${configuredPort}`;
|
|
266
306
|
}
|
|
267
307
|
let connectedSession = null;
|
|
@@ -296,12 +336,12 @@ async function handleStartAction(host, port) {
|
|
|
296
336
|
if (!connectedSession) {
|
|
297
337
|
return `Session start failed - no Tauri app found at localhost or ${configuredHost}:${configuredPort}`;
|
|
298
338
|
}
|
|
299
|
-
if (
|
|
339
|
+
if (await keepResponsiveSession(connectedSession.port)) {
|
|
300
340
|
return `Already connected to app on port ${connectedSession.port}`;
|
|
301
341
|
}
|
|
302
342
|
const client = new PluginClient(connectedSession.host, connectedSession.port);
|
|
303
343
|
await client.connect();
|
|
304
|
-
const { identifier, cwd } = await fetchAppMetadata(client);
|
|
344
|
+
const { identifier, cwd, pluginVersion } = await fetchAppMetadata(client);
|
|
305
345
|
const sessionInfo = {
|
|
306
346
|
name: connectedSession.name,
|
|
307
347
|
identifier,
|
|
@@ -310,11 +350,17 @@ async function handleStartAction(host, port) {
|
|
|
310
350
|
port: connectedSession.port,
|
|
311
351
|
client,
|
|
312
352
|
connected: true,
|
|
353
|
+
pluginVersion,
|
|
313
354
|
};
|
|
314
355
|
activeSessions.set(connectedSession.port, sessionInfo);
|
|
315
356
|
defaultPort = connectedSession.port;
|
|
316
357
|
sessionLogger.info(`Session started: ${sessionInfo.name} (${sessionInfo.host}:${sessionInfo.port}) [DEFAULT]`);
|
|
317
|
-
|
|
358
|
+
let result = `Session started with app: ${sessionInfo.name} (${sessionInfo.host}:${sessionInfo.port}) [DEFAULT]`;
|
|
359
|
+
const versionWarning = describeVersionSkew(sessionInfo.pluginVersion);
|
|
360
|
+
if (versionWarning) {
|
|
361
|
+
result = `${versionWarning}\n\n${result}`;
|
|
362
|
+
}
|
|
363
|
+
return result;
|
|
318
364
|
}
|
|
319
365
|
async function handleStopAction(appIdentifier) {
|
|
320
366
|
if (appIdentifier !== undefined) {
|
|
@@ -376,16 +422,17 @@ async function fetchAppMetadata(client) {
|
|
|
376
422
|
args: { command: 'plugin:mcp-bridge|get_backend_state', args: {} },
|
|
377
423
|
});
|
|
378
424
|
if (!response.success || !response.data) {
|
|
379
|
-
return { identifier: null, cwd: null };
|
|
425
|
+
return { identifier: null, cwd: null, pluginVersion: null };
|
|
380
426
|
}
|
|
381
427
|
const state = response.data;
|
|
382
428
|
return {
|
|
383
429
|
identifier: state.app?.identifier ?? null,
|
|
384
430
|
cwd: typeof state.cwd === 'string' && state.cwd.length > 0 ? state.cwd : null,
|
|
431
|
+
pluginVersion: state.bridge?.pluginVersion ?? null,
|
|
385
432
|
};
|
|
386
433
|
}
|
|
387
434
|
catch {
|
|
388
|
-
return { identifier: null, cwd: null };
|
|
435
|
+
return { identifier: null, cwd: null, pluginVersion: null };
|
|
389
436
|
}
|
|
390
437
|
}
|
|
391
438
|
/**
|