@hypothesi/tauri-mcp-server 0.5.1 → 0.6.2

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
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@hypothesi/tauri-mcp-server)](https://www.npmjs.com/package/@hypothesi/tauri-mcp-server)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-8b5cf6.svg)](https://github.com/hypothesi/mcp-server-tauri/blob/main/LICENSE)
5
5
 
6
- A **Model Context Protocol (MCP) server** that enables AI assistants like Claude, Cursor, and Windsurf to build, test, and debug Tauri v2 applications.
6
+ A **Model Context Protocol (MCP) server** that enables AI assistants like Claude, Cursor, and Windsurf to build, test, and debug Tauri® v2 applications.
7
7
 
8
8
  📖 **[Full Documentation](https://hypothesi.github.io/mcp-server-tauri)**
9
9
 
@@ -52,6 +52,40 @@ npx -y install-mcp @hypothesi/tauri-mcp-server --client claude-code
52
52
 
53
53
  Supported clients: `claude-code`, `cursor`, `windsurf`, `vscode`, `cline`, `roo-cline`, `claude`, `zed`, `goose`, `warp`, `codex`
54
54
 
55
+ ## Multi-App Support
56
+
57
+ The MCP server supports connecting to multiple Tauri apps simultaneously. Each app runs on a unique port, and the most recently connected app becomes the "default" app.
58
+
59
+ **Key Features:**
60
+ - Connect to multiple apps on different ports
61
+ - Default app used when no `appIdentifier` specified
62
+ - Target specific apps using port number or bundle ID
63
+ - Stop individual sessions or all sessions at once
64
+
65
+ **Example Usage:**
66
+ ```javascript
67
+ // Connect to first app
68
+ await tauri_driver_session({ action: "start", port: 9223 })
69
+
70
+ // Connect to second app
71
+ await tauri_driver_session({ action: "start", port: 9224 })
72
+
73
+ // Check status - shows both apps with default indicator
74
+ await tauri_driver_session({ action: "status" })
75
+
76
+ // Use default app (most recent - port 9224)
77
+ await tauri_webview_screenshot()
78
+
79
+ // Target specific app by port
80
+ await tauri_webview_screenshot({ appIdentifier: 9223 })
81
+
82
+ // Stop specific app
83
+ await tauri_driver_session({ action: "stop", appIdentifier: 9223 })
84
+
85
+ // Stop all apps
86
+ await tauri_driver_session({ action: "stop" })
87
+ ```
88
+
55
89
  ## Available Tools (16 total)
56
90
 
57
91
  ### UI Automation
@@ -95,3 +129,11 @@ Supported clients: `claude-code`, `cursor`, `windsurf`, `vscode`, `cline`, `roo-
95
129
  ## License
96
130
 
97
131
  MIT © [hypothesi](https://github.com/hypothesi)
132
+
133
+ ---
134
+
135
+ ## Trademark Notice
136
+
137
+ TAURI® is a registered trademark of The Tauri Programme within the Commons Conservancy. [https://tauri.app/](https://tauri.app/)
138
+
139
+ This project is not affiliated with, endorsed by, or sponsored by The Tauri Programme within the Commons Conservancy.
@@ -225,20 +225,18 @@ export async function connectPlugin(host, port) {
225
225
  * Ensures a session is active and connects to the plugin using session config.
226
226
  * This should be used by all tools that require a connected Tauri app.
227
227
  *
228
+ * @param appIdentifier - Optional app identifier to target specific app
228
229
  * @throws Error if no session is active
229
230
  */
230
- export async function ensureSessionAndConnect() {
231
+ export async function ensureSessionAndConnect(appIdentifier) {
231
232
  // 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.');
233
+ const { resolveTargetApp } = await import('./session-manager.js');
234
+ const session = resolveTargetApp(appIdentifier);
235
+ // Ensure client is connected
236
+ if (!session.client.isConnected()) {
237
+ await session.client.connect();
235
238
  }
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);
239
+ return session.client;
242
240
  }
243
241
  export async function disconnectPlugin() {
244
242
  const client = getPluginClient();
@@ -3,11 +3,13 @@ import { ensureSessionAndConnect, getExistingPluginClient } from './plugin-clien
3
3
  export const ExecuteIPCCommandSchema = z.object({
4
4
  command: z.string(),
5
5
  args: z.unknown().optional(),
6
+ appIdentifier: z.union([z.string(), z.number()]).optional().describe('App port or bundle ID to target. Defaults to the only connected app or the default app if multiple are connected.'),
6
7
  });
7
- export async function executeIPCCommand(command, args = {}) {
8
+ export async function executeIPCCommand(options) {
8
9
  try {
10
+ const { command, args = {}, appIdentifier } = options;
9
11
  // Ensure we have an active session and are connected
10
- const client = await ensureSessionAndConnect();
12
+ const client = await ensureSessionAndConnect(appIdentifier);
11
13
  // Send IPC command via WebSocket to the mcp-bridge plugin
12
14
  const response = await client.sendCommand({
13
15
  command: 'invoke_tauri',
@@ -26,19 +28,20 @@ export async function executeIPCCommand(command, args = {}) {
26
28
  // Combined schema for managing IPC monitoring
27
29
  export const ManageIPCMonitoringSchema = z.object({
28
30
  action: z.enum(['start', 'stop']).describe('Action to perform: start or stop IPC monitoring'),
31
+ appIdentifier: z.union([z.string(), z.number()]).optional().describe('App port or bundle ID to target. Defaults to the only connected app or the default app if multiple are connected.'),
29
32
  });
30
33
  // Keep individual schemas for backward compatibility if needed
31
34
  export const StartIPCMonitoringSchema = z.object({});
32
35
  export const StopIPCMonitoringSchema = z.object({});
33
- export async function manageIPCMonitoring(action) {
36
+ export async function manageIPCMonitoring(action, appIdentifier) {
34
37
  if (action === 'start') {
35
- return startIPCMonitoring();
38
+ return startIPCMonitoring(appIdentifier);
36
39
  }
37
- return stopIPCMonitoring();
40
+ return stopIPCMonitoring(appIdentifier);
38
41
  }
39
- export async function startIPCMonitoring() {
42
+ export async function startIPCMonitoring(appIdentifier) {
40
43
  try {
41
- const result = await executeIPCCommand('plugin:mcp-bridge|start_ipc_monitor');
44
+ const result = await executeIPCCommand({ command: 'plugin:mcp-bridge|start_ipc_monitor', appIdentifier });
42
45
  const parsed = JSON.parse(result);
43
46
  if (!parsed.success) {
44
47
  throw new Error(parsed.error || 'Unknown error');
@@ -50,9 +53,9 @@ export async function startIPCMonitoring() {
50
53
  throw new Error(`Failed to start IPC monitoring: ${message}`);
51
54
  }
52
55
  }
53
- export async function stopIPCMonitoring() {
56
+ export async function stopIPCMonitoring(appIdentifier) {
54
57
  try {
55
- const result = await executeIPCCommand('plugin:mcp-bridge|stop_ipc_monitor');
58
+ const result = await executeIPCCommand({ command: 'plugin:mcp-bridge|stop_ipc_monitor', appIdentifier });
56
59
  const parsed = JSON.parse(result);
57
60
  if (!parsed.success) {
58
61
  throw new Error(parsed.error || 'Unknown error');
@@ -66,10 +69,11 @@ export async function stopIPCMonitoring() {
66
69
  }
67
70
  export const GetIPCEventsSchema = z.object({
68
71
  filter: z.string().optional().describe('Filter events by command name'),
72
+ appIdentifier: z.union([z.string(), z.number()]).optional().describe('App port or bundle ID to target. Defaults to the only connected app or the default app if multiple are connected.'),
69
73
  });
70
- export async function getIPCEvents(filter) {
74
+ export async function getIPCEvents(filter, appIdentifier) {
71
75
  try {
72
- const result = await executeIPCCommand('plugin:mcp-bridge|get_ipc_events');
76
+ const result = await executeIPCCommand({ command: 'plugin:mcp-bridge|get_ipc_events', appIdentifier });
73
77
  const parsed = JSON.parse(result);
74
78
  if (!parsed.success) {
75
79
  throw new Error(parsed.error || 'Unknown error');
@@ -91,12 +95,17 @@ export async function getIPCEvents(filter) {
91
95
  export const EmitTestEventSchema = z.object({
92
96
  eventName: z.string(),
93
97
  payload: z.unknown(),
98
+ appIdentifier: z.union([z.string(), z.number()]).optional().describe('App port or bundle ID to target. Defaults to the only connected app or the default app if multiple are connected.'),
94
99
  });
95
- export async function emitTestEvent(eventName, payload) {
100
+ export async function emitTestEvent(eventName, payload, appIdentifier) {
96
101
  try {
97
- const result = await executeIPCCommand('plugin:mcp-bridge|emit_event', {
98
- eventName,
99
- payload,
102
+ const result = await executeIPCCommand({
103
+ command: 'plugin:mcp-bridge|emit_event',
104
+ args: {
105
+ eventName,
106
+ payload,
107
+ },
108
+ appIdentifier,
100
109
  });
101
110
  const parsed = JSON.parse(result);
102
111
  if (!parsed.success) {
@@ -109,10 +118,12 @@ export async function emitTestEvent(eventName, payload) {
109
118
  throw new Error(`Failed to emit event: ${message}`);
110
119
  }
111
120
  }
112
- export const GetWindowInfoSchema = z.object({});
113
- export async function getWindowInfo() {
121
+ export const GetWindowInfoSchema = z.object({
122
+ appIdentifier: z.union([z.string(), z.number()]).optional().describe('App port or bundle ID to target. Defaults to the only connected app or the default app if multiple are connected.'),
123
+ });
124
+ export async function getWindowInfo(appIdentifier) {
114
125
  try {
115
- const result = await executeIPCCommand('plugin:mcp-bridge|get_window_info');
126
+ const result = await executeIPCCommand({ command: 'plugin:mcp-bridge|get_window_info', appIdentifier });
116
127
  const parsed = JSON.parse(result);
117
128
  if (!parsed.success) {
118
129
  throw new Error(parsed.error || 'Unknown error');
@@ -124,7 +135,9 @@ export async function getWindowInfo() {
124
135
  throw new Error(`Failed to get window info: ${message}`);
125
136
  }
126
137
  }
127
- export const GetBackendStateSchema = z.object({});
138
+ export const GetBackendStateSchema = z.object({
139
+ appIdentifier: z.union([z.string(), z.number()]).optional().describe('App port or bundle ID to target. Defaults to the only connected app or the default app if multiple are connected.'),
140
+ });
128
141
  /**
129
142
  * Get backend state from the connected Tauri app.
130
143
  *
@@ -135,8 +148,9 @@ export const GetBackendStateSchema = z.object({});
135
148
  * @param useExistingClient If true, uses the existing connected client without
136
149
  * session validation. Used during session setup before currentSession is set.
137
150
  */
138
- export async function getBackendState(useExistingClient = false) {
151
+ export async function getBackendState(options = {}) {
139
152
  try {
153
+ const { useExistingClient = false, appIdentifier } = options;
140
154
  if (useExistingClient) {
141
155
  // During session setup, use the already-connected client directly
142
156
  const client = getExistingPluginClient();
@@ -153,7 +167,7 @@ export async function getBackendState(useExistingClient = false) {
153
167
  return JSON.stringify(response.data);
154
168
  }
155
169
  // Normal mode: use executeIPCCommand which validates session
156
- const result = await executeIPCCommand('plugin:mcp-bridge|get_backend_state');
170
+ const result = await executeIPCCommand({ command: 'plugin:mcp-bridge|get_backend_state', appIdentifier });
157
171
  const parsed = JSON.parse(result);
158
172
  if (!parsed.success) {
159
173
  throw new Error(parsed.error || 'Unknown error');
@@ -172,9 +186,9 @@ export const ListWindowsSchema = z.object({});
172
186
  /**
173
187
  * Lists all open webview windows in the Tauri application.
174
188
  */
175
- export async function listWindows() {
189
+ export async function listWindows(appIdentifier) {
176
190
  try {
177
- const client = await ensureSessionAndConnect();
191
+ const client = await ensureSessionAndConnect(appIdentifier);
178
192
  const response = await client.sendCommand({
179
193
  command: 'list_windows',
180
194
  });
@@ -208,7 +222,7 @@ export const ResizeWindowSchema = z.object({
208
222
  */
209
223
  export async function resizeWindow(options) {
210
224
  try {
211
- const client = await ensureSessionAndConnect();
225
+ const client = await ensureSessionAndConnect(options.appIdentifier);
212
226
  const response = await client.sendCommand({
213
227
  command: 'resize_window',
214
228
  args: {
@@ -239,6 +253,7 @@ export const ManageWindowSchema = z.object({
239
253
  .describe('Height in pixels (required for "resize" action)'),
240
254
  logical: z.boolean().optional().default(true)
241
255
  .describe('Use logical pixels (true, default) or physical pixels (false). Only for "resize"'),
256
+ appIdentifier: z.union([z.string(), z.number()]).optional().describe('App port or bundle ID to target. Defaults to the only connected app or the default app if multiple are connected.'),
242
257
  });
243
258
  /**
244
259
  * Unified window management function.
@@ -252,14 +267,14 @@ export const ManageWindowSchema = z.object({
252
267
  * @returns JSON string with the result
253
268
  */
254
269
  export async function manageWindow(options) {
255
- const { action, windowId, width, height, logical } = options;
270
+ const { action, windowId, width, height, logical, appIdentifier } = options;
256
271
  switch (action) {
257
272
  case 'list': {
258
- return listWindows();
273
+ return listWindows(appIdentifier);
259
274
  }
260
275
  case 'info': {
261
276
  try {
262
- const client = await ensureSessionAndConnect();
277
+ const client = await ensureSessionAndConnect(appIdentifier);
263
278
  const response = await client.sendCommand({
264
279
  command: 'get_window_info',
265
280
  args: { windowId },
@@ -278,7 +293,7 @@ export async function manageWindow(options) {
278
293
  if (width === undefined || height === undefined) {
279
294
  throw new Error('width and height are required for resize action');
280
295
  }
281
- return resizeWindow({ width, height, windowId, logical });
296
+ return resizeWindow({ width, height, windowId, logical, appIdentifier });
282
297
  }
283
298
  default: {
284
299
  throw new Error(`Unknown action: ${action}`);
@@ -1,9 +1,10 @@
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, getExistingPluginClient, connectPlugin } from './plugin-client.js';
5
- import { getBackendState } from './plugin-commands.js';
4
+ import { PluginClient } from './plugin-client.js';
6
5
  import { resetInitialization } from './webview-executor.js';
6
+ import { createMcpLogger } from '../logger.js';
7
+ const sessionLogger = createMcpLogger('SESSION');
7
8
  /**
8
9
  * Session Manager - Native IPC-based session management
9
10
  *
@@ -22,25 +23,45 @@ export const ManageDriverSessionSchema = z.object({
22
23
  action: z.enum(['start', 'stop', 'status']).describe('Action to perform: start or stop the session, or check status'),
23
24
  host: z.string().optional().describe('Host address to connect to (e.g., 192.168.1.100). Falls back to MCP_BRIDGE_HOST or TAURI_DEV_HOST env vars'),
24
25
  port: z.number().optional().describe('Port to connect to (default: 9223)'),
26
+ appIdentifier: z.union([z.string(), z.number()]).optional().describe('App identifier (port number or bundle ID) to stop. Only used with action "stop". If omitted, stops all sessions.'),
25
27
  });
26
28
  // ============================================================================
27
29
  // Module State
28
30
  // ============================================================================
29
31
  // AppDiscovery instance - recreated when host changes
30
- // Track current session info including app identifier for session reuse
31
- let appDiscovery = null, currentSession = null;
32
+ let appDiscovery = null;
33
+ // Track multiple concurrent sessions keyed by port
34
+ const activeSessions = new Map();
35
+ // Track the default app (most recently connected)
36
+ let defaultPort = null;
32
37
  /**
33
- * Check if a session is currently active.
34
- * @returns true if a session has been started and not stopped
38
+ * Check if any session is currently active.
39
+ * @returns true if at least one session exists
35
40
  */
36
41
  export function hasActiveSession() {
37
- return currentSession !== null;
42
+ return activeSessions.size > 0;
38
43
  }
39
44
  /**
40
- * Get the current session info, or null if no session is active.
45
+ * Get a specific session by port.
41
46
  */
42
- export function getCurrentSession() {
43
- return currentSession;
47
+ export function getSession(port) {
48
+ return activeSessions.get(port) ?? null;
49
+ }
50
+ /**
51
+ * Get the default session (most recently connected).
52
+ */
53
+ export function getDefaultSession() {
54
+ if (defaultPort !== null && activeSessions.has(defaultPort)) {
55
+ const session = activeSessions.get(defaultPort);
56
+ return session ?? null;
57
+ }
58
+ return null;
59
+ }
60
+ /**
61
+ * Get all active sessions.
62
+ */
63
+ export function getAllSessions() {
64
+ return Array.from(activeSessions.values());
44
65
  }
45
66
  function getAppDiscovery(host) {
46
67
  if (!appDiscovery || appDiscovery.host !== host) {
@@ -48,6 +69,208 @@ function getAppDiscovery(host) {
48
69
  }
49
70
  return appDiscovery;
50
71
  }
72
+ /**
73
+ * Resolve target app from port or identifier.
74
+ * Returns the appropriate session based on the routing logic.
75
+ */
76
+ export function resolveTargetApp(portOrIdentifier) {
77
+ if (activeSessions.size === 0) {
78
+ throw new Error('No active session. Call tauri_driver_session with action "start" first to connect to a Tauri app.');
79
+ }
80
+ // Single app - return it
81
+ if (activeSessions.size === 1) {
82
+ const session = activeSessions.values().next().value;
83
+ if (!session) {
84
+ throw new Error('Session state inconsistent');
85
+ }
86
+ return session;
87
+ }
88
+ // Multiple apps - need identifier or use default
89
+ if (portOrIdentifier !== undefined) {
90
+ // Try port lookup first
91
+ const port = typeof portOrIdentifier === 'number'
92
+ ? portOrIdentifier
93
+ : parseInt(String(portOrIdentifier), 10);
94
+ if (!isNaN(port) && activeSessions.has(port)) {
95
+ const session = activeSessions.get(port);
96
+ if (session) {
97
+ return session;
98
+ }
99
+ }
100
+ // Try identifier match
101
+ for (const session of activeSessions.values()) {
102
+ if (session.identifier === String(portOrIdentifier)) {
103
+ return session;
104
+ }
105
+ }
106
+ throw new Error(formatAppNotFoundError(portOrIdentifier));
107
+ }
108
+ // Use default app
109
+ if (defaultPort !== null && activeSessions.has(defaultPort)) {
110
+ const session = activeSessions.get(defaultPort);
111
+ if (session) {
112
+ return session;
113
+ }
114
+ }
115
+ throw new Error('No default app set. This should not happen.');
116
+ }
117
+ /**
118
+ * Format error message when app not found.
119
+ */
120
+ function formatAppNotFoundError(identifier) {
121
+ const appList = Array.from(activeSessions.values())
122
+ .map((session) => {
123
+ const isDefault = session.port === defaultPort;
124
+ const defaultMarker = isDefault ? ' [DEFAULT]' : '';
125
+ return ` - ${session.port}: ${session.identifier || 'unknown'} (${session.host}:${session.port})${defaultMarker}`;
126
+ })
127
+ .join('\n');
128
+ return (`App "${identifier}" not found.\n\n` +
129
+ `Connected apps:\n${appList}\n\n` +
130
+ 'Use tauri_driver_session with action "status" to list all connected apps.');
131
+ }
132
+ /**
133
+ * Promote the next default app when the current default is removed.
134
+ * Selects the oldest remaining session (first in insertion order).
135
+ */
136
+ function promoteNextDefault() {
137
+ if (activeSessions.size > 0) {
138
+ // Get first session (oldest)
139
+ const firstSession = activeSessions.values().next().value;
140
+ if (firstSession) {
141
+ defaultPort = firstSession.port;
142
+ sessionLogger.info(`Promoted port ${defaultPort} as new default app`);
143
+ }
144
+ else {
145
+ defaultPort = null;
146
+ }
147
+ }
148
+ else {
149
+ defaultPort = null;
150
+ }
151
+ }
152
+ async function handleStatusAction() {
153
+ if (activeSessions.size === 0) {
154
+ return JSON.stringify({
155
+ connected: false,
156
+ app: null,
157
+ identifier: null,
158
+ host: null,
159
+ port: null,
160
+ });
161
+ }
162
+ if (activeSessions.size === 1) {
163
+ const session = activeSessions.values().next().value;
164
+ if (!session) {
165
+ return JSON.stringify({
166
+ connected: false,
167
+ app: null,
168
+ identifier: null,
169
+ host: null,
170
+ port: null,
171
+ });
172
+ }
173
+ return JSON.stringify({
174
+ connected: true,
175
+ app: session.name,
176
+ identifier: session.identifier,
177
+ host: session.host,
178
+ port: session.port,
179
+ });
180
+ }
181
+ const apps = Array.from(activeSessions.values()).map((session) => {
182
+ return {
183
+ name: session.name,
184
+ identifier: session.identifier,
185
+ host: session.host,
186
+ port: session.port,
187
+ isDefault: session.port === defaultPort,
188
+ };
189
+ });
190
+ return JSON.stringify({
191
+ connected: true,
192
+ apps,
193
+ totalCount: apps.length,
194
+ defaultPort,
195
+ });
196
+ }
197
+ async function handleStartAction(host, port) {
198
+ const configuredHost = host ?? getDefaultHost();
199
+ const configuredPort = port ?? getDefaultPort();
200
+ if (activeSessions.has(configuredPort)) {
201
+ return `Already connected to app on port ${configuredPort}`;
202
+ }
203
+ let connectedSession = null;
204
+ if (configuredHost !== 'localhost' && configuredHost !== '127.0.0.1') {
205
+ try {
206
+ connectedSession = await tryConnect('localhost', configuredPort);
207
+ }
208
+ catch {
209
+ // ignore
210
+ }
211
+ }
212
+ if (!connectedSession) {
213
+ try {
214
+ connectedSession = await tryConnect(configuredHost, configuredPort);
215
+ }
216
+ catch {
217
+ // ignore
218
+ }
219
+ }
220
+ if (!connectedSession) {
221
+ const localhostDiscovery = getAppDiscovery('localhost');
222
+ const firstApp = await localhostDiscovery.getFirstAvailableApp();
223
+ if (firstApp) {
224
+ try {
225
+ connectedSession = await tryConnect('localhost', firstApp.port);
226
+ }
227
+ catch {
228
+ // ignore
229
+ }
230
+ }
231
+ }
232
+ if (!connectedSession) {
233
+ return `Session start failed - no Tauri app found at localhost or ${configuredHost}:${configuredPort}`;
234
+ }
235
+ const client = new PluginClient(connectedSession.host, connectedSession.port);
236
+ await client.connect();
237
+ const identifier = await fetchAppIdentifier(client);
238
+ const sessionInfo = {
239
+ name: connectedSession.name,
240
+ identifier,
241
+ host: connectedSession.host,
242
+ port: connectedSession.port,
243
+ client,
244
+ connected: true,
245
+ };
246
+ activeSessions.set(connectedSession.port, sessionInfo);
247
+ defaultPort = connectedSession.port;
248
+ sessionLogger.info(`Session started: ${sessionInfo.name} (${sessionInfo.host}:${sessionInfo.port}) [DEFAULT]`);
249
+ return `Session started with app: ${sessionInfo.name} (${sessionInfo.host}:${sessionInfo.port}) [DEFAULT]`;
250
+ }
251
+ async function handleStopAction(appIdentifier) {
252
+ if (appIdentifier !== undefined) {
253
+ const session = resolveTargetApp(appIdentifier);
254
+ session.client.disconnect();
255
+ activeSessions.delete(session.port);
256
+ if (session.port === defaultPort) {
257
+ promoteNextDefault();
258
+ }
259
+ sessionLogger.info(`Session stopped: ${session.name} (${session.host}:${session.port})`);
260
+ return `Session stopped: ${session.name} (${session.host}:${session.port})`;
261
+ }
262
+ for (const session of activeSessions.values()) {
263
+ session.client.disconnect();
264
+ }
265
+ activeSessions.clear();
266
+ defaultPort = null;
267
+ if (appDiscovery) {
268
+ await appDiscovery.disconnectAll();
269
+ }
270
+ resetInitialization();
271
+ sessionLogger.info('All sessions stopped');
272
+ return 'All sessions stopped';
273
+ }
51
274
  // ============================================================================
52
275
  // Session Management
53
276
  // ============================================================================
@@ -66,18 +289,24 @@ async function tryConnect(host, port) {
66
289
  }
67
290
  /**
68
291
  * Fetch the app identifier from the backend state.
69
- * Must be called after the singleton pluginClient is connected.
292
+ * Must be called after a PluginClient is connected.
70
293
  *
294
+ * @param client - The PluginClient to query
71
295
  * @returns The app identifier (bundle ID) or null if not available. Returns null when:
72
296
  * - The plugin doesn't support the identifier field (older versions)
73
297
  * - The backend state request fails
74
298
  * - The identifier field is missing from the response
75
299
  */
76
- async function fetchAppIdentifier() {
300
+ async function fetchAppIdentifier(client) {
77
301
  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);
302
+ const response = await client.sendCommand({
303
+ command: 'invoke_tauri',
304
+ args: { command: 'plugin:mcp-bridge|get_backend_state', args: {} },
305
+ });
306
+ if (!response.success || !response.data) {
307
+ return null;
308
+ }
309
+ const state = response.data;
81
310
  // Return null if identifier is not present (backward compat with older plugins)
82
311
  return state.app?.identifier ?? null;
83
312
  }
@@ -98,113 +327,23 @@ async function fetchAppIdentifier() {
98
327
  * @param action - 'start', 'stop', or 'status'
99
328
  * @param host - Optional host address (defaults to env var or localhost)
100
329
  * @param port - Optional port number (defaults to 9223)
330
+ * @param appIdentifier - Optional app identifier for 'stop' action (port or bundle ID)
101
331
  * @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)
332
+ * For 'status': A JSON string with connection details
108
333
  */
109
- export async function manageDriverSession(action, host, port) {
110
- // Handle status action
111
- if (action === 'status') {
112
- const client = getExistingPluginClient();
113
- if (client?.isConnected() && currentSession) {
114
- return JSON.stringify({
115
- connected: true,
116
- app: currentSession.name,
117
- identifier: currentSession.identifier,
118
- host: currentSession.host,
119
- port: currentSession.port,
120
- });
334
+ export async function manageDriverSession(action, host, port, appIdentifier) {
335
+ switch (action) {
336
+ case 'status': {
337
+ return handleStatusAction();
121
338
  }
122
- return JSON.stringify({
123
- connected: false,
124
- app: null,
125
- identifier: null,
126
- host: null,
127
- port: null,
128
- });
129
- }
130
- if (action === 'start') {
131
- // Reset any existing connections to ensure fresh connection
132
- if (appDiscovery) {
133
- await appDiscovery.disconnectAll();
134
- }
135
- resetPluginClient();
136
- const configuredHost = host ?? getDefaultHost();
137
- const configuredPort = port ?? getDefaultPort();
138
- // Strategy 1: Try localhost first (most reliable)
139
- if (configuredHost !== 'localhost' && configuredHost !== '127.0.0.1') {
140
- try {
141
- const session = await tryConnect('localhost', configuredPort);
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 };
147
- return `Session started with app: ${session.name} (localhost:${session.port})`;
148
- }
149
- catch {
150
- // Localhost failed, will try configured host next
151
- }
339
+ case 'start': {
340
+ return handleStartAction(host, port);
152
341
  }
153
- // Strategy 2: Try the configured/provided host
154
- try {
155
- const session = await tryConnect(configuredHost, configuredPort);
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 };
161
- return `Session started with app: ${session.name} (${session.host}:${session.port})`;
342
+ case 'stop': {
343
+ return handleStopAction(appIdentifier);
162
344
  }
163
- catch {
164
- // Configured host failed
345
+ default: {
346
+ return handleStopAction(appIdentifier);
165
347
  }
166
- // Strategy 3: Auto-discover on localhost (scan port range)
167
- const localhostDiscovery = getAppDiscovery('localhost');
168
- const firstApp = await localhostDiscovery.getFirstAvailableApp();
169
- if (firstApp) {
170
- try {
171
- // Reset client again to connect to discovered port
172
- resetPluginClient();
173
- const session = await tryConnect('localhost', firstApp.port);
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 };
179
- return `Session started with app: ${session.name} (localhost:${session.port})`;
180
- }
181
- catch {
182
- // Discovery found app but connection failed
183
- }
184
- }
185
- // Strategy 4: Try default port on configured host as last resort
186
- try {
187
- resetPluginClient();
188
- const session = await tryConnect(configuredHost, configuredPort);
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 };
194
- return `Session started with app: ${session.name} (${session.host}:${session.port})`;
195
- }
196
- catch {
197
- // All attempts failed
198
- currentSession = null;
199
- return `Session started (native IPC mode - no Tauri app found at localhost or ${configuredHost}:${configuredPort})`;
200
- }
201
- }
202
- // Stop action - disconnect all apps and reset initialization state
203
- if (appDiscovery) {
204
- await appDiscovery.disconnectAll();
205
348
  }
206
- resetPluginClient();
207
- resetInitialization();
208
- currentSession = null;
209
- return 'Session stopped';
210
349
  }
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { getPluginClient, connectPlugin } from './plugin-client.js';
3
- import { hasActiveSession, getCurrentSession } from './session-manager.js';
2
+ import { connectPlugin } from './plugin-client.js';
3
+ import { hasActiveSession, getDefaultSession, resolveTargetApp } from './session-manager.js';
4
4
  import { createMcpLogger } from '../logger.js';
5
5
  import { buildScreenshotScript, buildScreenshotCaptureScript, getHtml2CanvasSource, HTML2CANVAS_SCRIPT_ID, } from './scripts/html2canvas-loader.js';
6
6
  import { registerScript, isScriptRegistered } from './script-manager.js';
@@ -39,8 +39,8 @@ export async function ensureReady() {
39
39
  if (!hasActiveSession()) {
40
40
  throw new Error('No active session. Call tauri_driver_session with action "start" first to connect to a Tauri app.');
41
41
  }
42
- // Get session config and connect with explicit host/port
43
- const session = getCurrentSession();
42
+ // Get default session for initial connection
43
+ const session = getDefaultSession();
44
44
  if (session) {
45
45
  await connectPlugin(session.host, session.port);
46
46
  }
@@ -57,10 +57,11 @@ export function resetInitialization() {
57
57
  *
58
58
  * @param script - JavaScript code to execute in the webview context
59
59
  * @param windowId - Optional window label to target (defaults to "main")
60
+ * @param appIdentifier - Optional app identifier to target specific app
60
61
  * @returns Result of the script execution with window context
61
62
  */
62
- export async function executeInWebview(script, windowId) {
63
- const { result } = await executeInWebviewWithContext(script, windowId);
63
+ export async function executeInWebview(script, windowId, appIdentifier) {
64
+ const { result } = await executeInWebviewWithContext(script, windowId, appIdentifier);
64
65
  return result;
65
66
  }
66
67
  /**
@@ -68,18 +69,16 @@ export async function executeInWebview(script, windowId) {
68
69
  *
69
70
  * @param script - JavaScript code to execute in the webview context
70
71
  * @param windowId - Optional window label to target (defaults to "main")
72
+ * @param appIdentifier - Optional app identifier to target specific app
71
73
  * @returns Result of the script execution with window context
72
74
  */
73
- export async function executeInWebviewWithContext(script, windowId) {
75
+ export async function executeInWebviewWithContext(script, windowId, appIdentifier) {
74
76
  try {
75
77
  // Ensure we're fully initialized
76
78
  await ensureReady();
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);
79
+ // Resolve target session
80
+ const session = resolveTargetApp(appIdentifier);
81
+ const client = session.client;
83
82
  // Send script directly - Rust handles wrapping and IPC callbacks.
84
83
  // Use 7s timeout (longer than Rust's 5s) so errors return before Node times out.
85
84
  const response = await client.sendCommand({
@@ -185,9 +184,11 @@ export async function initializeConsoleCapture() {
185
184
  *
186
185
  * @param filter - Optional regex pattern to filter log messages
187
186
  * @param since - Optional ISO timestamp to filter logs after this time
187
+ * @param windowId - Optional window label to target (defaults to "main")
188
+ * @param appIdentifier - Optional app identifier to target specific app
188
189
  * @returns Formatted console logs as string
189
190
  */
190
- export async function getConsoleLogs(filter, since) {
191
+ export async function getConsoleLogs(filter, since, windowId, appIdentifier) {
191
192
  const filterStr = filter ? filter.replace(/'/g, '\\\'') : '';
192
193
  const sinceStr = since || '';
193
194
  const script = `
@@ -212,7 +213,7 @@ export async function getConsoleLogs(filter, since) {
212
213
  '[ ' + new Date(l.timestamp).toISOString() + ' ] [ ' + l.level.toUpperCase() + ' ] ' + l.message
213
214
  ).join('\\n');
214
215
  `;
215
- return executeInWebview(script);
216
+ return executeInWebview(script, windowId, appIdentifier);
216
217
  }
217
218
  /**
218
219
  * Clear all captured console logs.
@@ -284,11 +285,11 @@ async function prepareHtml2canvasScript(format, quality) {
284
285
  /**
285
286
  * Capture a screenshot of the entire webview.
286
287
  *
287
- * @param options - Screenshot options (format, quality, windowId)
288
+ * @param options - Screenshot options (format, quality, windowId, appIdentifier)
288
289
  * @returns Screenshot result with image content
289
290
  */
290
291
  export async function captureScreenshot(options = {}) {
291
- const { format = 'png', quality = 90, windowId } = options;
292
+ const { format = 'png', quality = 90, windowId, appIdentifier } = options;
292
293
  // Primary implementation: Use native platform-specific APIs
293
294
  // - macOS: WKWebView takeSnapshot
294
295
  // - Windows: WebView2 CapturePreview
@@ -296,7 +297,9 @@ export async function captureScreenshot(options = {}) {
296
297
  try {
297
298
  // Ensure we're fully initialized
298
299
  await ensureReady();
299
- const client = getPluginClient();
300
+ // Resolve target session
301
+ const session = resolveTargetApp(appIdentifier);
302
+ const client = session.client;
300
303
  // Use longer timeout (15s) for native screenshot - the Rust code waits up to 10s
301
304
  const response = await client.sendCommand({
302
305
  command: 'capture_native_screenshot',
@@ -7,11 +7,12 @@ import { SCRIPTS, buildScript, buildTypeScript, buildKeyEventScript } from './sc
7
7
  // Base Schema for Window Targeting
8
8
  // ============================================================================
9
9
  /**
10
- * Base schema mixin for tools that can target a specific window.
11
- * All webview tools extend this to support multi-window applications.
10
+ * Base schema mixin for tools that can target a specific window and app.
11
+ * All webview tools extend this to support multi-window and multi-app scenarios.
12
12
  */
13
13
  export const WindowTargetSchema = z.object({
14
14
  windowId: z.string().optional().describe('Window label to target (defaults to "main")'),
15
+ appIdentifier: z.union([z.string(), z.number()]).optional().describe('App port or bundle ID to target. Defaults to the only connected app or the default app if multiple are connected.'),
15
16
  });
16
17
  // ============================================================================
17
18
  // Schemas
@@ -76,17 +77,17 @@ export const GetConsoleLogsSchema = WindowTargetSchema.extend({
76
77
  // Implementation Functions
77
78
  // ============================================================================
78
79
  export async function interact(options) {
79
- const { action, selector, x, y, duration, scrollX, scrollY, fromX, fromY, toX, toY, windowId } = options;
80
+ const { action, selector, x, y, duration, scrollX, scrollY, fromX, fromY, toX, toY, windowId, appIdentifier } = options;
80
81
  // Handle swipe action separately since it has different logic
81
82
  if (action === 'swipe') {
82
- return performSwipe({ fromX, fromY, toX, toY, duration, windowId });
83
+ return performSwipe({ fromX, fromY, toX, toY, duration, windowId, appIdentifier });
83
84
  }
84
85
  // Handle focus action
85
86
  if (action === 'focus') {
86
87
  if (!selector) {
87
88
  throw new Error('Focus action requires a selector');
88
89
  }
89
- return focusElement({ selector, windowId });
90
+ return focusElement({ selector, windowId, appIdentifier });
90
91
  }
91
92
  const script = buildScript(SCRIPTS.interact, {
92
93
  action,
@@ -98,7 +99,7 @@ export async function interact(options) {
98
99
  scrollY: scrollY ?? 0,
99
100
  });
100
101
  try {
101
- return await executeInWebview(script, windowId);
102
+ return await executeInWebview(script, windowId, appIdentifier);
102
103
  }
103
104
  catch (error) {
104
105
  const message = error instanceof Error ? error.message : String(error);
@@ -106,13 +107,13 @@ export async function interact(options) {
106
107
  }
107
108
  }
108
109
  async function performSwipe(options) {
109
- const { fromX, fromY, toX, toY, duration = 300, windowId } = options;
110
+ const { fromX, fromY, toX, toY, duration = 300, windowId, appIdentifier } = options;
110
111
  if (fromX === undefined || fromY === undefined || toX === undefined || toY === undefined) {
111
112
  throw new Error('Swipe action requires fromX, fromY, toX, and toY coordinates');
112
113
  }
113
114
  const script = buildScript(SCRIPTS.swipe, { fromX, fromY, toX, toY, duration });
114
115
  try {
115
- return await executeInWebview(script, windowId);
116
+ return await executeInWebview(script, windowId, appIdentifier);
116
117
  }
117
118
  catch (error) {
118
119
  const message = error instanceof Error ? error.message : String(error);
@@ -120,9 +121,9 @@ async function performSwipe(options) {
120
121
  }
121
122
  }
122
123
  export async function screenshot(options = {}) {
123
- const { quality, format = 'png', windowId, filePath } = options;
124
+ const { quality, format = 'png', windowId, filePath, appIdentifier } = options;
124
125
  // Use the native screenshot function from webview-executor
125
- const result = await captureScreenshot({ format, quality, windowId });
126
+ const result = await captureScreenshot({ format, quality, windowId, appIdentifier });
126
127
  // If filePath is provided, write to file instead of returning base64
127
128
  if (filePath) {
128
129
  // Find the image content in the result
@@ -139,7 +140,7 @@ export async function screenshot(options = {}) {
139
140
  return result;
140
141
  }
141
142
  export async function keyboard(options) {
142
- const { action, selectorOrKey, textOrModifiers, modifiers, windowId } = options;
143
+ const { action, selectorOrKey, textOrModifiers, modifiers, windowId, appIdentifier } = options;
143
144
  // Handle the different parameter combinations based on action
144
145
  if (action === 'type') {
145
146
  const selector = selectorOrKey;
@@ -149,7 +150,7 @@ export async function keyboard(options) {
149
150
  }
150
151
  const script = buildTypeScript(selector, text);
151
152
  try {
152
- return await executeInWebview(script, windowId);
153
+ return await executeInWebview(script, windowId, appIdentifier);
153
154
  }
154
155
  catch (error) {
155
156
  const message = error instanceof Error ? error.message : String(error);
@@ -164,7 +165,7 @@ export async function keyboard(options) {
164
165
  }
165
166
  const script = buildKeyEventScript(action, key, mods || []);
166
167
  try {
167
- return await executeInWebview(script, windowId);
168
+ return await executeInWebview(script, windowId, appIdentifier);
168
169
  }
169
170
  catch (error) {
170
171
  const message = error instanceof Error ? error.message : String(error);
@@ -172,10 +173,10 @@ export async function keyboard(options) {
172
173
  }
173
174
  }
174
175
  export async function waitFor(options) {
175
- const { type, value, timeout = 5000, windowId } = options;
176
+ const { type, value, timeout = 5000, windowId, appIdentifier } = options;
176
177
  const script = buildScript(SCRIPTS.waitFor, { type, value, timeout });
177
178
  try {
178
- return await executeInWebview(script, windowId);
179
+ return await executeInWebview(script, windowId, appIdentifier);
179
180
  }
180
181
  catch (error) {
181
182
  const message = error instanceof Error ? error.message : String(error);
@@ -183,14 +184,14 @@ export async function waitFor(options) {
183
184
  }
184
185
  }
185
186
  export async function getStyles(options) {
186
- const { selector, properties, multiple = false, windowId } = options;
187
+ const { selector, properties, multiple = false, windowId, appIdentifier } = options;
187
188
  const script = buildScript(SCRIPTS.getStyles, {
188
189
  selector,
189
190
  properties: properties || [],
190
191
  multiple,
191
192
  });
192
193
  try {
193
- return await executeInWebview(script, windowId);
194
+ return await executeInWebview(script, windowId, appIdentifier);
194
195
  }
195
196
  catch (error) {
196
197
  const message = error instanceof Error ? error.message : String(error);
@@ -198,7 +199,7 @@ export async function getStyles(options) {
198
199
  }
199
200
  }
200
201
  export async function executeJavaScript(options) {
201
- const { script, args, windowId } = options;
202
+ const { script, args, windowId, appIdentifier } = options;
202
203
  // If args are provided, we need to inject them into the script context
203
204
  const wrappedScript = args && args.length > 0
204
205
  ? `
@@ -209,7 +210,7 @@ export async function executeJavaScript(options) {
209
210
  `
210
211
  : script;
211
212
  try {
212
- const { result, windowLabel, warning } = await executeInWebviewWithContext(wrappedScript, windowId);
213
+ const { result, windowLabel, warning } = await executeInWebviewWithContext(wrappedScript, windowId, appIdentifier);
213
214
  // Build response with window context
214
215
  let response = result;
215
216
  if (warning) {
@@ -225,10 +226,10 @@ export async function executeJavaScript(options) {
225
226
  }
226
227
  }
227
228
  export async function focusElement(options) {
228
- const { selector, windowId } = options;
229
+ const { selector, windowId, appIdentifier } = options;
229
230
  const script = buildScript(SCRIPTS.focus, { selector });
230
231
  try {
231
- return await executeInWebview(script, windowId);
232
+ return await executeInWebview(script, windowId, appIdentifier);
232
233
  }
233
234
  catch (error) {
234
235
  const message = error instanceof Error ? error.message : String(error);
@@ -239,10 +240,10 @@ export async function focusElement(options) {
239
240
  * Find an element using various selector strategies.
240
241
  */
241
242
  export async function findElement(options) {
242
- const { selector, strategy, windowId } = options;
243
+ const { selector, strategy, windowId, appIdentifier } = options;
243
244
  const script = buildScript(SCRIPTS.findElement, { selector, strategy });
244
245
  try {
245
- return await executeInWebview(script, windowId);
246
+ return await executeInWebview(script, windowId, appIdentifier);
246
247
  }
247
248
  catch (error) {
248
249
  const message = error instanceof Error ? error.message : String(error);
@@ -253,9 +254,9 @@ export async function findElement(options) {
253
254
  * Get console logs from the webview.
254
255
  */
255
256
  export async function getConsoleLogs(options = {}) {
256
- const { filter, since } = options;
257
+ const { filter, since, windowId, appIdentifier } = options;
257
258
  try {
258
- return await getConsoleLogsFromCapture(filter, since);
259
+ return await getConsoleLogsFromCapture(filter, since, windowId, appIdentifier);
259
260
  }
260
261
  catch (error) {
261
262
  const message = error instanceof Error ? error.message : String(error);
@@ -8,14 +8,15 @@ export const ReadLogsSchema = z.object({
8
8
  filter: z.string().optional().describe('Regex or keyword to filter logs'),
9
9
  since: z.string().optional().describe('ISO timestamp to filter logs since (e.g. 2023-10-27T10:00:00Z)'),
10
10
  windowId: z.string().optional().describe('Window label for console logs (defaults to "main")'),
11
+ 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.'),
11
12
  });
12
13
  export async function readLogs(options) {
13
- const { source, lines = 50, filter, since, windowId } = options;
14
+ const { source, lines = 50, filter, since, windowId, appIdentifier } = options;
14
15
  try {
15
16
  let output = '';
16
17
  // Handle console logs (webview JS logs)
17
18
  if (source === 'console') {
18
- return await getConsoleLogs({ filter, since, windowId });
19
+ return await getConsoleLogs({ filter, since, windowId, appIdentifier });
19
20
  }
20
21
  if (source === 'android') {
21
22
  // Find adb - check ANDROID_HOME first, then fall back to PATH
@@ -8,6 +8,11 @@ import { manageDriverSession, ManageDriverSessionSchema, } from './driver/sessio
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
10
  import { interact, screenshot, keyboard, waitFor, getStyles, executeJavaScript, findElement, InteractSchema, ScreenshotSchema, KeyboardSchema, WaitForSchema, GetStylesSchema, ExecuteJavaScriptSchema, FindElementSchema, } from './driver/webview-interactions.js';
11
+ /**
12
+ * Standard multi-app description for webview tools.
13
+ */
14
+ const MULTI_APP_DESC = 'Targets the only connected app, or the default app if multiple are connected. ' +
15
+ 'Specify appIdentifier (port or bundle ID) to target a specific app.';
11
16
  /**
12
17
  * Tool categories for organization
13
18
  */
@@ -136,11 +141,12 @@ export const TOOLS = [
136
141
  {
137
142
  name: 'tauri_driver_session',
138
143
  description: '[Tauri Apps Only] Start/stop automation session to connect to a RUNNING Tauri app. ' +
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. ' +
144
+ 'Supports multiple concurrent app connections - each app runs on a unique port. ' +
145
+ 'The most recently connected app becomes the "default" app used when no appIdentifier is specified. ' +
146
+ 'Use action "status" to check connection state: returns single app format when 1 app connected, ' +
147
+ 'or array format with "isDefault" indicator when multiple apps connected. ' +
148
+ 'Action "stop" without appIdentifier stops ALL sessions; with appIdentifier stops only that app. ' +
149
+ 'The identifier field (e.g., "com.example.myapp") uniquely identifies each app. ' +
144
150
  'REQUIRED before using other tauri_webview_* or tauri_plugin_* tools. ' +
145
151
  'Connects via WebSocket to the MCP Bridge plugin in the Tauri app. ' +
146
152
  'For browser automation, use Chrome DevTools MCP instead. ' +
@@ -156,13 +162,14 @@ export const TOOLS = [
156
162
  },
157
163
  handler: async (args) => {
158
164
  const parsed = ManageDriverSessionSchema.parse(args);
159
- return await manageDriverSession(parsed.action, parsed.host, parsed.port);
165
+ return await manageDriverSession(parsed.action, parsed.host, parsed.port, parsed.appIdentifier);
160
166
  },
161
167
  },
162
168
  {
163
169
  name: 'tauri_webview_find_element',
164
170
  description: '[Tauri Apps Only] Find DOM elements in a running Tauri app\'s webview. ' +
165
171
  'Requires active tauri_driver_session. ' +
172
+ MULTI_APP_DESC + ' ' +
166
173
  'For browser pages or documentation sites, use Chrome DevTools MCP instead.',
167
174
  category: TOOL_CATEGORIES.UI_AUTOMATION,
168
175
  schema: FindElementSchema,
@@ -177,6 +184,7 @@ export const TOOLS = [
177
184
  selector: parsed.selector,
178
185
  strategy: parsed.strategy,
179
186
  windowId: parsed.windowId,
187
+ appIdentifier: parsed.appIdentifier,
180
188
  });
181
189
  },
182
190
  },
@@ -222,6 +230,7 @@ export const TOOLS = [
222
230
  name: 'tauri_webview_screenshot',
223
231
  description: '[Tauri Apps Only] Screenshot a running Tauri app\'s webview. ' +
224
232
  'Requires active tauri_driver_session. Captures only visible viewport. ' +
233
+ MULTI_APP_DESC + ' ' +
225
234
  'For browser screenshots, use Chrome DevTools MCP instead. ' +
226
235
  'For Electron apps, this will NOT work.',
227
236
  category: TOOL_CATEGORIES.UI_AUTOMATION,
@@ -238,6 +247,7 @@ export const TOOLS = [
238
247
  format: parsed.format,
239
248
  windowId: parsed.windowId,
240
249
  filePath: parsed.filePath,
250
+ appIdentifier: parsed.appIdentifier,
241
251
  });
242
252
  // If saved to file, return text confirmation
243
253
  if ('filePath' in result) {
@@ -251,6 +261,7 @@ export const TOOLS = [
251
261
  name: 'tauri_webview_keyboard',
252
262
  description: '[Tauri Apps Only] Type text or send keyboard events in a Tauri app. ' +
253
263
  'Requires active tauri_driver_session. ' +
264
+ MULTI_APP_DESC + ' ' +
254
265
  'For browser keyboard input, use Chrome DevTools MCP instead.',
255
266
  category: TOOL_CATEGORIES.UI_AUTOMATION,
256
267
  schema: KeyboardSchema,
@@ -268,6 +279,7 @@ export const TOOLS = [
268
279
  selectorOrKey: parsed.selector,
269
280
  textOrModifiers: parsed.text,
270
281
  windowId: parsed.windowId,
282
+ appIdentifier: parsed.appIdentifier,
271
283
  });
272
284
  }
273
285
  return await keyboard({
@@ -275,6 +287,7 @@ export const TOOLS = [
275
287
  selectorOrKey: parsed.key,
276
288
  textOrModifiers: parsed.modifiers,
277
289
  windowId: parsed.windowId,
290
+ appIdentifier: parsed.appIdentifier,
278
291
  });
279
292
  },
280
293
  },
@@ -282,6 +295,7 @@ export const TOOLS = [
282
295
  name: 'tauri_webview_wait_for',
283
296
  description: '[Tauri Apps Only] Wait for elements, text, or IPC events in a Tauri app. ' +
284
297
  'Requires active tauri_driver_session. ' +
298
+ MULTI_APP_DESC + ' ' +
285
299
  'For browser waits, use Chrome DevTools MCP instead.',
286
300
  category: TOOL_CATEGORIES.UI_AUTOMATION,
287
301
  schema: WaitForSchema,
@@ -297,6 +311,7 @@ export const TOOLS = [
297
311
  value: parsed.value,
298
312
  timeout: parsed.timeout,
299
313
  windowId: parsed.windowId,
314
+ appIdentifier: parsed.appIdentifier,
300
315
  });
301
316
  },
302
317
  },
@@ -304,6 +319,7 @@ export const TOOLS = [
304
319
  name: 'tauri_webview_get_styles',
305
320
  description: '[Tauri Apps Only] Get computed CSS styles from elements in a Tauri app. ' +
306
321
  'Requires active tauri_driver_session. ' +
322
+ MULTI_APP_DESC + ' ' +
307
323
  'For browser style inspection, use Chrome DevTools MCP instead.',
308
324
  category: TOOL_CATEGORIES.UI_AUTOMATION,
309
325
  schema: GetStylesSchema,
@@ -319,6 +335,7 @@ export const TOOLS = [
319
335
  properties: parsed.properties,
320
336
  multiple: parsed.multiple,
321
337
  windowId: parsed.windowId,
338
+ appIdentifier: parsed.appIdentifier,
322
339
  });
323
340
  },
324
341
  },
@@ -328,6 +345,7 @@ export const TOOLS = [
328
345
  'Requires active tauri_driver_session. Has access to window.__TAURI__. ' +
329
346
  'If you need a return value, it must be JSON-serializable. ' +
330
347
  'For functions that return values, use an IIFE: "(() => { return 5; })()" not "() => { return 5; }". ' +
348
+ MULTI_APP_DESC + ' ' +
331
349
  'For browser JS execution, use Chrome DevTools MCP instead.',
332
350
  category: TOOL_CATEGORIES.UI_AUTOMATION,
333
351
  schema: ExecuteJavaScriptSchema,
@@ -343,6 +361,7 @@ export const TOOLS = [
343
361
  script: parsed.script,
344
362
  args: parsed.args,
345
363
  windowId: parsed.windowId,
364
+ appIdentifier: parsed.appIdentifier,
346
365
  });
347
366
  },
348
367
  },
@@ -362,7 +381,11 @@ export const TOOLS = [
362
381
  },
363
382
  handler: async (args) => {
364
383
  const parsed = ExecuteIPCCommandSchema.parse(args);
365
- return await executeIPCCommand(parsed.command, parsed.args);
384
+ return await executeIPCCommand({
385
+ command: parsed.command,
386
+ args: parsed.args,
387
+ appIdentifier: parsed.appIdentifier,
388
+ });
366
389
  },
367
390
  },
368
391
  {
@@ -381,7 +404,7 @@ export const TOOLS = [
381
404
  },
382
405
  handler: async (args) => {
383
406
  const parsed = ManageIPCMonitoringSchema.parse(args);
384
- return await manageIPCMonitoring(parsed.action);
407
+ return await manageIPCMonitoring(parsed.action, parsed.appIdentifier);
385
408
  },
386
409
  },
387
410
  {
@@ -398,7 +421,7 @@ export const TOOLS = [
398
421
  },
399
422
  handler: async (args) => {
400
423
  const parsed = GetIPCEventsSchema.parse(args);
401
- return await getIPCEvents(parsed.filter);
424
+ return await getIPCEvents(parsed.filter, parsed.appIdentifier);
402
425
  },
403
426
  },
404
427
  {
@@ -416,7 +439,7 @@ export const TOOLS = [
416
439
  },
417
440
  handler: async (args) => {
418
441
  const parsed = EmitTestEventSchema.parse(args);
419
- return await emitTestEvent(parsed.eventName, parsed.payload);
442
+ return await emitTestEvent(parsed.eventName, parsed.payload, parsed.appIdentifier);
420
443
  },
421
444
  },
422
445
  {
@@ -431,8 +454,9 @@ export const TOOLS = [
431
454
  readOnlyHint: true,
432
455
  openWorldHint: false,
433
456
  },
434
- handler: async () => {
435
- return await getBackendState();
457
+ handler: async (args) => {
458
+ const parsed = GetBackendStateSchema.parse(args);
459
+ return await getBackendState({ appIdentifier: parsed.appIdentifier });
436
460
  },
437
461
  },
438
462
  // Window Management Tools
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@hypothesi/tauri-mcp-server",
3
- "version": "0.5.1",
4
- "description": "A Model Context Protocol server for Tauri v2 development",
3
+ "version": "0.6.2",
4
+ "mcpName": "io.github.hypothesi/tauri",
5
+ "description": "A Model Context Protocol server for use with Tauri v2 applications",
5
6
  "type": "module",
6
7
  "bin": {
7
8
  "mcp-server-tauri": "./dist/index.js"