@hypothesi/tauri-mcp-server 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -1
- package/dist/driver/plugin-client.js +36 -0
- package/dist/driver/plugin-commands.js +71 -34
- package/dist/driver/script-manager.js +5 -9
- package/dist/driver/session-manager.js +263 -110
- package/dist/driver/webview-executor.js +32 -13
- package/dist/driver/webview-interactions.js +26 -25
- package/dist/monitor/logs.js +3 -2
- package/dist/tools-registry.js +36 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@hypothesi/tauri-mcp-server)
|
|
4
4
|
[](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.
|
|
@@ -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,23 @@ 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
|
+
* @param appIdentifier - Optional app identifier to target specific app
|
|
229
|
+
* @throws Error if no session is active
|
|
230
|
+
*/
|
|
231
|
+
export async function ensureSessionAndConnect(appIdentifier) {
|
|
232
|
+
// Import dynamically to avoid circular dependency
|
|
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();
|
|
238
|
+
}
|
|
239
|
+
return session.client;
|
|
240
|
+
}
|
|
205
241
|
export async function disconnectPlugin() {
|
|
206
242
|
const client = getPluginClient();
|
|
207
243
|
client.disconnect();
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
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
|
+
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(
|
|
8
|
+
export async function executeIPCCommand(options) {
|
|
8
9
|
try {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const client =
|
|
10
|
+
const { command, args = {}, appIdentifier } = options;
|
|
11
|
+
// Ensure we have an active session and are connected
|
|
12
|
+
const client = await ensureSessionAndConnect(appIdentifier);
|
|
12
13
|
// Send IPC command via WebSocket to the mcp-bridge plugin
|
|
13
14
|
const response = await client.sendCommand({
|
|
14
15
|
command: 'invoke_tauri',
|
|
@@ -27,19 +28,20 @@ export async function executeIPCCommand(command, args = {}) {
|
|
|
27
28
|
// Combined schema for managing IPC monitoring
|
|
28
29
|
export const ManageIPCMonitoringSchema = z.object({
|
|
29
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.'),
|
|
30
32
|
});
|
|
31
33
|
// Keep individual schemas for backward compatibility if needed
|
|
32
34
|
export const StartIPCMonitoringSchema = z.object({});
|
|
33
35
|
export const StopIPCMonitoringSchema = z.object({});
|
|
34
|
-
export async function manageIPCMonitoring(action) {
|
|
36
|
+
export async function manageIPCMonitoring(action, appIdentifier) {
|
|
35
37
|
if (action === 'start') {
|
|
36
|
-
return startIPCMonitoring();
|
|
38
|
+
return startIPCMonitoring(appIdentifier);
|
|
37
39
|
}
|
|
38
|
-
return stopIPCMonitoring();
|
|
40
|
+
return stopIPCMonitoring(appIdentifier);
|
|
39
41
|
}
|
|
40
|
-
export async function startIPCMonitoring() {
|
|
42
|
+
export async function startIPCMonitoring(appIdentifier) {
|
|
41
43
|
try {
|
|
42
|
-
const result = await executeIPCCommand('plugin:mcp-bridge|start_ipc_monitor');
|
|
44
|
+
const result = await executeIPCCommand({ command: 'plugin:mcp-bridge|start_ipc_monitor', appIdentifier });
|
|
43
45
|
const parsed = JSON.parse(result);
|
|
44
46
|
if (!parsed.success) {
|
|
45
47
|
throw new Error(parsed.error || 'Unknown error');
|
|
@@ -51,9 +53,9 @@ export async function startIPCMonitoring() {
|
|
|
51
53
|
throw new Error(`Failed to start IPC monitoring: ${message}`);
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
|
-
export async function stopIPCMonitoring() {
|
|
56
|
+
export async function stopIPCMonitoring(appIdentifier) {
|
|
55
57
|
try {
|
|
56
|
-
const result = await executeIPCCommand('plugin:mcp-bridge|stop_ipc_monitor');
|
|
58
|
+
const result = await executeIPCCommand({ command: 'plugin:mcp-bridge|stop_ipc_monitor', appIdentifier });
|
|
57
59
|
const parsed = JSON.parse(result);
|
|
58
60
|
if (!parsed.success) {
|
|
59
61
|
throw new Error(parsed.error || 'Unknown error');
|
|
@@ -67,10 +69,11 @@ export async function stopIPCMonitoring() {
|
|
|
67
69
|
}
|
|
68
70
|
export const GetIPCEventsSchema = z.object({
|
|
69
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.'),
|
|
70
73
|
});
|
|
71
|
-
export async function getIPCEvents(filter) {
|
|
74
|
+
export async function getIPCEvents(filter, appIdentifier) {
|
|
72
75
|
try {
|
|
73
|
-
const result = await executeIPCCommand('plugin:mcp-bridge|get_ipc_events');
|
|
76
|
+
const result = await executeIPCCommand({ command: 'plugin:mcp-bridge|get_ipc_events', appIdentifier });
|
|
74
77
|
const parsed = JSON.parse(result);
|
|
75
78
|
if (!parsed.success) {
|
|
76
79
|
throw new Error(parsed.error || 'Unknown error');
|
|
@@ -92,12 +95,17 @@ export async function getIPCEvents(filter) {
|
|
|
92
95
|
export const EmitTestEventSchema = z.object({
|
|
93
96
|
eventName: z.string(),
|
|
94
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.'),
|
|
95
99
|
});
|
|
96
|
-
export async function emitTestEvent(eventName, payload) {
|
|
100
|
+
export async function emitTestEvent(eventName, payload, appIdentifier) {
|
|
97
101
|
try {
|
|
98
|
-
const result = await executeIPCCommand(
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
const result = await executeIPCCommand({
|
|
103
|
+
command: 'plugin:mcp-bridge|emit_event',
|
|
104
|
+
args: {
|
|
105
|
+
eventName,
|
|
106
|
+
payload,
|
|
107
|
+
},
|
|
108
|
+
appIdentifier,
|
|
101
109
|
});
|
|
102
110
|
const parsed = JSON.parse(result);
|
|
103
111
|
if (!parsed.success) {
|
|
@@ -110,10 +118,12 @@ export async function emitTestEvent(eventName, payload) {
|
|
|
110
118
|
throw new Error(`Failed to emit event: ${message}`);
|
|
111
119
|
}
|
|
112
120
|
}
|
|
113
|
-
export const GetWindowInfoSchema = z.object({
|
|
114
|
-
|
|
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) {
|
|
115
125
|
try {
|
|
116
|
-
const result = await executeIPCCommand('plugin:mcp-bridge|get_window_info');
|
|
126
|
+
const result = await executeIPCCommand({ command: 'plugin:mcp-bridge|get_window_info', appIdentifier });
|
|
117
127
|
const parsed = JSON.parse(result);
|
|
118
128
|
if (!parsed.success) {
|
|
119
129
|
throw new Error(parsed.error || 'Unknown error');
|
|
@@ -125,10 +135,39 @@ export async function getWindowInfo() {
|
|
|
125
135
|
throw new Error(`Failed to get window info: ${message}`);
|
|
126
136
|
}
|
|
127
137
|
}
|
|
128
|
-
export const GetBackendStateSchema = z.object({
|
|
129
|
-
|
|
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
|
+
});
|
|
141
|
+
/**
|
|
142
|
+
* Get backend state from the connected Tauri app.
|
|
143
|
+
*
|
|
144
|
+
* This function can work in two modes:
|
|
145
|
+
* 1. Normal mode: Requires an active session (for MCP tool calls)
|
|
146
|
+
* 2. Setup mode: Uses existing connected client (for session setup)
|
|
147
|
+
*
|
|
148
|
+
* @param useExistingClient If true, uses the existing connected client without
|
|
149
|
+
* session validation. Used during session setup before currentSession is set.
|
|
150
|
+
*/
|
|
151
|
+
export async function getBackendState(options = {}) {
|
|
130
152
|
try {
|
|
131
|
-
const
|
|
153
|
+
const { useExistingClient = false, appIdentifier } = options;
|
|
154
|
+
if (useExistingClient) {
|
|
155
|
+
// During session setup, use the already-connected client directly
|
|
156
|
+
const client = getExistingPluginClient();
|
|
157
|
+
if (!client || !client.isConnected()) {
|
|
158
|
+
throw new Error('No connected client available');
|
|
159
|
+
}
|
|
160
|
+
const response = await client.sendCommand({
|
|
161
|
+
command: 'invoke_tauri',
|
|
162
|
+
args: { command: 'plugin:mcp-bridge|get_backend_state', args: {} },
|
|
163
|
+
});
|
|
164
|
+
if (!response.success) {
|
|
165
|
+
throw new Error(response.error || 'Unknown error');
|
|
166
|
+
}
|
|
167
|
+
return JSON.stringify(response.data);
|
|
168
|
+
}
|
|
169
|
+
// Normal mode: use executeIPCCommand which validates session
|
|
170
|
+
const result = await executeIPCCommand({ command: 'plugin:mcp-bridge|get_backend_state', appIdentifier });
|
|
132
171
|
const parsed = JSON.parse(result);
|
|
133
172
|
if (!parsed.success) {
|
|
134
173
|
throw new Error(parsed.error || 'Unknown error');
|
|
@@ -147,10 +186,9 @@ export const ListWindowsSchema = z.object({});
|
|
|
147
186
|
/**
|
|
148
187
|
* Lists all open webview windows in the Tauri application.
|
|
149
188
|
*/
|
|
150
|
-
export async function listWindows() {
|
|
189
|
+
export async function listWindows(appIdentifier) {
|
|
151
190
|
try {
|
|
152
|
-
await
|
|
153
|
-
const client = getPluginClient();
|
|
191
|
+
const client = await ensureSessionAndConnect(appIdentifier);
|
|
154
192
|
const response = await client.sendCommand({
|
|
155
193
|
command: 'list_windows',
|
|
156
194
|
});
|
|
@@ -184,8 +222,7 @@ export const ResizeWindowSchema = z.object({
|
|
|
184
222
|
*/
|
|
185
223
|
export async function resizeWindow(options) {
|
|
186
224
|
try {
|
|
187
|
-
await
|
|
188
|
-
const client = getPluginClient();
|
|
225
|
+
const client = await ensureSessionAndConnect(options.appIdentifier);
|
|
189
226
|
const response = await client.sendCommand({
|
|
190
227
|
command: 'resize_window',
|
|
191
228
|
args: {
|
|
@@ -216,6 +253,7 @@ export const ManageWindowSchema = z.object({
|
|
|
216
253
|
.describe('Height in pixels (required for "resize" action)'),
|
|
217
254
|
logical: z.boolean().optional().default(true)
|
|
218
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.'),
|
|
219
257
|
});
|
|
220
258
|
/**
|
|
221
259
|
* Unified window management function.
|
|
@@ -229,15 +267,14 @@ export const ManageWindowSchema = z.object({
|
|
|
229
267
|
* @returns JSON string with the result
|
|
230
268
|
*/
|
|
231
269
|
export async function manageWindow(options) {
|
|
232
|
-
const { action, windowId, width, height, logical } = options;
|
|
270
|
+
const { action, windowId, width, height, logical, appIdentifier } = options;
|
|
233
271
|
switch (action) {
|
|
234
272
|
case 'list': {
|
|
235
|
-
return listWindows();
|
|
273
|
+
return listWindows(appIdentifier);
|
|
236
274
|
}
|
|
237
275
|
case 'info': {
|
|
238
276
|
try {
|
|
239
|
-
await
|
|
240
|
-
const client = getPluginClient();
|
|
277
|
+
const client = await ensureSessionAndConnect(appIdentifier);
|
|
241
278
|
const response = await client.sendCommand({
|
|
242
279
|
command: 'get_window_info',
|
|
243
280
|
args: { windowId },
|
|
@@ -256,7 +293,7 @@ export async function manageWindow(options) {
|
|
|
256
293
|
if (width === undefined || height === undefined) {
|
|
257
294
|
throw new Error('width and height are required for resize action');
|
|
258
295
|
}
|
|
259
|
-
return resizeWindow({ width, height, windowId, logical });
|
|
296
|
+
return resizeWindow({ width, height, windowId, logical, appIdentifier });
|
|
260
297
|
}
|
|
261
298
|
default: {
|
|
262
299
|
throw new Error(`Unknown action: ${action}`);
|
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,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 {
|
|
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,19 +23,254 @@ 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
|
-
|
|
31
|
-
|
|
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;
|
|
37
|
+
/**
|
|
38
|
+
* Check if any session is currently active.
|
|
39
|
+
* @returns true if at least one session exists
|
|
40
|
+
*/
|
|
41
|
+
export function hasActiveSession() {
|
|
42
|
+
return activeSessions.size > 0;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get a specific session by port.
|
|
46
|
+
*/
|
|
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());
|
|
65
|
+
}
|
|
32
66
|
function getAppDiscovery(host) {
|
|
33
67
|
if (!appDiscovery || appDiscovery.host !== host) {
|
|
34
68
|
appDiscovery = new AppDiscovery(host);
|
|
35
69
|
}
|
|
36
70
|
return appDiscovery;
|
|
37
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
|
+
}
|
|
38
274
|
// ============================================================================
|
|
39
275
|
// Session Management
|
|
40
276
|
// ============================================================================
|
|
@@ -53,17 +289,24 @@ async function tryConnect(host, port) {
|
|
|
53
289
|
}
|
|
54
290
|
/**
|
|
55
291
|
* Fetch the app identifier from the backend state.
|
|
56
|
-
* Must be called after
|
|
292
|
+
* Must be called after a PluginClient is connected.
|
|
57
293
|
*
|
|
294
|
+
* @param client - The PluginClient to query
|
|
58
295
|
* @returns The app identifier (bundle ID) or null if not available. Returns null when:
|
|
59
296
|
* - The plugin doesn't support the identifier field (older versions)
|
|
60
297
|
* - The backend state request fails
|
|
61
298
|
* - The identifier field is missing from the response
|
|
62
299
|
*/
|
|
63
|
-
async function fetchAppIdentifier() {
|
|
300
|
+
async function fetchAppIdentifier(client) {
|
|
64
301
|
try {
|
|
65
|
-
const
|
|
66
|
-
|
|
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;
|
|
67
310
|
// Return null if identifier is not present (backward compat with older plugins)
|
|
68
311
|
return state.app?.identifier ?? null;
|
|
69
312
|
}
|
|
@@ -84,113 +327,23 @@ async function fetchAppIdentifier() {
|
|
|
84
327
|
* @param action - 'start', 'stop', or 'status'
|
|
85
328
|
* @param host - Optional host address (defaults to env var or localhost)
|
|
86
329
|
* @param port - Optional port number (defaults to 9223)
|
|
330
|
+
* @param appIdentifier - Optional app identifier for 'stop' action (port or bundle ID)
|
|
87
331
|
* @returns For 'start'/'stop': A message string describing the result.
|
|
88
|
-
* For 'status': A JSON string with connection details
|
|
89
|
-
* - `connected`: boolean indicating if connected
|
|
90
|
-
* - `app`: app name (or null if not connected)
|
|
91
|
-
* - `identifier`: app bundle ID (e.g., "com.example.app"), or null
|
|
92
|
-
* - `host`: connected host (or null)
|
|
93
|
-
* - `port`: connected port (or null)
|
|
332
|
+
* For 'status': A JSON string with connection details
|
|
94
333
|
*/
|
|
95
|
-
export async function manageDriverSession(action, host, port) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (client.isConnected() && currentSession) {
|
|
100
|
-
return JSON.stringify({
|
|
101
|
-
connected: true,
|
|
102
|
-
app: currentSession.name,
|
|
103
|
-
identifier: currentSession.identifier,
|
|
104
|
-
host: currentSession.host,
|
|
105
|
-
port: currentSession.port,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
return JSON.stringify({
|
|
109
|
-
connected: false,
|
|
110
|
-
app: null,
|
|
111
|
-
identifier: null,
|
|
112
|
-
host: null,
|
|
113
|
-
port: null,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
if (action === 'start') {
|
|
117
|
-
// Reset any existing connections to ensure fresh connection
|
|
118
|
-
if (appDiscovery) {
|
|
119
|
-
await appDiscovery.disconnectAll();
|
|
334
|
+
export async function manageDriverSession(action, host, port, appIdentifier) {
|
|
335
|
+
switch (action) {
|
|
336
|
+
case 'status': {
|
|
337
|
+
return handleStatusAction();
|
|
120
338
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const configuredPort = port ?? getDefaultPort();
|
|
124
|
-
// Strategy 1: Try localhost first (most reliable)
|
|
125
|
-
if (configuredHost !== 'localhost' && configuredHost !== '127.0.0.1') {
|
|
126
|
-
try {
|
|
127
|
-
const session = await tryConnect('localhost', configuredPort);
|
|
128
|
-
// Connect the singleton pluginClient so status checks work
|
|
129
|
-
await connectPlugin(session.host, session.port);
|
|
130
|
-
// Fetch app identifier after singleton is connected
|
|
131
|
-
const identifier = await fetchAppIdentifier();
|
|
132
|
-
currentSession = { ...session, identifier };
|
|
133
|
-
return `Session started with app: ${session.name} (localhost:${session.port})`;
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
// Localhost failed, will try configured host next
|
|
137
|
-
}
|
|
339
|
+
case 'start': {
|
|
340
|
+
return handleStartAction(host, port);
|
|
138
341
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const session = await tryConnect(configuredHost, 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} (${session.host}:${session.port})`;
|
|
342
|
+
case 'stop': {
|
|
343
|
+
return handleStopAction(appIdentifier);
|
|
148
344
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
// Strategy 3: Auto-discover on localhost (scan port range)
|
|
153
|
-
const localhostDiscovery = getAppDiscovery('localhost');
|
|
154
|
-
const firstApp = await localhostDiscovery.getFirstAvailableApp();
|
|
155
|
-
if (firstApp) {
|
|
156
|
-
try {
|
|
157
|
-
// Reset client again to connect to discovered port
|
|
158
|
-
resetPluginClient();
|
|
159
|
-
const session = await tryConnect('localhost', firstApp.port);
|
|
160
|
-
// Connect the singleton pluginClient so status checks work
|
|
161
|
-
await connectPlugin(session.host, session.port);
|
|
162
|
-
// Fetch app identifier after singleton is connected
|
|
163
|
-
const identifier = await fetchAppIdentifier();
|
|
164
|
-
currentSession = { ...session, identifier };
|
|
165
|
-
return `Session started with app: ${session.name} (localhost:${session.port})`;
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
// Discovery found app but connection failed
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
// Strategy 4: Try default port on configured host as last resort
|
|
172
|
-
try {
|
|
173
|
-
resetPluginClient();
|
|
174
|
-
const session = await tryConnect(configuredHost, configuredPort);
|
|
175
|
-
// Connect the singleton pluginClient so status checks work
|
|
176
|
-
await connectPlugin(session.host, session.port);
|
|
177
|
-
// Fetch app identifier after singleton is connected
|
|
178
|
-
const identifier = await fetchAppIdentifier();
|
|
179
|
-
currentSession = { ...session, identifier };
|
|
180
|
-
return `Session started with app: ${session.name} (${session.host}:${session.port})`;
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
// All attempts failed
|
|
184
|
-
currentSession = null;
|
|
185
|
-
return `Session started (native IPC mode - no Tauri app found at localhost or ${configuredHost}:${configuredPort})`;
|
|
345
|
+
default: {
|
|
346
|
+
return handleStopAction(appIdentifier);
|
|
186
347
|
}
|
|
187
348
|
}
|
|
188
|
-
// Stop action - disconnect all apps and reset initialization state
|
|
189
|
-
if (appDiscovery) {
|
|
190
|
-
await appDiscovery.disconnectAll();
|
|
191
|
-
}
|
|
192
|
-
resetPluginClient();
|
|
193
|
-
resetInitialization();
|
|
194
|
-
currentSession = null;
|
|
195
|
-
return 'Session stopped';
|
|
196
349
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { connectPlugin } from './plugin-client.js';
|
|
3
|
+
import { hasActiveSession, getDefaultSession, resolveTargetApp } 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
|
-
* -
|
|
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
|
-
//
|
|
35
|
-
|
|
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 default session for initial connection
|
|
43
|
+
const session = getDefaultSession();
|
|
44
|
+
if (session) {
|
|
45
|
+
await connectPlugin(session.host, session.port);
|
|
46
|
+
}
|
|
36
47
|
isInitialized = true;
|
|
37
48
|
}
|
|
38
49
|
/**
|
|
@@ -46,10 +57,11 @@ export function resetInitialization() {
|
|
|
46
57
|
*
|
|
47
58
|
* @param script - JavaScript code to execute in the webview context
|
|
48
59
|
* @param windowId - Optional window label to target (defaults to "main")
|
|
60
|
+
* @param appIdentifier - Optional app identifier to target specific app
|
|
49
61
|
* @returns Result of the script execution with window context
|
|
50
62
|
*/
|
|
51
|
-
export async function executeInWebview(script, windowId) {
|
|
52
|
-
const { result } = await executeInWebviewWithContext(script, windowId);
|
|
63
|
+
export async function executeInWebview(script, windowId, appIdentifier) {
|
|
64
|
+
const { result } = await executeInWebviewWithContext(script, windowId, appIdentifier);
|
|
53
65
|
return result;
|
|
54
66
|
}
|
|
55
67
|
/**
|
|
@@ -57,13 +69,16 @@ export async function executeInWebview(script, windowId) {
|
|
|
57
69
|
*
|
|
58
70
|
* @param script - JavaScript code to execute in the webview context
|
|
59
71
|
* @param windowId - Optional window label to target (defaults to "main")
|
|
72
|
+
* @param appIdentifier - Optional app identifier to target specific app
|
|
60
73
|
* @returns Result of the script execution with window context
|
|
61
74
|
*/
|
|
62
|
-
export async function executeInWebviewWithContext(script, windowId) {
|
|
75
|
+
export async function executeInWebviewWithContext(script, windowId, appIdentifier) {
|
|
63
76
|
try {
|
|
64
77
|
// Ensure we're fully initialized
|
|
65
78
|
await ensureReady();
|
|
66
|
-
|
|
79
|
+
// Resolve target session
|
|
80
|
+
const session = resolveTargetApp(appIdentifier);
|
|
81
|
+
const client = session.client;
|
|
67
82
|
// Send script directly - Rust handles wrapping and IPC callbacks.
|
|
68
83
|
// Use 7s timeout (longer than Rust's 5s) so errors return before Node times out.
|
|
69
84
|
const response = await client.sendCommand({
|
|
@@ -169,9 +184,11 @@ export async function initializeConsoleCapture() {
|
|
|
169
184
|
*
|
|
170
185
|
* @param filter - Optional regex pattern to filter log messages
|
|
171
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
|
|
172
189
|
* @returns Formatted console logs as string
|
|
173
190
|
*/
|
|
174
|
-
export async function getConsoleLogs(filter, since) {
|
|
191
|
+
export async function getConsoleLogs(filter, since, windowId, appIdentifier) {
|
|
175
192
|
const filterStr = filter ? filter.replace(/'/g, '\\\'') : '';
|
|
176
193
|
const sinceStr = since || '';
|
|
177
194
|
const script = `
|
|
@@ -196,7 +213,7 @@ export async function getConsoleLogs(filter, since) {
|
|
|
196
213
|
'[ ' + new Date(l.timestamp).toISOString() + ' ] [ ' + l.level.toUpperCase() + ' ] ' + l.message
|
|
197
214
|
).join('\\n');
|
|
198
215
|
`;
|
|
199
|
-
return executeInWebview(script);
|
|
216
|
+
return executeInWebview(script, windowId, appIdentifier);
|
|
200
217
|
}
|
|
201
218
|
/**
|
|
202
219
|
* Clear all captured console logs.
|
|
@@ -268,11 +285,11 @@ async function prepareHtml2canvasScript(format, quality) {
|
|
|
268
285
|
/**
|
|
269
286
|
* Capture a screenshot of the entire webview.
|
|
270
287
|
*
|
|
271
|
-
* @param options - Screenshot options (format, quality, windowId)
|
|
288
|
+
* @param options - Screenshot options (format, quality, windowId, appIdentifier)
|
|
272
289
|
* @returns Screenshot result with image content
|
|
273
290
|
*/
|
|
274
291
|
export async function captureScreenshot(options = {}) {
|
|
275
|
-
const { format = 'png', quality = 90, windowId } = options;
|
|
292
|
+
const { format = 'png', quality = 90, windowId, appIdentifier } = options;
|
|
276
293
|
// Primary implementation: Use native platform-specific APIs
|
|
277
294
|
// - macOS: WKWebView takeSnapshot
|
|
278
295
|
// - Windows: WebView2 CapturePreview
|
|
@@ -280,7 +297,9 @@ export async function captureScreenshot(options = {}) {
|
|
|
280
297
|
try {
|
|
281
298
|
// Ensure we're fully initialized
|
|
282
299
|
await ensureReady();
|
|
283
|
-
|
|
300
|
+
// Resolve target session
|
|
301
|
+
const session = resolveTargetApp(appIdentifier);
|
|
302
|
+
const client = session.client;
|
|
284
303
|
// Use longer timeout (15s) for native screenshot - the Rust code waits up to 10s
|
|
285
304
|
const response = await client.sendCommand({
|
|
286
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
|
|
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);
|
package/dist/monitor/logs.js
CHANGED
|
@@ -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
|
package/dist/tools-registry.js
CHANGED
|
@@ -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
|
-
'
|
|
140
|
-
'The
|
|
141
|
-
'
|
|
142
|
-
'
|
|
143
|
-
'
|
|
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(
|
|
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
|
-
|
|
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,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypothesi/tauri-mcp-server",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A Model Context Protocol server for Tauri v2
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "A Model Context Protocol server for use with Tauri v2 applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mcp-server-tauri": "./dist/index.js"
|