@hypothesi/tauri-mcp-server 0.4.0 → 0.5.1

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 CHANGED
@@ -67,7 +67,7 @@ Supported clients: `claude-code`, `cursor`, `windsurf`, `vscode`, `cline`, `roo-
67
67
  | `tauri_webview_wait_for` | Wait for elements, text, or events |
68
68
  | `tauri_webview_get_styles` | Get computed CSS styles |
69
69
  | `tauri_webview_execute_js` | Execute JavaScript in webview |
70
- | `tauri_list_windows` | List all open webview windows |
70
+ | `tauri_manage_window` | List windows, get info, or resize |
71
71
 
72
72
  ### IPC & Plugin
73
73
 
@@ -174,14 +174,33 @@ export class PluginClient extends EventEmitter {
174
174
  }
175
175
  // Singleton instance
176
176
  let pluginClient = null;
177
+ /**
178
+ * Gets the existing singleton PluginClient without creating or modifying it.
179
+ * Use this for status checks where you don't want to affect the current connection.
180
+ *
181
+ * @returns The existing PluginClient or null if none exists
182
+ */
183
+ export function getExistingPluginClient() {
184
+ return pluginClient;
185
+ }
177
186
  /**
178
187
  * Gets or creates a singleton PluginClient.
188
+ *
189
+ * If host/port are provided and differ from the existing client's configuration,
190
+ * the existing client is disconnected and a new one is created. This ensures
191
+ * that session start with a specific port always uses that port.
192
+ *
179
193
  * @param host Optional host override
180
194
  * @param port Optional port override
181
195
  */
182
196
  export function getPluginClient(host, port) {
183
197
  const resolvedHost = host ?? getDefaultHost();
184
198
  const resolvedPort = port ?? getDefaultPort();
199
+ // If singleton exists but host/port don't match, reset it
200
+ if (pluginClient && (pluginClient.host !== resolvedHost || pluginClient.port !== resolvedPort)) {
201
+ pluginClient.disconnect();
202
+ pluginClient = null;
203
+ }
185
204
  if (!pluginClient) {
186
205
  pluginClient = new PluginClient(resolvedHost, resolvedPort);
187
206
  }
@@ -202,6 +221,25 @@ export async function connectPlugin(host, port) {
202
221
  await client.connect();
203
222
  }
204
223
  }
224
+ /**
225
+ * Ensures a session is active and connects to the plugin using session config.
226
+ * This should be used by all tools that require a connected Tauri app.
227
+ *
228
+ * @throws Error if no session is active
229
+ */
230
+ export async function ensureSessionAndConnect() {
231
+ // Import dynamically to avoid circular dependency
232
+ const { hasActiveSession, getCurrentSession } = await import('./session-manager.js');
233
+ if (!hasActiveSession()) {
234
+ throw new Error('No active session. Call tauri_driver_session with action "start" first to connect to a Tauri app.');
235
+ }
236
+ const session = getCurrentSession();
237
+ if (!session) {
238
+ throw new Error('Session state is inconsistent. Please restart the session.');
239
+ }
240
+ await connectPlugin(session.host, session.port);
241
+ return getPluginClient(session.host, session.port);
242
+ }
205
243
  export async function disconnectPlugin() {
206
244
  const client = getPluginClient();
207
245
  client.disconnect();
@@ -1,14 +1,13 @@
1
1
  import { z } from 'zod';
2
- import { getPluginClient, connectPlugin } from './plugin-client.js';
2
+ import { ensureSessionAndConnect, getExistingPluginClient } from './plugin-client.js';
3
3
  export const ExecuteIPCCommandSchema = z.object({
4
4
  command: z.string(),
5
5
  args: z.unknown().optional(),
6
6
  });
7
7
  export async function executeIPCCommand(command, args = {}) {
8
8
  try {
9
- // Ensure we're connected to the plugin
10
- await connectPlugin();
11
- const client = getPluginClient();
9
+ // Ensure we have an active session and are connected
10
+ const client = await ensureSessionAndConnect();
12
11
  // Send IPC command via WebSocket to the mcp-bridge plugin
13
12
  const response = await client.sendCommand({
14
13
  command: 'invoke_tauri',
@@ -110,9 +109,50 @@ export async function emitTestEvent(eventName, payload) {
110
109
  throw new Error(`Failed to emit event: ${message}`);
111
110
  }
112
111
  }
112
+ export const GetWindowInfoSchema = z.object({});
113
+ export async function getWindowInfo() {
114
+ try {
115
+ const result = await executeIPCCommand('plugin:mcp-bridge|get_window_info');
116
+ const parsed = JSON.parse(result);
117
+ if (!parsed.success) {
118
+ throw new Error(parsed.error || 'Unknown error');
119
+ }
120
+ return JSON.stringify(parsed.result);
121
+ }
122
+ catch (error) {
123
+ const message = error instanceof Error ? error.message : String(error);
124
+ throw new Error(`Failed to get window info: ${message}`);
125
+ }
126
+ }
113
127
  export const GetBackendStateSchema = z.object({});
114
- export async function getBackendState() {
128
+ /**
129
+ * Get backend state from the connected Tauri app.
130
+ *
131
+ * This function can work in two modes:
132
+ * 1. Normal mode: Requires an active session (for MCP tool calls)
133
+ * 2. Setup mode: Uses existing connected client (for session setup)
134
+ *
135
+ * @param useExistingClient If true, uses the existing connected client without
136
+ * session validation. Used during session setup before currentSession is set.
137
+ */
138
+ export async function getBackendState(useExistingClient = false) {
115
139
  try {
140
+ if (useExistingClient) {
141
+ // During session setup, use the already-connected client directly
142
+ const client = getExistingPluginClient();
143
+ if (!client || !client.isConnected()) {
144
+ throw new Error('No connected client available');
145
+ }
146
+ const response = await client.sendCommand({
147
+ command: 'invoke_tauri',
148
+ args: { command: 'plugin:mcp-bridge|get_backend_state', args: {} },
149
+ });
150
+ if (!response.success) {
151
+ throw new Error(response.error || 'Unknown error');
152
+ }
153
+ return JSON.stringify(response.data);
154
+ }
155
+ // Normal mode: use executeIPCCommand which validates session
116
156
  const result = await executeIPCCommand('plugin:mcp-bridge|get_backend_state');
117
157
  const parsed = JSON.parse(result);
118
158
  if (!parsed.success) {
@@ -134,8 +174,7 @@ export const ListWindowsSchema = z.object({});
134
174
  */
135
175
  export async function listWindows() {
136
176
  try {
137
- await connectPlugin();
138
- const client = getPluginClient();
177
+ const client = await ensureSessionAndConnect();
139
178
  const response = await client.sendCommand({
140
179
  command: 'list_windows',
141
180
  });
@@ -154,3 +193,95 @@ export async function listWindows() {
154
193
  throw new Error(`Failed to list windows: ${message}`);
155
194
  }
156
195
  }
196
+ export const ResizeWindowSchema = z.object({
197
+ width: z.number().int().positive().describe('Width in pixels'),
198
+ height: z.number().int().positive().describe('Height in pixels'),
199
+ windowId: z.string().optional().describe('Window label to resize (defaults to "main")'),
200
+ logical: z.boolean().optional().default(true)
201
+ .describe('Use logical pixels (true, default) or physical pixels (false)'),
202
+ });
203
+ /**
204
+ * Resizes a window to the specified dimensions.
205
+ *
206
+ * @param options - Resize options including width, height, and optional windowId
207
+ * @returns JSON string with the result of the resize operation
208
+ */
209
+ export async function resizeWindow(options) {
210
+ try {
211
+ const client = await ensureSessionAndConnect();
212
+ const response = await client.sendCommand({
213
+ command: 'resize_window',
214
+ args: {
215
+ width: options.width,
216
+ height: options.height,
217
+ windowId: options.windowId,
218
+ logical: options.logical ?? true,
219
+ },
220
+ });
221
+ if (!response.success) {
222
+ throw new Error(response.error || 'Unknown error');
223
+ }
224
+ return JSON.stringify(response.data);
225
+ }
226
+ catch (error) {
227
+ const message = error instanceof Error ? error.message : String(error);
228
+ throw new Error(`Failed to resize window: ${message}`);
229
+ }
230
+ }
231
+ export const ManageWindowSchema = z.object({
232
+ action: z.enum(['list', 'info', 'resize'])
233
+ .describe('Action: "list" all windows, get "info" for one window, or "resize" a window'),
234
+ windowId: z.string().optional()
235
+ .describe('Window label to target (defaults to "main"). Required for "info", optional for "resize"'),
236
+ width: z.number().int().positive().optional()
237
+ .describe('Width in pixels (required for "resize" action)'),
238
+ height: z.number().int().positive().optional()
239
+ .describe('Height in pixels (required for "resize" action)'),
240
+ logical: z.boolean().optional().default(true)
241
+ .describe('Use logical pixels (true, default) or physical pixels (false). Only for "resize"'),
242
+ });
243
+ /**
244
+ * Unified window management function.
245
+ *
246
+ * Actions:
247
+ * - `list`: List all open webview windows with their labels, titles, URLs, and state
248
+ * - `info`: Get detailed info for a window (size, position, title, focus, visibility)
249
+ * - `resize`: Resize a window to specified dimensions
250
+ *
251
+ * @param options - Action and parameters
252
+ * @returns JSON string with the result
253
+ */
254
+ export async function manageWindow(options) {
255
+ const { action, windowId, width, height, logical } = options;
256
+ switch (action) {
257
+ case 'list': {
258
+ return listWindows();
259
+ }
260
+ case 'info': {
261
+ try {
262
+ const client = await ensureSessionAndConnect();
263
+ const response = await client.sendCommand({
264
+ command: 'get_window_info',
265
+ args: { windowId },
266
+ });
267
+ if (!response.success) {
268
+ throw new Error(response.error || 'Unknown error');
269
+ }
270
+ return JSON.stringify(response.data);
271
+ }
272
+ catch (error) {
273
+ const message = error instanceof Error ? error.message : String(error);
274
+ throw new Error(`Failed to get window info: ${message}`);
275
+ }
276
+ }
277
+ case 'resize': {
278
+ if (width === undefined || height === undefined) {
279
+ throw new Error('width and height are required for resize action');
280
+ }
281
+ return resizeWindow({ width, height, windowId, logical });
282
+ }
283
+ default: {
284
+ throw new Error(`Unknown action: ${action}`);
285
+ }
286
+ }
287
+ }
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @internal This module is for internal use only and is not exposed as MCP tools.
8
8
  */
9
- import { getPluginClient, connectPlugin } from './plugin-client.js';
9
+ import { ensureSessionAndConnect } from './plugin-client.js';
10
10
  /**
11
11
  * Registers a script to be injected into the webview.
12
12
  *
@@ -20,8 +20,7 @@ import { getPluginClient, connectPlugin } from './plugin-client.js';
20
20
  * @returns Promise resolving to registration result
21
21
  */
22
22
  export async function registerScript(id, type, content, windowLabel) {
23
- await connectPlugin();
24
- const client = getPluginClient();
23
+ const client = await ensureSessionAndConnect();
25
24
  const response = await client.sendCommand({
26
25
  command: 'register_script',
27
26
  args: { id, type, content, windowLabel },
@@ -39,8 +38,7 @@ export async function registerScript(id, type, content, windowLabel) {
39
38
  * @returns Promise resolving to removal result
40
39
  */
41
40
  export async function removeScript(id, windowLabel) {
42
- await connectPlugin();
43
- const client = getPluginClient();
41
+ const client = await ensureSessionAndConnect();
44
42
  const response = await client.sendCommand({
45
43
  command: 'remove_script',
46
44
  args: { id, windowLabel },
@@ -57,8 +55,7 @@ export async function removeScript(id, windowLabel) {
57
55
  * @returns Promise resolving to the number of scripts cleared
58
56
  */
59
57
  export async function clearScripts(windowLabel) {
60
- await connectPlugin();
61
- const client = getPluginClient();
58
+ const client = await ensureSessionAndConnect();
62
59
  const response = await client.sendCommand({
63
60
  command: 'clear_scripts',
64
61
  args: { windowLabel },
@@ -74,8 +71,7 @@ export async function clearScripts(windowLabel) {
74
71
  * @returns Promise resolving to the list of registered scripts
75
72
  */
76
73
  export async function getScripts() {
77
- await connectPlugin();
78
- const client = getPluginClient();
74
+ const client = await ensureSessionAndConnect();
79
75
  const response = await client.sendCommand({
80
76
  command: 'get_scripts',
81
77
  args: {},
@@ -1,7 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { getDefaultHost, getDefaultPort } from '../config.js';
3
3
  import { AppDiscovery } from './app-discovery.js';
4
- import { resetPluginClient, getPluginClient } from './plugin-client.js';
4
+ import { resetPluginClient, getExistingPluginClient, connectPlugin } from './plugin-client.js';
5
+ import { getBackendState } from './plugin-commands.js';
5
6
  import { resetInitialization } from './webview-executor.js';
6
7
  /**
7
8
  * Session Manager - Native IPC-based session management
@@ -26,8 +27,21 @@ export const ManageDriverSessionSchema = z.object({
26
27
  // Module State
27
28
  // ============================================================================
28
29
  // AppDiscovery instance - recreated when host changes
29
- // Track current session info
30
+ // Track current session info including app identifier for session reuse
30
31
  let appDiscovery = null, currentSession = null;
32
+ /**
33
+ * Check if a session is currently active.
34
+ * @returns true if a session has been started and not stopped
35
+ */
36
+ export function hasActiveSession() {
37
+ return currentSession !== null;
38
+ }
39
+ /**
40
+ * Get the current session info, or null if no session is active.
41
+ */
42
+ export function getCurrentSession() {
43
+ return currentSession;
44
+ }
31
45
  function getAppDiscovery(host) {
32
46
  if (!appDiscovery || appDiscovery.host !== host) {
33
47
  appDiscovery = new AppDiscovery(host);
@@ -51,7 +65,29 @@ async function tryConnect(host, port) {
51
65
  };
52
66
  }
53
67
  /**
54
- * Manage session lifecycle (start or stop).
68
+ * Fetch the app identifier from the backend state.
69
+ * Must be called after the singleton pluginClient is connected.
70
+ *
71
+ * @returns The app identifier (bundle ID) or null if not available. Returns null when:
72
+ * - The plugin doesn't support the identifier field (older versions)
73
+ * - The backend state request fails
74
+ * - The identifier field is missing from the response
75
+ */
76
+ async function fetchAppIdentifier() {
77
+ try {
78
+ // Use existing client - called during session setup before currentSession is set
79
+ const stateJson = await getBackendState(true);
80
+ const state = JSON.parse(stateJson);
81
+ // Return null if identifier is not present (backward compat with older plugins)
82
+ return state.app?.identifier ?? null;
83
+ }
84
+ catch {
85
+ // Return null on any error (e.g., older plugin version that doesn't support this)
86
+ return null;
87
+ }
88
+ }
89
+ /**
90
+ * Manage session lifecycle (start, stop, or status).
55
91
  *
56
92
  * Connection strategy for 'start':
57
93
  * 1. Try localhost:{port} first (most reliable for simulators/emulators/desktop)
@@ -59,18 +95,26 @@ async function tryConnect(host, port) {
59
95
  * 3. If both fail, try auto-discovery on localhost
60
96
  * 4. Return error if all attempts fail
61
97
  *
62
- * @param action - 'start' or 'stop'
98
+ * @param action - 'start', 'stop', or 'status'
63
99
  * @param host - Optional host address (defaults to env var or localhost)
64
100
  * @param port - Optional port number (defaults to 9223)
101
+ * @returns For 'start'/'stop': A message string describing the result.
102
+ * For 'status': A JSON string with connection details including:
103
+ * - `connected`: boolean indicating if connected
104
+ * - `app`: app name (or null if not connected)
105
+ * - `identifier`: app bundle ID (e.g., "com.example.app"), or null
106
+ * - `host`: connected host (or null)
107
+ * - `port`: connected port (or null)
65
108
  */
66
109
  export async function manageDriverSession(action, host, port) {
67
110
  // Handle status action
68
111
  if (action === 'status') {
69
- const client = getPluginClient();
70
- if (client.isConnected() && currentSession) {
112
+ const client = getExistingPluginClient();
113
+ if (client?.isConnected() && currentSession) {
71
114
  return JSON.stringify({
72
115
  connected: true,
73
116
  app: currentSession.name,
117
+ identifier: currentSession.identifier,
74
118
  host: currentSession.host,
75
119
  port: currentSession.port,
76
120
  });
@@ -78,12 +122,16 @@ export async function manageDriverSession(action, host, port) {
78
122
  return JSON.stringify({
79
123
  connected: false,
80
124
  app: null,
125
+ identifier: null,
81
126
  host: null,
82
127
  port: null,
83
128
  });
84
129
  }
85
130
  if (action === 'start') {
86
- // Reset any existing plugin client to ensure fresh connection
131
+ // Reset any existing connections to ensure fresh connection
132
+ if (appDiscovery) {
133
+ await appDiscovery.disconnectAll();
134
+ }
87
135
  resetPluginClient();
88
136
  const configuredHost = host ?? getDefaultHost();
89
137
  const configuredPort = port ?? getDefaultPort();
@@ -91,7 +139,11 @@ export async function manageDriverSession(action, host, port) {
91
139
  if (configuredHost !== 'localhost' && configuredHost !== '127.0.0.1') {
92
140
  try {
93
141
  const session = await tryConnect('localhost', configuredPort);
94
- currentSession = session;
142
+ // Connect the singleton pluginClient so status checks work
143
+ await connectPlugin(session.host, session.port);
144
+ // Fetch app identifier after singleton is connected
145
+ const identifier = await fetchAppIdentifier();
146
+ currentSession = { ...session, identifier };
95
147
  return `Session started with app: ${session.name} (localhost:${session.port})`;
96
148
  }
97
149
  catch {
@@ -101,7 +153,11 @@ export async function manageDriverSession(action, host, port) {
101
153
  // Strategy 2: Try the configured/provided host
102
154
  try {
103
155
  const session = await tryConnect(configuredHost, configuredPort);
104
- currentSession = session;
156
+ // Connect the singleton pluginClient so status checks work
157
+ await connectPlugin(session.host, session.port);
158
+ // Fetch app identifier after singleton is connected
159
+ const identifier = await fetchAppIdentifier();
160
+ currentSession = { ...session, identifier };
105
161
  return `Session started with app: ${session.name} (${session.host}:${session.port})`;
106
162
  }
107
163
  catch {
@@ -115,7 +171,11 @@ export async function manageDriverSession(action, host, port) {
115
171
  // Reset client again to connect to discovered port
116
172
  resetPluginClient();
117
173
  const session = await tryConnect('localhost', firstApp.port);
118
- currentSession = session;
174
+ // Connect the singleton pluginClient so status checks work
175
+ await connectPlugin(session.host, session.port);
176
+ // Fetch app identifier after singleton is connected
177
+ const identifier = await fetchAppIdentifier();
178
+ currentSession = { ...session, identifier };
119
179
  return `Session started with app: ${session.name} (localhost:${session.port})`;
120
180
  }
121
181
  catch {
@@ -126,7 +186,11 @@ export async function manageDriverSession(action, host, port) {
126
186
  try {
127
187
  resetPluginClient();
128
188
  const session = await tryConnect(configuredHost, configuredPort);
129
- currentSession = session;
189
+ // Connect the singleton pluginClient so status checks work
190
+ await connectPlugin(session.host, session.port);
191
+ // Fetch app identifier after singleton is connected
192
+ const identifier = await fetchAppIdentifier();
193
+ currentSession = { ...session, identifier };
130
194
  return `Session started with app: ${session.name} (${session.host}:${session.port})`;
131
195
  }
132
196
  catch {
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { getPluginClient, connectPlugin } from './plugin-client.js';
3
+ import { hasActiveSession, getCurrentSession } from './session-manager.js';
3
4
  import { createMcpLogger } from '../logger.js';
4
5
  import { buildScreenshotScript, buildScreenshotCaptureScript, getHtml2CanvasSource, HTML2CANVAS_SCRIPT_ID, } from './scripts/html2canvas-loader.js';
5
6
  import { registerScript, isScriptRegistered } from './script-manager.js';
@@ -22,17 +23,27 @@ const driverLogger = createMcpLogger('DRIVER');
22
23
  * This is called automatically by all tool functions.
23
24
  *
24
25
  * Initialization includes:
25
- * - Connecting to the plugin WebSocket
26
+ * - Verifying an active session exists (via tauri_driver_session)
27
+ * - Connecting to the plugin WebSocket using session config
26
28
  * - Console capture is already initialized by bridge.js in the Tauri app
27
29
  *
28
30
  * This function is idempotent - calling it multiple times is safe.
31
+ *
32
+ * @throws Error if no session is active (tauri_driver_session must be called first)
29
33
  */
30
34
  export async function ensureReady() {
31
35
  if (isInitialized) {
32
36
  return;
33
37
  }
34
- // Connect to the plugin
35
- await connectPlugin();
38
+ // Require an active session to prevent connecting to wrong app
39
+ if (!hasActiveSession()) {
40
+ throw new Error('No active session. Call tauri_driver_session with action "start" first to connect to a Tauri app.');
41
+ }
42
+ // Get session config and connect with explicit host/port
43
+ const session = getCurrentSession();
44
+ if (session) {
45
+ await connectPlugin(session.host, session.port);
46
+ }
36
47
  isInitialized = true;
37
48
  }
38
49
  /**
@@ -63,7 +74,12 @@ export async function executeInWebviewWithContext(script, windowId) {
63
74
  try {
64
75
  // Ensure we're fully initialized
65
76
  await ensureReady();
66
- const client = getPluginClient();
77
+ // Get session config to use correct host/port
78
+ const session = getCurrentSession();
79
+ if (!session) {
80
+ throw new Error('No active session');
81
+ }
82
+ const client = getPluginClient(session.host, session.port);
67
83
  // Send script directly - Rust handles wrapping and IPC callbacks.
68
84
  // Use 7s timeout (longer than Rust's 5s) so errors return before Node times out.
69
85
  const response = await client.sendCommand({
@@ -24,96 +24,72 @@ Please follow these steps:
24
24
  If no errors are found, let me know the app is running cleanly.
25
25
 
26
26
  If the session fails to start, help me troubleshoot the connection (is the app running? is the MCP bridge plugin installed?).`;
