@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,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { writeFile } from 'node:fs/promises';
3
3
  import { resolve } from 'node:path';
4
- import { executeInWebview, executeInWebviewWithContext, captureScreenshot, getConsoleLogs as getConsoleLogsFromCapture, } from './webview-executor.js';
4
+ import { executeInWebview, executeInWebviewWithContext, executeAsyncInWebview, captureScreenshot, getConsoleLogs as getConsoleLogsFromCapture, } from './webview-executor.js';
5
5
  import { SCRIPTS, buildScript, buildTypeScript, buildKeyEventScript } from './scripts/index.js';
6
6
  // ============================================================================
7
7
  // Base Schema for Window Targeting
@@ -24,14 +24,21 @@ export const WindowTargetSchema = z.object({
24
24
  const selectorStrategyField = z.enum(['css', 'xpath', 'text']).default('css').describe('Selector strategy: "css" (default) for CSS selectors, "xpath" for XPath expressions, ' +
25
25
  '"text" to find elements by text content, with fallback to placeholder, aria-label, ' +
26
26
  'and title attributes. Ref IDs (e.g., "ref=e3") work with any strategy.');
27
+ /**
28
+ * Optional nth parameter for disambiguating multiple matches.
29
+ * Required when a text selector matches more than one visible element.
30
+ */
31
+ const nthField = z.number().int().min(0).optional().describe('Zero-based index to pick one of several matching elements. ' +
32
+ 'Omit when the selector is unique. Required when a text selector matches more than one visible element.');
27
33
  // ============================================================================
28
34
  // Schemas
29
35
  // ============================================================================
30
36
  export const InteractSchema = WindowTargetSchema.extend({
31
- action: z.enum(['click', 'double-click', 'long-press', 'scroll', 'swipe', 'focus'])
37
+ action: z.enum(['click', 'double-click', 'long-press', 'scroll', 'swipe', 'focus', 'hover', 'right-click'])
32
38
  .describe('Type of interaction to perform'),
33
39
  selector: z.string().optional().describe('Element selector: CSS selector (default), XPath expression, text content, or ref ID (e.g., "ref=e3")'),
34
40
  strategy: selectorStrategyField,
41
+ nth: nthField,
35
42
  x: z.number().optional().describe('X coordinate for direct coordinate interaction'),
36
43
  y: z.number().optional().describe('Y coordinate for direct coordinate interaction'),
37
44
  duration: z.number().optional()
@@ -49,6 +56,7 @@ export const ScreenshotSchema = WindowTargetSchema.extend({
49
56
  filePath: z.string().optional().describe('File path to save the screenshot to instead of returning as base64'),
50
57
  maxWidth: z.number().int().positive().optional().describe('Maximum width in pixels. Images wider than this will be scaled down proportionally. ' +
51
58
  'Can also be set via TAURI_MCP_SCREENSHOT_MAX_WIDTH environment variable.'),
59
+ allowScreenCapture: z.boolean().optional().default(false).describe('Allow an interactive OS screen-sharing permission prompt if native and html2canvas capture fail'),
52
60
  });
53
61
  export const KeyboardSchema = WindowTargetSchema.extend({
54
62
  action: z.enum(['type', 'press', 'down', 'up'])
@@ -56,6 +64,7 @@ export const KeyboardSchema = WindowTargetSchema.extend({
56
64
  selector: z.string().optional().describe('Element selector for element to type into (required for "type" action): ' +
57
65
  'CSS selector (default), XPath, text content, or ref ID'),
58
66
  strategy: selectorStrategyField,
67
+ nth: nthField,
59
68
  text: z.string().optional().describe('Text to type (required for "type" action)'),
60
69
  key: z.string().optional().describe('Key to press (required for "press/down/up" actions, e.g., "Enter", "a", "Escape")'),
61
70
  modifiers: z.array(z.enum(['Control', 'Alt', 'Shift', 'Meta'])).optional().describe('Modifier keys to hold'),
@@ -66,26 +75,30 @@ export const WaitForSchema = WindowTargetSchema.extend({
66
75
  strategy: selectorStrategyField.describe('Selector strategy (applies when type is "selector"): "css" (default), "xpath", or "text".'),
67
76
  timeout: z.number().optional().default(5000).describe('Timeout in milliseconds (default: 5000ms)'),
68
77
  });
69
- export const GetStylesSchema = WindowTargetSchema.extend({
70
- selector: z.string().describe('Element selector: CSS selector (default), XPath expression, text content, or ref ID'),
71
- strategy: selectorStrategyField,
72
- properties: z.array(z.string()).optional().describe('Specific CSS properties to retrieve. If omitted, returns all computed styles'),
73
- multiple: z.boolean().optional().default(false)
74
- .describe('Whether to get styles for all matching elements (true) or just the first (false)'),
75
- });
76
78
  export const ExecuteJavaScriptSchema = WindowTargetSchema.extend({
77
79
  script: z.string().describe('JavaScript code to execute in the webview context. ' +
78
80
  'If returning a value, it must be JSON-serializable. ' +
79
81
  'For functions that return values, use IIFE syntax: "(() => { return value; })()" not "() => { return value; }"'),
80
82
  args: z.array(z.unknown()).optional().describe('Arguments to pass to the script'),
83
+ timeout: z.number().int().positive().optional().describe('Execution timeout in milliseconds'),
81
84
  });
82
85
  export const FocusElementSchema = WindowTargetSchema.extend({
83
86
  selector: z.string().describe('CSS selector for element to focus'),
87
+ strategy: selectorStrategyField,
88
+ nth: nthField,
84
89
  });
85
90
  export const FindElementSchema = WindowTargetSchema.extend({
86
91
  selector: z.string().describe('The selector to find: CSS selector (default), XPath expression, text content, or ref ID (e.g., "ref=e3"). ' +
87
92
  'Interpretation depends on strategy.'),
88
93
  strategy: selectorStrategyField,
94
+ nth: nthField,
95
+ limit: z.number().int().min(1).max(50).optional().default(5)
96
+ .describe('Maximum number of matches to return. Total match count is always reported.'),
97
+ properties: z.array(z.string()).optional().describe('Computed CSS properties to include for each match, e.g. ["display","opacity","font-size"].'),
98
+ visibleOnly: z.boolean().optional().default(true)
99
+ .describe('Return only rendered elements. Set false to include display:none and off-screen nodes.'),
100
+ includeHtml: z.boolean().optional().default(false)
101
+ .describe('Include outerHTML for each match, truncated to 2000 characters.'),
89
102
  });
90
103
  export const GetConsoleLogsSchema = WindowTargetSchema.extend({
91
104
  filter: z.string().optional().describe('Regex or keyword to filter logs'),
@@ -100,7 +113,7 @@ export const DomSnapshotSchema = WindowTargetSchema.extend({
100
113
  // Implementation Functions
101
114
  // ============================================================================
102
115
  export async function interact(options) {
103
- const { action, selector, strategy, x, y, duration, scrollX, scrollY, fromX, fromY, toX, toY, windowId, appIdentifier } = options;
116
+ const { action, selector, strategy, nth, x, y, duration, scrollX, scrollY, fromX, fromY, toX, toY, windowId, appIdentifier } = options;
104
117
  // Handle swipe action separately since it has different logic
105
118
  if (action === 'swipe') {
106
119
  return performSwipe({ fromX, fromY, toX, toY, duration, windowId, appIdentifier });
@@ -110,12 +123,13 @@ export async function interact(options) {
110
123
  if (!selector) {
111
124
  throw new Error('Focus action requires a selector');
112
125
  }
113
- return focusElement({ selector, strategy, windowId, appIdentifier });
126
+ return focusElement({ selector, strategy, nth, windowId, appIdentifier });
114
127
  }
115
128
  const script = buildScript(SCRIPTS.interact, {
116
129
  action,
117
130
  selector: selector ?? null,
118
131
  strategy: strategy ?? 'css',
132
+ nth: nth ?? null,
119
133
  x: x ?? null,
120
134
  y: y ?? null,
121
135
  duration: duration ?? 500,
@@ -145,9 +159,11 @@ async function performSwipe(options) {
145
159
  }
146
160
  }
147
161
  export async function screenshot(options = {}) {
148
- const { quality, format = 'jpeg', windowId, filePath, appIdentifier, maxWidth } = options;
162
+ const { quality, format = 'jpeg', windowId, filePath, appIdentifier, maxWidth, allowScreenCapture } = options;
149
163
  // Use the native screenshot function from webview-executor
150
- const result = await captureScreenshot({ format, quality, windowId, appIdentifier, maxWidth });
164
+ const result = await captureScreenshot({
165
+ format, quality, windowId, appIdentifier, maxWidth, allowScreenCapture,
166
+ });
151
167
  // If filePath is provided, write to file instead of returning base64
152
168
  if (filePath) {
153
169
  // Find the image content in the result
@@ -162,15 +178,15 @@ export async function screenshot(options = {}) {
162
178
  return result;
163
179
  }
164
180
  export async function keyboard(options) {
165
- const { action, selectorOrKey, strategy, textOrModifiers, modifiers, windowId, appIdentifier } = options;
181
+ const { action, selectorOrKey, strategy, nth, textOrModifiers, modifiers, windowId, appIdentifier } = options;
166
182
  // Handle the different parameter combinations based on action
167
183
  if (action === 'type') {
168
184
  const selector = selectorOrKey;
169
185
  const text = textOrModifiers;
170
- if (!selector || !text) {
186
+ if (!selector || text === undefined) {
171
187
  throw new Error('Type action requires both selector and text parameters');
172
188
  }
173
- const script = buildTypeScript(selector, text, strategy);
189
+ const script = buildTypeScript(selector, text, strategy, nth);
174
190
  try {
175
191
  return await executeInWebview(script, windowId, appIdentifier);
176
192
  }
@@ -205,24 +221,8 @@ export async function waitFor(options) {
205
221
  throw new Error(`Wait failed: ${message}`);
206
222
  }
207
223
  }
208
- export async function getStyles(options) {
209
- const { selector, strategy, properties, multiple = false, windowId, appIdentifier } = options;
210
- const script = buildScript(SCRIPTS.getStyles, {
211
- selector,
212
- strategy: strategy ?? 'css',
213
- properties: properties || [],
214
- multiple,
215
- });
216
- try {
217
- return await executeInWebview(script, windowId, appIdentifier);
218
- }
219
- catch (error) {
220
- const message = error instanceof Error ? error.message : String(error);
221
- throw new Error(`Get styles failed: ${message}`);
222
- }
223
- }
224
224
  export async function executeJavaScript(options) {
225
- const { script, args, windowId, appIdentifier } = options;
225
+ const { script, args, timeout, windowId, appIdentifier } = options;
226
226
  // If args are provided, we need to inject them into the script context
227
227
  const wrappedScript = args && args.length > 0
228
228
  ? `
@@ -233,7 +233,13 @@ export async function executeJavaScript(options) {
233
233
  `
234
234
  : script;
235
235
  try {
236
- const { result, windowLabel, warning } = await executeInWebviewWithContext(wrappedScript, windowId, appIdentifier);
236
+ const execution = timeout === undefined
237
+ ? await executeInWebviewWithContext(wrappedScript, windowId, appIdentifier)
238
+ : {
239
+ result: await executeAsyncInWebview(buildTimedScript(wrappedScript), windowId, timeout, appIdentifier),
240
+ windowLabel: windowId ?? 'main',
241
+ };
242
+ const { result, windowLabel, warning } = execution;
237
243
  // Build response with window context
238
244
  let response = result;
239
245
  if (warning) {
@@ -248,9 +254,16 @@ export async function executeJavaScript(options) {
248
254
  throw new Error(`JavaScript execution failed: ${message}`);
249
255
  }
250
256
  }
257
+ function buildTimedScript(script) {
258
+ const trimmed = script.trim();
259
+ if (/\breturn\b/.test(trimmed) && !trimmed.startsWith('(')) {
260
+ return `return (async () => { ${trimmed} })();`;
261
+ }
262
+ return `return (${trimmed});`;
263
+ }
251
264
  export async function focusElement(options) {
252
- const { selector, strategy, windowId, appIdentifier } = options;
253
- const script = buildScript(SCRIPTS.focus, { selector, strategy: strategy ?? 'css' });
265
+ const { selector, strategy, nth, windowId, appIdentifier } = options;
266
+ const script = buildScript(SCRIPTS.focus, { selector, strategy: strategy ?? 'css', nth: nth ?? null });
254
267
  try {
255
268
  return await executeInWebview(script, windowId, appIdentifier);
256
269
  }
@@ -263,8 +276,17 @@ export async function focusElement(options) {
263
276
  * Find an element using various selector strategies.
264
277
  */
265
278
  export async function findElement(options) {
266
- const { selector, strategy, windowId, appIdentifier } = options;
267
- const script = buildScript(SCRIPTS.findElement, { selector, strategy });
279
+ const { selector, strategy, nth, limit, properties, visibleOnly, includeHtml, windowId, appIdentifier } = options;
280
+ await ensureAriaApiLoaded(windowId, appIdentifier);
281
+ const script = buildScript(SCRIPTS.findElement, {
282
+ selector,
283
+ strategy,
284
+ nth: nth ?? null,
285
+ limit: limit ?? 5,
286
+ properties: properties || [],
287
+ visibleOnly: visibleOnly ?? true,
288
+ includeHtml: includeHtml ?? false,
289
+ });
268
290
  try {
269
291
  return await executeInWebview(script, windowId, appIdentifier);
270
292
  }
@@ -277,9 +299,9 @@ export async function findElement(options) {
277
299
  * Get console logs from the webview.
278
300
  */
279
301
  export async function getConsoleLogs(options = {}) {
280
- const { filter, since, windowId, appIdentifier } = options;
302
+ const { lines = 50, level, maxChars = 20000, maxCharsPerEntry = 2000, filter, since, windowId, appIdentifier } = options;
281
303
  try {
282
- return await getConsoleLogsFromCapture(filter, since, windowId, appIdentifier);
304
+ return await getConsoleLogsFromCapture({ filter, since, lines, windowId, appIdentifier, level, maxChars, maxCharsPerEntry });
283
305
  }
284
306
  catch (error) {
285
307
  const message = error instanceof Error ? error.message : String(error);
@@ -1,9 +1,10 @@
1
1
  import { z } from 'zod';
2
2
  import { execa } from 'execa';
3
3
  export const ListDevicesSchema = z.object({});
4
+ const DEVICE_LIST_TIMEOUT_MS = 5000;
4
5
  async function getAndroidDevices() {
5
6
  try {
6
- const { stdout } = await execa('adb', ['devices', '-l']);
7
+ const { stdout } = await execa('adb', ['devices', '-l'], { timeout: DEVICE_LIST_TIMEOUT_MS });
7
8
  return stdout
8
9
  .split('\n')
9
10
  .slice(1)
@@ -20,7 +21,7 @@ async function getIOSSimulators() {
20
21
  return [];
21
22
  }
22
23
  try {
23
- const { stdout } = await execa('xcrun', ['simctl', 'list', 'devices', 'booted']);
24
+ const { stdout } = await execa('xcrun', ['simctl', 'list', 'devices', 'booted'], { timeout: DEVICE_LIST_TIMEOUT_MS });
24
25
  return stdout
25
26
  .split('\n')
26
27
  .filter((line) => { return line.trim().length > 0 && !line.includes('== Devices =='); });
@@ -2,28 +2,40 @@ import { z } from 'zod';
2
2
  export declare const ReadLogsSchema: z.ZodObject<{
3
3
  source: z.ZodEnum<["console", "android", "ios", "system"]>;
4
4
  lines: z.ZodDefault<z.ZodNumber>;
5
+ level: z.ZodOptional<z.ZodEnum<["log", "debug", "info", "warn", "error"]>>;
6
+ maxChars: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
7
+ maxCharsPerEntry: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
5
8
  filter: z.ZodOptional<z.ZodString>;
6
9
  since: z.ZodOptional<z.ZodString>;
7
10
  windowId: z.ZodOptional<z.ZodString>;
8
11
  appIdentifier: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
9
12
  }, "strip", z.ZodTypeAny, {
10
13
  lines: number;
14
+ maxChars: number;
15
+ maxCharsPerEntry: number;
11
16
  source: "android" | "ios" | "console" | "system";
12
17
  filter?: string | undefined;
18
+ since?: string | undefined;
13
19
  windowId?: string | undefined;
14
20
  appIdentifier?: string | number | undefined;
15
- since?: string | undefined;
21
+ level?: "error" | "log" | "debug" | "info" | "warn" | undefined;
16
22
  }, {
17
23
  source: "android" | "ios" | "console" | "system";
18
24
  filter?: string | undefined;
19
25
  lines?: number | undefined;
26
+ since?: string | undefined;
20
27
  windowId?: string | undefined;
21
28
  appIdentifier?: string | number | undefined;
22
- since?: string | undefined;
29
+ level?: "error" | "log" | "debug" | "info" | "warn" | undefined;
30
+ maxChars?: number | undefined;
31
+ maxCharsPerEntry?: number | undefined;
23
32
  }>;
24
33
  export interface ReadLogsOptions {
25
34
  source: 'console' | 'android' | 'ios' | 'system';
26
35
  lines?: number;
36
+ level?: 'log' | 'debug' | 'info' | 'warn' | 'error';
37
+ maxChars?: number;
38
+ maxCharsPerEntry?: number;
27
39
  filter?: string;
28
40
  since?: string;
29
41
  windowId?: string;
@@ -47,19 +47,25 @@ function findAdbPath() {
47
47
  export const ReadLogsSchema = z.object({
48
48
  source: z.enum(['console', 'android', 'ios', 'system'])
49
49
  .describe('Log source: "console" for webview JS logs, "android" for logcat, "ios" for simulator, "system" for desktop'),
50
- lines: z.number().default(50),
50
+ lines: z.number().int().positive().default(50),
51
+ level: z.enum(['log', 'debug', 'info', 'warn', 'error']).optional()
52
+ .describe('Only return entries at this console level.'),
53
+ maxChars: z.number().int().min(500).max(100000).optional().default(20000)
54
+ .describe('Maximum characters in the response. Entries are truncated and the oldest are dropped to fit.'),
55
+ maxCharsPerEntry: z.number().int().min(100).max(20000).optional().default(2000)
56
+ .describe('Maximum characters kept from each log entry.'),
51
57
  filter: z.string().optional().describe('Regex or keyword to filter logs'),
52
58
  since: z.string().optional().describe('ISO timestamp to filter logs since (e.g. 2023-10-27T10:00:00Z)'),
53
59
  windowId: z.string().optional().describe('Window label for console logs (defaults to "main")'),
54
60
  appIdentifier: z.union([z.string(), z.number()]).optional().describe('App port or bundle ID for console logs. Defaults to the only connected app or the default app if multiple are connected.'),
55
61
  });
56
62
  export async function readLogs(options) {
57
- const { source, lines = 50, filter, since, windowId, appIdentifier } = options;
63
+ const { source, lines = 50, level, maxChars = 20000, maxCharsPerEntry = 2000, filter, since, windowId, appIdentifier } = options;
58
64
  try {
59
65
  let output = '';
60
66
  // Handle console logs (webview JS logs)
61
67
  if (source === 'console') {
62
- return await getConsoleLogs({ filter, since, windowId, appIdentifier });
68
+ return await getConsoleLogs({ lines, level, maxChars, maxCharsPerEntry, filter, since, windowId, appIdentifier });
63
69
  }
64
70
  if (source === 'android') {
65
71
  const adbPath = findAdbPath();
@@ -134,16 +140,42 @@ export async function readLogs(options) {
134
140
  return `Error reading system logs: ${e}`;
135
141
  }
136
142
  }
143
+ // Apply level filter
144
+ if (level) {
145
+ const levelPrefix = `[ ${level.toUpperCase()} ]`;
146
+ output = output.split('\n').filter((line) => { return line.includes(levelPrefix); }).join('\n');
147
+ }
148
+ // Apply regex filter
137
149
  if (filter) {
138
150
  try {
139
151
  const regex = new RegExp(filter, 'i');
140
- return output.split('\n').filter((line) => { return regex.test(line); }).join('\n');
152
+ output = output.split('\n').filter((line) => { return regex.test(line); }).join('\n');
141
153
  }
142
154
  catch (e) {
143
155
  return `Invalid filter regex: ${e}`;
144
156
  }
145
157
  }
146
- return output;
158
+ // Apply character budget
159
+ const outputLines = output.split('\n');
160
+ let budget = maxChars, dropped = 0;
161
+ const finalLines = [];
162
+ for (let i = outputLines.length - 1; i >= 0; i--) {
163
+ let line = outputLines[i];
164
+ if (line.length > maxCharsPerEntry) {
165
+ line = line.slice(0, maxCharsPerEntry) + '…[' + (line.length - maxCharsPerEntry) + ' more chars]';
166
+ }
167
+ if (line.length > budget) {
168
+ dropped = i + 1;
169
+ break;
170
+ }
171
+ budget -= line.length + 1; // +1 for newline
172
+ finalLines.unshift(line);
173
+ }
174
+ if (dropped > 0) {
175
+ finalLines.unshift('[ ' + dropped + ' older entries dropped to fit maxChars=' + maxChars +
176
+ '. Narrow with level, filter, or since. ]');
177
+ }
178
+ return finalLines.join('\n');
147
179
  }
148
180
  catch (error) {
149
181
  return `Error reading logs: ${error}`;
@@ -7,7 +7,7 @@ import { listDevices, ListDevicesSchema } from './manager/mobile.js';
7
7
  import { manageDriverSession, ManageDriverSessionSchema, } from './driver/session-manager.js';
8
8
  import { readLogs, ReadLogsSchema } from './monitor/logs.js';
9
9
  import { executeIPCCommand, manageIPCMonitoring, getIPCEvents, emitTestEvent, getBackendState, manageWindow, ExecuteIPCCommandSchema, ManageIPCMonitoringSchema, GetIPCEventsSchema, EmitTestEventSchema, GetBackendStateSchema, ManageWindowSchema, } from './driver/plugin-commands.js';
10
- import { interact, screenshot, keyboard, waitFor, getStyles, executeJavaScript, findElement, domSnapshot, InteractSchema, ScreenshotSchema, KeyboardSchema, WaitForSchema, GetStylesSchema, ExecuteJavaScriptSchema, FindElementSchema, DomSnapshotSchema, } from './driver/webview-interactions.js';
10
+ import { interact, screenshot, keyboard, waitFor, executeJavaScript, findElement, domSnapshot, InteractSchema, ScreenshotSchema, KeyboardSchema, WaitForSchema, ExecuteJavaScriptSchema, FindElementSchema, DomSnapshotSchema, } from './driver/webview-interactions.js';
11
11
  import { selectElement, getPointedElement, SelectElementSchema, GetPointedElementSchema, } from './driver/element-picker.js';
12
12
  import { SETUP_INSTRUCTIONS } from './constants.js';
13
13
  /**
@@ -99,10 +99,16 @@ export const TOOLS = [
99
99
  },
100
100
  {
101
101
  name: 'webview_find_element',
102
- description: '[Tauri Apps Only] Find DOM elements in a running Tauri app\'s webview. ' +
102
+ description: '[Tauri Apps Only] Find elements in a running Tauri app\'s webview and return their geometry, text, ' +
103
+ 'accessible name, attributes, visibility, and optionally their computed styles. ' +
103
104
  'Supports CSS selectors (default), XPath expressions, and text content matching via the strategy parameter. ' +
104
- 'The "text" strategy first searches element text content, then falls back to placeholder, aria-label, and title attributes. ' +
105
- 'Returns the element\'s HTML. ' +
105
+ 'The "text" strategy searches element text content (not just direct text nodes), prefers exact matches over ' +
106
+ 'substring matches, prefers innermost elements, and prefers interactive ancestors. ' +
107
+ 'Returns structured JSON with totalMatches, returned count, and array of matches each containing: ' +
108
+ 'nth index, ref ID, tag, trimmed text, accessible name, attributes, bounding rect, visibility, interactability, ' +
109
+ 'optional computed styles, and optional truncated outerHTML. ' +
110
+ 'Use this instead of webview_execute_js for querySelector, getBoundingClientRect, textContent, ' +
111
+ 'and getComputedStyle reads. ' +
106
112
  'Requires active driver_session. ' +
107
113
  MULTI_APP_DESC + ' ' +
108
114
  'For browser pages or documentation sites, use Chrome DevTools MCP instead.',
@@ -118,6 +124,11 @@ export const TOOLS = [
118
124
  return await findElement({
119
125
  selector: parsed.selector,
120
126
  strategy: parsed.strategy,
127
+ nth: parsed.nth,
128
+ limit: parsed.limit,
129
+ properties: parsed.properties,
130
+ visibleOnly: parsed.visibleOnly,
131
+ includeHtml: parsed.includeHtml,
121
132
  windowId: parsed.windowId,
122
133
  appIdentifier: parsed.appIdentifier,
123
134
  });
@@ -144,9 +155,11 @@ export const TOOLS = [
144
155
  // WebView Interaction Tools
145
156
  {
146
157
  name: 'webview_interact',
147
- description: '[Tauri Apps Only] Click, scroll, swipe, focus, or perform gestures in a Tauri app webview. ' +
148
- 'Supported actions: click, double-click, long-press, scroll, swipe, focus. ' +
158
+ description: '[Tauri Apps Only] Click, hover, right-click, scroll, swipe, focus, or perform gestures in a Tauri app webview. ' +
159
+ 'Supported actions: click, double-click, long-press, scroll, swipe, focus, hover, right-click. ' +
149
160
  'Supports CSS selectors (default), XPath, and text content matching via the strategy parameter. ' +
161
+ 'When using a text selector that matches multiple visible elements, you must provide the nth parameter ' +
162
+ 'to disambiguate, or the tool will throw an error listing the candidates. ' +
150
163
  'Requires active driver_session. ' +
151
164
  'For browser interaction, use Chrome DevTools MCP instead.',
152
165
  category: TOOL_CATEGORIES.UI_AUTOMATION,
@@ -185,6 +198,7 @@ export const TOOLS = [
185
198
  filePath: parsed.filePath,
186
199
  appIdentifier: parsed.appIdentifier,
187
200
  maxWidth: parsed.maxWidth,
201
+ allowScreenCapture: parsed.allowScreenCapture,
188
202
  });
189
203
  // If saved to file, return text confirmation
190
204
  if ('filePath' in result) {
@@ -217,6 +231,7 @@ export const TOOLS = [
217
231
  action: parsed.action,
218
232
  selectorOrKey: parsed.selector,
219
233
  strategy: parsed.strategy,
234
+ nth: parsed.nth,
220
235
  textOrModifiers: parsed.text,
221
236
  windowId: parsed.windowId,
222
237
  appIdentifier: parsed.appIdentifier,
@@ -257,38 +272,14 @@ export const TOOLS = [
257
272
  });
258
273
  },
259
274
  },
260
- {
261
- name: 'webview_get_styles',
262
- description: '[Tauri Apps Only] Get computed CSS styles from elements in a Tauri app. ' +
263
- 'Supports CSS selectors (default), XPath, and text content matching via the strategy parameter. ' +
264
- 'Requires active driver_session. ' +
265
- MULTI_APP_DESC + ' ' +
266
- 'For browser style inspection, use Chrome DevTools MCP instead.',
267
- category: TOOL_CATEGORIES.UI_AUTOMATION,
268
- schema: GetStylesSchema,
269
- annotations: {
270
- title: 'Get Styles in Tauri Webview',
271
- readOnlyHint: true,
272
- openWorldHint: false,
273
- },
274
- handler: async (args) => {
275
- const parsed = GetStylesSchema.parse(args);
276
- return await getStyles({
277
- selector: parsed.selector,
278
- strategy: parsed.strategy,
279
- properties: parsed.properties,
280
- multiple: parsed.multiple,
281
- windowId: parsed.windowId,
282
- appIdentifier: parsed.appIdentifier,
283
- });
284
- },
285
- },
286
275
  {
287
276
  name: 'webview_execute_js',
288
277
  description: '[Tauri Apps Only] Execute JavaScript in a Tauri app\'s webview context. ' +
289
278
  'Requires active driver_session. Has access to window.__TAURI__. ' +
290
279
  'If you need a return value, it must be JSON-serializable. ' +
291
280
  'For functions that return values, use an IIFE: "(() => { return 5; })()" not "() => { return 5; }". ' +
281
+ 'Accepts an optional timeout parameter (milliseconds) for long-running scripts. ' +
282
+ 'Timeout errors will identify which layer timed out (transport, script self-race, or Rust eval). ' +
292
283
  MULTI_APP_DESC + ' ' +
293
284
  'For browser JS execution, use Chrome DevTools MCP instead.',
294
285
  category: TOOL_CATEGORIES.UI_AUTOMATION,
@@ -304,6 +295,7 @@ export const TOOLS = [
304
295
  return await executeJavaScript({
305
296
  script: parsed.script,
306
297
  args: parsed.args,
298
+ timeout: parsed.timeout,
307
299
  windowId: parsed.windowId,
308
300
  appIdentifier: parsed.appIdentifier,
309
301
  });
@@ -396,6 +388,9 @@ export const TOOLS = [
396
388
  name: 'ipc_execute_command',
397
389
  description: '[Tauri Apps Only] Execute Tauri IPC commands (invoke Rust backend functions). ' +
398
390
  'Requires active driver_session. This is Tauri-specific IPC, not browser APIs. ' +
391
+ 'Plugin commands (plugin:mcp-bridge|*) are routed through the bridge; all other commands ' +
392
+ 'are executed via the webview using window.__TAURI_INTERNALS__.invoke, which keeps ' +
393
+ 'the application\'s Tauri capability rules in force. ' +
399
394
  'For Electron IPC or browser APIs, use appropriate tools for those frameworks.',
400
395
  category: TOOL_CATEGORIES.IPC_PLUGIN,
401
396
  schema: ExecuteIPCCommandSchema,
@@ -410,6 +405,7 @@ export const TOOLS = [
410
405
  return await executeIPCCommand({
411
406
  command: parsed.command,
412
407
  args: parsed.args,
408
+ windowId: parsed.windowId,
413
409
  appIdentifier: parsed.appIdentifier,
414
410
  });
415
411
  },
@@ -417,7 +413,8 @@ export const TOOLS = [
417
413
  {
418
414
  name: 'ipc_monitor',
419
415
  description: '[Tauri Apps Only] Monitor Tauri IPC calls between frontend and Rust backend. ' +
420
- 'Requires active driver_session. Captures invoke() calls and responses. ' +
416
+ 'Requires active driver_session. Captures invoke() calls made through window.__TAURI__.core. ' +
417
+ 'Bridge-internal commands (plugin:mcp-bridge|*) are excluded from capture. ' +
421
418
  'This is Tauri-specific; for browser network monitoring, use Chrome DevTools MCP.',
422
419
  category: TOOL_CATEGORIES.IPC_PLUGIN,
423
420
  schema: ManageIPCMonitoringSchema,
@@ -492,6 +489,11 @@ export const TOOLS = [
492
489
  '"list" - List all windows with labels, titles, URLs, and state. ' +
493
490
  '"info" - Get detailed info for a window (size, position, title, focus, visibility). ' +
494
491
  '"resize" - Resize a window (requires width/height, uses logical pixels by default). ' +
492
+ '"focus" - Focus a window (bring to front). ' +
493
+ '"minimize" - Minimize a window. ' +
494
+ '"maximize" - Maximize a window. ' +
495
+ 'Desktop-only actions (focus, minimize, maximize) require tauri-plugin-mcp-bridge 0.13.0+ ' +
496
+ 'and return a version error on older plugins. ' +
495
497
  'Requires active driver_session. ' +
496
498
  'For browser windows, use Chrome DevTools MCP instead.',
497
499
  category: TOOL_CATEGORIES.UI_AUTOMATION,
package/dist/version.d.ts CHANGED
@@ -13,3 +13,8 @@ export declare const PLUGIN_VERSION_FULL: string;
13
13
  * This is the major.minor version used in Cargo.toml dependency specifications.
14
14
  */
15
15
  export declare const PLUGIN_VERSION_CARGO: string;
16
+ /**
17
+ * Describe version skew between plugin and server.
18
+ * Returns null if versions match (major.minor), otherwise returns an upgrade message.
19
+ */
20
+ export declare function describeVersionSkew(pluginVersion: string | null): string | null;
package/dist/version.js CHANGED
@@ -15,3 +15,19 @@ export const PLUGIN_VERSION_FULL = pkg.version;
15
15
  * This is the major.minor version used in Cargo.toml dependency specifications.
16
16
  */
17
17
  export const PLUGIN_VERSION_CARGO = `${major}.${minor}`;
18
+ /**
19
+ * Describe version skew between plugin and server.
20
+ * Returns null if versions match (major.minor), otherwise returns an upgrade message.
21
+ */
22
+ export function describeVersionSkew(pluginVersion) {
23
+ if (!pluginVersion) {
24
+ return 'The connected app cannot report its plugin version. ' +
25
+ `Update tauri-plugin-mcp-bridge to ${PLUGIN_VERSION_CARGO} and rebuild the app.`;
26
+ }
27
+ const [pMajor, pMinor] = pluginVersion.split('.'), [sMajor, sMinor] = PLUGIN_VERSION_FULL.split('.');
28
+ if (pMajor === sMajor && pMinor === sMinor) {
29
+ return null;
30
+ }
31
+ return `Plugin ${pluginVersion} does not match server ${PLUGIN_VERSION_FULL}. ` +
32
+ `Set tauri-plugin-mcp-bridge = "${PLUGIN_VERSION_CARGO}" in Cargo.toml and rebuild the app.`;
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypothesi/tauri-mcp-server",
3
- "version": "0.11.2",
3
+ "version": "0.13.0",
4
4
  "mcpName": "io.github.hypothesi/mcp-server-tauri",
5
5
  "description": "A Model Context Protocol server for use with Tauri v2 applications",
6
6
  "type": "module",
@@ -1,47 +0,0 @@
1
- /**
2
- * Get computed CSS styles for elements
3
- *
4
- * @param {Object} params
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'
7
- * @param {string[]} params.properties - Specific CSS properties to retrieve
8
- * @param {boolean} params.multiple - Whether to get styles for all matching elements
9
- */
10
- (function(params) {
11
- const { selector, strategy, properties, multiple } = params;
12
-
13
- var elements;
14
-
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
- }
21
-
22
- if (!elements[0]) {
23
- throw new Error('Element not found: ' + selector);
24
- }
25
-
26
- const results = elements.map(function(element) {
27
- const styles = window.getComputedStyle(element);
28
-
29
- if (properties.length > 0) {
30
- const result = {};
31
- properties.forEach(function(prop) {
32
- result[prop] = styles.getPropertyValue(prop);
33
- });
34
- return result;
35
- }
36
-
37
- // Return all styles
38
- const allStyles = {};
39
- for (let i = 0; i < styles.length; i++) {
40
- const prop = styles[i];
41
- allStyles[prop] = styles.getPropertyValue(prop);
42
- }
43
- return allStyles;
44
- });
45
-
46
- return JSON.stringify(multiple ? results : results[0]);
47
- })