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