27
- const SETUP_PROMPT = `Help me set up the MCP Bridge plugin in my Tauri project so I can use these AI development tools.
27
+ const SETUP_PROMPT = `Help me set up or update the MCP Bridge plugin in my Tauri project.
28
28
 
29
- ## Prerequisites
29
+ ## IMPORTANT: Do Not Act Without Permission
30
30
 
31
- - This is a **Tauri v2** project (check for \`src-tauri/\` directory and \`tauri.conf.json\`)
32
- - If this is NOT a Tauri project, stop and let the user know this setup only applies to Tauri apps
31
+ **You must NOT make any changes to files without my explicit approval.**
33
32
 
34
- ## Setup Steps
33
+ 1. First, examine my project to understand its current state
34
+ 2. Then, present a clear summary of what changes are needed
35
+ 3. Wait for my approval before making ANY modifications
36
+ 4. Only proceed with changes after I confirm
35
37
 
36
- ### Step 1: Add the Rust Plugin
38
+ ## Prerequisites Check
37
39
 
38
- Add the plugin to \`src-tauri/Cargo.toml\` dependencies:
40
+ First, verify this is a Tauri v2 project:
41
+ - Look for \`src-tauri/\` directory and \`tauri.conf.json\`
42
+ - If this is NOT a Tauri project, stop and let me know this setup only applies to Tauri apps
39
43
 
44
+ ## What to Check
45
+
46
+ Examine these files and report what needs to be added or updated:
47
+
48
+ ### 1. Rust Plugin Dependency
49
+ Check \`src-tauri/Cargo.toml\` for \`tauri-plugin-mcp-bridge\`. If missing or outdated, note that it needs:
40
50
  \`\`\`toml
41
51
  [dependencies]
42
- tauri-plugin-mcp-bridge = "0.2"
52
+ tauri-plugin-mcp-bridge = "0.4"
43
53
  \`\`\`
44
54
 
45
- Or run from the \`src-tauri\` directory:
46
- \`\`\`bash
47
- cargo add tauri-plugin-mcp-bridge
48
- \`\`\`
49
-
50
- ### Step 2: Register the Plugin
51
-
52
- In the Tauri app's entry point (usually \`src-tauri/src/lib.rs\` or \`src-tauri/src/main.rs\`), register the plugin.
53
-
54
- Find the \`tauri::Builder\` and add the plugin (only in debug builds):
55
-
55
+ ### 2. Plugin Registration
56
+ Check \`src-tauri/src/lib.rs\` or \`src-tauri/src/main.rs\` for plugin registration. It should have:
56
57
  \`\`\`rust
57
- let mut builder = tauri::Builder::default();
58
- // ... existing plugins ...
59
-
60
58
  #[cfg(debug_assertions)]
61
59
  {
62
60
  builder = builder.plugin(tauri_plugin_mcp_bridge::init());
63
61
  }
64
-
65
- builder
66
- .run(tauri::generate_context!())
67
- .expect("error while running tauri application");
68
- \`\`\`
69
-
70
- ### Step 3: Enable Global Tauri (REQUIRED)
71
-
72
- In \`src-tauri/tauri.conf.json\`, ensure \`withGlobalTauri\` is enabled:
73
-
74
- \`\`\`json
75
- {
76
- "app": {
77
- "withGlobalTauri": true
78
- }
79
- }
80
62
  \`\`\`
81
63
 
64
+ ### 3. Global Tauri Setting
65
+ Check \`src-tauri/tauri.conf.json\` for \`withGlobalTauri: true\` under the \`app\` section.
82
66
  **This is required** - without it, the MCP bridge cannot communicate with the webview.
83
67
 
84
- ### Step 4: Add Plugin Permissions
68
+ ### 4. Plugin Permissions
69
+ Check \`src-tauri/capabilities/default.json\` (or similar) for \`"mcp-bridge:default"\` permission.
85
70
 
86
- Add the plugin permission to \`src-tauri/capabilities/default.json\` (create the file if it doesn't exist):
71
+ ## Your Response Format
87
72
 
88
- \`\`\`json
89
- {
90
- "$schema": "../gen/schemas/desktop-schema.json",
91
- "identifier": "default",
92
- "description": "Default capabilities",
93
- "windows": ["main"],
94
- "permissions": [
95
- "mcp-bridge:default"
96
- ]
97
- }
98
- \`\`\`
73
+ After examining the project, respond with:
99
74
 
100
- If the file already exists, just add \`"mcp-bridge:default"\` to the existing permissions array.
75
+ 1. **Current State**: What's already configured correctly
76
+ 2. **Changes Needed**: A numbered list of specific changes required
77
+ 3. **Ask for Permission**: "May I proceed with these changes?"
101
78
 
102
- ## Verification
79
+ Only after I say yes should you make any modifications.
103
80
 
104
- After setup:
105
- 1. Run the Tauri app in development mode (\`cargo tauri dev\` or \`npm run tauri dev\`)
106
- 2. The MCP bridge will start a WebSocket server on port 9223
107
- 3. Use \`tauri_driver_session\` with action "start" to connect
108
- 4. Use \`tauri_driver_session\` with action "status" to verify the connection
81
+ ## After Setup
109
82
 
110
- ## Notes
83
+ Once changes are approved and made:
84
+ 1. Run the Tauri app in development mode (\`cargo tauri dev\`)
85
+ 2. Use \`tauri_driver_session\` with action "start" to connect
86
+ 3. Use \`tauri_driver_session\` with action "status" to verify
111
87
 
112
- - The plugin only runs in debug builds (\`#[cfg(debug_assertions)]\`) so it won't affect production
113
- - The WebSocket server binds to \`0.0.0.0\` by default to support mobile device testing
114
- - For localhost-only access, use \`Builder::new().bind_address("127.0.0.1").build()\` instead of \`init()\`
88
+ ## Notes
115
89
 
116
- Please examine the project structure and make the necessary changes to set up the MCP bridge plugin.`;
90
+ - The plugin only runs in debug builds so it won't affect production
91
+ - The WebSocket server binds to \`0.0.0.0:9223\` by default
92
+ - For localhost-only access, use \`Builder::new().bind_address("127.0.0.1").build()\``;
117
93
  /**
118
94
  * Complete registry of all available prompts
119
95
  */
@@ -139,9 +115,9 @@ export const PROMPTS = [
139
115
  },
140
116
  {
141
117
  name: 'setup',
142
- description: 'Set up the MCP Bridge plugin in a Tauri project. ' +
143
- 'Guides through adding the Rust crate, registering the plugin, enabling withGlobalTauri, ' +
144
- 'and adding permissions. Use this when starting with a new Tauri project.',
118
+ description: 'Set up or update the MCP Bridge plugin in a Tauri project. ' +
119
+ 'Examines the project, reports what changes are needed, and asks for permission before ' +
120
+ 'making any modifications. Use for initial setup or to update to the latest version.',
145
121
  arguments: [],
146
122
  handler: () => {
147
123
  return [
@@ -2,24 +2,118 @@
2
2
  * Single source of truth for all MCP tool definitions
3
3
  * This file defines all available tools and their metadata
4
4
  */
5
+ import { z } from 'zod';
5
6
  import { listDevices, ListDevicesSchema } from './manager/mobile.js';
6
7
  import { manageDriverSession, ManageDriverSessionSchema, } from './driver/session-manager.js';
7
8
  import { readLogs, ReadLogsSchema } from './monitor/logs.js';
8
- import { executeIPCCommand, manageIPCMonitoring, getIPCEvents, emitTestEvent, getBackendState, listWindows, ExecuteIPCCommandSchema, ManageIPCMonitoringSchema, GetIPCEventsSchema, EmitTestEventSchema, GetBackendStateSchema, ListWindowsSchema, } from './driver/plugin-commands.js';
9
+ import { executeIPCCommand, manageIPCMonitoring, getIPCEvents, emitTestEvent, getBackendState, manageWindow, ExecuteIPCCommandSchema, ManageIPCMonitoringSchema, GetIPCEventsSchema, EmitTestEventSchema, GetBackendStateSchema, ManageWindowSchema, } from './driver/plugin-commands.js';
9
10
  import { interact, screenshot, keyboard, waitFor, getStyles, executeJavaScript, findElement, InteractSchema, ScreenshotSchema, KeyboardSchema, WaitForSchema, GetStylesSchema, ExecuteJavaScriptSchema, FindElementSchema, } from './driver/webview-interactions.js';
10
11
  /**
11
12
  * Tool categories for organization
12
13
  */
13
14
  export const TOOL_CATEGORIES = {
15
+ SETUP: 'Setup & Configuration',
14
16
  MOBILE_DEVELOPMENT: 'Mobile Development',
15
17
  UI_AUTOMATION: 'UI Automation & WebView Interaction',
16
18
  IPC_PLUGIN: 'IPC & Plugin Tools (via MCP Bridge)',
17
19
  };
20
+ // Setup instructions for the MCP Bridge plugin
21
+ const SETUP_INSTRUCTIONS = `# MCP Bridge Plugin Setup Instructions
22
+
23
+ Use these instructions to set up or update the MCP Bridge plugin in a Tauri v2 project.
24
+
25
+ ## IMPORTANT: Do Not Act Without Permission
26
+
27
+ **You must NOT make any changes to files without the user's explicit approval.**
28
+
29
+ 1. First, examine the project to understand its current state
30
+ 2. Then, present a clear summary of what changes are needed
31
+ 3. Wait for user approval before making ANY modifications
32
+ 4. Only proceed with changes after they confirm
33
+
34
+ ## Prerequisites Check
35
+
36
+ First, verify this is a Tauri v2 project:
37
+ - Look for \`src-tauri/\` directory and \`tauri.conf.json\`
38
+ - If this is NOT a Tauri project, stop and let the user know this setup only applies to Tauri apps
39
+
40
+ ## What to Check
41
+
42
+ Examine these files and report what needs to be added or updated:
43
+
44
+ ### 1. Rust Plugin Dependency
45
+ Check \`src-tauri/Cargo.toml\` for \`tauri-plugin-mcp-bridge\`. If missing or outdated, note that it needs:
46
+ \`\`\`toml
47
+ [dependencies]
48
+ tauri-plugin-mcp-bridge = "0.4"
49
+ \`\`\`
50
+
51
+ ### 2. Plugin Registration
52
+ Check \`src-tauri/src/lib.rs\` or \`src-tauri/src/main.rs\` for plugin registration. It should have:
53
+ \`\`\`rust
54
+ #[cfg(debug_assertions)]
55
+ {
56
+ builder = builder.plugin(tauri_plugin_mcp_bridge::init());
57
+ }
58
+ \`\`\`
59
+
60
+ ### 3. Global Tauri Setting
61
+ Check \`src-tauri/tauri.conf.json\` for \`withGlobalTauri: true\` under the \`app\` section.
62
+ **This is required** - without it, the MCP bridge cannot communicate with the webview.
63
+
64
+ ### 4. Plugin Permissions
65
+ Check \`src-tauri/capabilities/default.json\` (or similar) for \`"mcp-bridge:default"\` permission.
66
+
67
+ ## Response Format
68
+
69
+ After examining the project, respond with:
70
+
71
+ 1. **Current State**: What's already configured correctly
72
+ 2. **Changes Needed**: A numbered list of specific changes required
73
+ 3. **Ask for Permission**: "May I proceed with these changes?"
74
+
75
+ Only after the user says yes should you make any modifications.
76
+
77
+ ## After Setup
78
+
79
+ Once changes are approved and made:
80
+ 1. Run the Tauri app in development mode (\`cargo tauri dev\`)
81
+ 2. Use \`tauri_driver_session\` with action "start" to connect
82
+ 3. Use \`tauri_driver_session\` with action "status" to verify
83
+
84
+ ## Notes
85
+
86
+ - The plugin only runs in debug builds so it won't affect production
87
+ - The WebSocket server binds to \`0.0.0.0:9223\` by default
88
+ - For localhost-only access, use \`Builder::new().bind_address("127.0.0.1").build()\`
89
+ `;
18
90
  /**
19
91
  * Complete registry of all available tools
20
92
  * This is the single source of truth for tool definitions
21
93
  */
22
94
  export const TOOLS = [
95
+ // Setup & Configuration Tools
96
+ {
97
+ name: 'tauri_get_setup_instructions',
98
+ description: 'Get instructions for setting up or updating the MCP Bridge plugin in a Tauri project. ' +
99
+ 'Call this tool when: (1) tauri_driver_session fails to connect, (2) you detect the plugin ' +
100
+ 'is not installed or outdated, or (3) the user asks about setup. ' +
101
+ 'Returns step-by-step guidance that you should follow to help the user configure their project. ' +
102
+ 'IMPORTANT: The instructions require you to examine the project first and ask for permission ' +
103
+ 'before making any changes.',
104
+ category: TOOL_CATEGORIES.SETUP,
105
+ schema: z.object({}),
106
+ annotations: {
107
+ title: 'Get Setup Instructions',
108
+ readOnlyHint: true,
109
+ destructiveHint: false,
110
+ idempotentHint: true,
111
+ openWorldHint: false,
112
+ },
113
+ handler: async () => {
114
+ return SETUP_INSTRUCTIONS;
115
+ },
116
+ },
23
117
  // Mobile Development Tools
24
118
  {
25
119
  name: 'tauri_list_devices',
@@ -42,7 +136,11 @@ export const TOOLS = [
42
136
  {
43
137
  name: 'tauri_driver_session',
44
138
  description: '[Tauri Apps Only] Start/stop automation session to connect to a RUNNING Tauri app. ' +
45
- 'Use action "status" to check current connection state. ' +
139
+ 'Use action "status" to check current connection state and get the app identifier. ' +
140
+ 'The status response includes an "identifier" field (e.g., "com.example.myapp") that uniquely identifies the connected app. ' +
141
+ 'The identifier may be null if the Tauri app uses an older plugin version that does not provide it. ' +
142
+ 'Before starting a new session, check status first - if already connected to the correct app (matching identifier), ' +
143
+ 'reuse the existing session. If identifier is null, you cannot verify the app identity. ' +
46
144
  'REQUIRED before using other tauri_webview_* or tauri_plugin_* tools. ' +
47
145
  'Connects via WebSocket to the MCP Bridge plugin in the Tauri app. ' +
48
146
  'For browser automation, use Chrome DevTools MCP instead. ' +
@@ -339,20 +437,25 @@ export const TOOLS = [
339
437
  },
340
438
  // Window Management Tools
341
439
  {
342
- name: 'tauri_list_windows',
343
- description: '[Tauri Apps Only] List all Tauri webview windows with details including ' +
344
- 'labels, titles, URLs, and state (focused, visible, isMain). ' +
345
- 'Requires active tauri_driver_session. Use to discover windows before targeting them. ' +
346
- 'For browser tabs/windows, use Chrome DevTools MCP instead.',
440
+ name: 'tauri_manage_window',
441
+ description: '[Tauri Apps Only] Manage Tauri windows. Actions: ' +
442
+ '"list" - List all windows with labels, titles, URLs, and state. ' +
443
+ '"info" - Get detailed info for a window (size, position, title, focus, visibility). ' +
444
+ '"resize" - Resize a window (requires width/height, uses logical pixels by default). ' +
445
+ 'Requires active tauri_driver_session. ' +
446
+ 'For browser windows, use Chrome DevTools MCP instead.',
347
447
  category: TOOL_CATEGORIES.UI_AUTOMATION,
348
- schema: ListWindowsSchema,
448
+ schema: ManageWindowSchema,
349
449
  annotations: {
350
- title: 'List Tauri Windows',
351
- readOnlyHint: true,
450
+ title: 'Manage Tauri Window',
451
+ readOnlyHint: false,
452
+ destructiveHint: false,
453
+ idempotentHint: true,
352
454
  openWorldHint: false,
353
455
  },
354
- handler: async () => {
355
- return await listWindows();
456
+ handler: async (args) => {
457
+ const parsed = ManageWindowSchema.parse(args);
458
+ return await manageWindow(parsed);
356
459
  },
357
460
  },
358
461
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypothesi/tauri-mcp-server",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "A Model Context Protocol server for Tauri v2 development",
5
5
  "type": "module",
6
6
  "bin": {