@hypothesi/tauri-mcp-server 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/driver/app-discovery.d.ts +4 -2
- package/dist/driver/app-discovery.js +23 -3
- package/dist/driver/plugin-client.d.ts +23 -1
- package/dist/driver/plugin-client.js +127 -2
- package/dist/driver/plugin-commands.d.ts +8 -4
- package/dist/driver/plugin-commands.js +71 -16
- package/dist/driver/scripts/find-element.js +129 -16
- package/dist/driver/scripts/focus.js +31 -4
- package/dist/driver/scripts/html2canvas-loader.js +20 -2
- package/dist/driver/scripts/index.d.ts +2 -4
- package/dist/driver/scripts/index.js +40 -11
- package/dist/driver/scripts/interact.js +66 -5
- package/dist/driver/scripts/resolve-ref.js +101 -50
- package/dist/driver/session-manager.d.ts +1 -0
- package/dist/driver/session-manager.js +53 -6
- package/dist/driver/webview-executor.d.ts +20 -8
- package/dist/driver/webview-executor.js +159 -58
- package/dist/driver/webview-interactions.d.ts +56 -41
- package/dist/driver/webview-interactions.js +62 -40
- package/dist/monitor/logs.d.ts +14 -2
- package/dist/monitor/logs.js +37 -5
- package/dist/tools-registry.js +35 -33
- package/dist/version.d.ts +5 -0
- package/dist/version.js +16 -0
- package/package.json +1 -1
- package/dist/driver/scripts/get-styles.js +0 -47
- package/dist/driver/scripts/keyboard.js +0 -76
|
@@ -17,6 +17,8 @@ export interface SessionInfo {
|
|
|
17
17
|
port: number;
|
|
18
18
|
client?: PluginClient;
|
|
19
19
|
connected: boolean;
|
|
20
|
+
cwd: string | null;
|
|
21
|
+
pluginVersion: string | null;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Manages discovery and connection to multiple Tauri app instances
|
|
@@ -40,8 +42,8 @@ export declare class AppDiscovery {
|
|
|
40
42
|
*/
|
|
41
43
|
discoverApps(): Promise<AppInstance[]>;
|
|
42
44
|
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
* Connects to a specific app on a host and port
|
|
46
|
+
*/
|
|
45
47
|
connectToPort(port: number, appName?: string, host?: string): Promise<SessionInfo>;
|
|
46
48
|
/**
|
|
47
49
|
* Gets the first available app
|
|
@@ -45,8 +45,8 @@ export class AppDiscovery {
|
|
|
45
45
|
return apps;
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
* Connects to a specific app on a host and port
|
|
49
|
+
*/
|
|
50
50
|
async connectToPort(port, appName, host) {
|
|
51
51
|
const targetHost = host ?? this._host;
|
|
52
52
|
const sessionId = `${targetHost}_${port}`;
|
|
@@ -58,13 +58,33 @@ export class AppDiscovery {
|
|
|
58
58
|
const client = new PluginClient(targetHost, port);
|
|
59
59
|
try {
|
|
60
60
|
await client.connect();
|
|
61
|
+
// Get backend state to retrieve app info and plugin version
|
|
62
|
+
const backendState = await client.sendCommand({
|
|
63
|
+
command: 'get_backend_state',
|
|
64
|
+
args: {},
|
|
65
|
+
});
|
|
66
|
+
let name = appName || `Tauri App (${targetHost}:${port})`, cwd = null, pluginVersion = null;
|
|
67
|
+
if (backendState.success && backendState.data) {
|
|
68
|
+
const data = backendState.data;
|
|
69
|
+
if (data.app?.name) {
|
|
70
|
+
name = data.app.name;
|
|
71
|
+
}
|
|
72
|
+
if (data.app?.identifier) {
|
|
73
|
+
// Use identifier if name not available
|
|
74
|
+
name = data.app.name || data.app.identifier;
|
|
75
|
+
}
|
|
76
|
+
cwd = data.cwd ?? null;
|
|
77
|
+
pluginVersion = data.bridge?.pluginVersion ?? null;
|
|
78
|
+
}
|
|
61
79
|
const session = {
|
|
62
80
|
appId: sessionId,
|
|
63
|
-
name
|
|
81
|
+
name,
|
|
64
82
|
host: targetHost,
|
|
65
83
|
port,
|
|
66
84
|
client,
|
|
67
85
|
connected: true,
|
|
86
|
+
cwd,
|
|
87
|
+
pluginVersion,
|
|
68
88
|
};
|
|
69
89
|
this._activeSessions.set(sessionId, session);
|
|
70
90
|
return session;
|
|
@@ -15,6 +15,12 @@ export interface PluginResponse {
|
|
|
15
15
|
warning?: string;
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
+
export interface PluginClientOptions {
|
|
19
|
+
/** How often to ping an idle bridge connection. */
|
|
20
|
+
readonly heartbeatIntervalMs?: number;
|
|
21
|
+
/** How long to wait for a heartbeat pong before terminating the socket. */
|
|
22
|
+
readonly heartbeatTimeoutMs?: number;
|
|
23
|
+
}
|
|
18
24
|
/**
|
|
19
25
|
* Client to communicate with the MCP Bridge plugin's WebSocket server
|
|
20
26
|
*/
|
|
@@ -26,13 +32,19 @@ export declare class PluginClient extends EventEmitter {
|
|
|
26
32
|
private _reconnectAttempts;
|
|
27
33
|
private _shouldReconnect;
|
|
28
34
|
private _reconnectDelay;
|
|
35
|
+
private _reconnectTimer;
|
|
36
|
+
private _heartbeatTimer;
|
|
37
|
+
private _heartbeatTimeout;
|
|
38
|
+
private readonly _heartbeatIntervalMs;
|
|
39
|
+
private readonly _heartbeatTimeoutMs;
|
|
29
40
|
private _pendingRequests;
|
|
30
41
|
/**
|
|
31
42
|
* Constructor for PluginClient
|
|
32
43
|
* @param host Host address of the WebSocket server
|
|
33
44
|
* @param port Port number of the WebSocket server
|
|
45
|
+
* @param options Optional connection liveness settings
|
|
34
46
|
*/
|
|
35
|
-
constructor(host: string, port: number);
|
|
47
|
+
constructor(host: string, port: number, options?: PluginClientOptions);
|
|
36
48
|
/**
|
|
37
49
|
* Creates a PluginClient with default configuration from environment.
|
|
38
50
|
*/
|
|
@@ -53,6 +65,13 @@ export declare class PluginClient extends EventEmitter {
|
|
|
53
65
|
* Disconnect from the plugin
|
|
54
66
|
*/
|
|
55
67
|
disconnect(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Probe the current WebSocket with a ping/pong exchange.
|
|
70
|
+
*
|
|
71
|
+
* `readyState === OPEN` is insufficient for half-open TCP connections, so
|
|
72
|
+
* session reuse calls this before trusting a cached client.
|
|
73
|
+
*/
|
|
74
|
+
checkConnection(timeoutMs?: number): Promise<boolean>;
|
|
56
75
|
/**
|
|
57
76
|
* Send a command to the plugin and wait for response.
|
|
58
77
|
*
|
|
@@ -64,6 +83,9 @@ export declare class PluginClient extends EventEmitter {
|
|
|
64
83
|
* Check if connected
|
|
65
84
|
*/
|
|
66
85
|
isConnected(): boolean;
|
|
86
|
+
private _clearHeartbeatTimeout;
|
|
87
|
+
private _stopHeartbeat;
|
|
88
|
+
private _startHeartbeat;
|
|
67
89
|
}
|
|
68
90
|
/**
|
|
69
91
|
* Gets the existing singleton PluginClient without creating or modifying it.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
2
|
import { EventEmitter } from 'events';
|
|
3
3
|
import { buildWebSocketURL, getDefaultHost, getDefaultPort } from '../config.js';
|
|
4
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 30000, DEFAULT_HEARTBEAT_TIMEOUT_MS = 10000, DEFAULT_CONNECTION_CHECK_TIMEOUT_MS = 1000;
|
|
4
5
|
/**
|
|
5
6
|
* Client to communicate with the MCP Bridge plugin's WebSocket server
|
|
6
7
|
*/
|
|
@@ -13,17 +14,25 @@ export class PluginClient extends EventEmitter {
|
|
|
13
14
|
_reconnectAttempts = 0;
|
|
14
15
|
_shouldReconnect = true; // Keep trying forever until explicitly disconnected
|
|
15
16
|
_reconnectDelay = 1000; // Start with 1s, max 30s
|
|
17
|
+
_reconnectTimer = null;
|
|
18
|
+
_heartbeatTimer = null;
|
|
19
|
+
_heartbeatTimeout = null;
|
|
20
|
+
_heartbeatIntervalMs;
|
|
21
|
+
_heartbeatTimeoutMs;
|
|
16
22
|
_pendingRequests = new Map();
|
|
17
23
|
/**
|
|
18
24
|
* Constructor for PluginClient
|
|
19
25
|
* @param host Host address of the WebSocket server
|
|
20
26
|
* @param port Port number of the WebSocket server
|
|
27
|
+
* @param options Optional connection liveness settings
|
|
21
28
|
*/
|
|
22
|
-
constructor(host, port) {
|
|
29
|
+
constructor(host, port, options = {}) {
|
|
23
30
|
super();
|
|
24
31
|
this._host = host;
|
|
25
32
|
this._port = port;
|
|
26
33
|
this._url = buildWebSocketURL(host, port);
|
|
34
|
+
this._heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
35
|
+
this._heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
|
|
27
36
|
// CRITICAL: Attach a default error handler to prevent crashes.
|
|
28
37
|
// In Node.js, if an EventEmitter emits 'error' with no listeners, it throws
|
|
29
38
|
// an uncaught exception that crashes the process. This is especially important
|
|
@@ -63,9 +72,13 @@ export class PluginClient extends EventEmitter {
|
|
|
63
72
|
this._ws.on('open', () => {
|
|
64
73
|
// Connected to MCP Bridge plugin
|
|
65
74
|
this._reconnectAttempts = 0;
|
|
75
|
+
this._startHeartbeat();
|
|
66
76
|
this.emit('connected');
|
|
67
77
|
resolve();
|
|
68
78
|
});
|
|
79
|
+
this._ws.on('pong', () => {
|
|
80
|
+
this._clearHeartbeatTimeout();
|
|
81
|
+
});
|
|
69
82
|
this._ws.on('message', (data) => {
|
|
70
83
|
try {
|
|
71
84
|
const message = JSON.parse(data.toString());
|
|
@@ -95,6 +108,7 @@ export class PluginClient extends EventEmitter {
|
|
|
95
108
|
});
|
|
96
109
|
this._ws.on('close', () => {
|
|
97
110
|
// Disconnected from MCP Bridge plugin
|
|
111
|
+
this._stopHeartbeat();
|
|
98
112
|
this.emit('disconnected');
|
|
99
113
|
this._ws = null;
|
|
100
114
|
// Reject all pending requests since the connection is gone
|
|
@@ -107,11 +121,16 @@ export class PluginClient extends EventEmitter {
|
|
|
107
121
|
if (this._shouldReconnect) {
|
|
108
122
|
this._reconnectAttempts++;
|
|
109
123
|
const delay = Math.min(this._reconnectDelay * this._reconnectAttempts, 30000);
|
|
110
|
-
setTimeout(() => {
|
|
124
|
+
this._reconnectTimer = setTimeout(() => {
|
|
125
|
+
this._reconnectTimer = null;
|
|
126
|
+
if (!this._shouldReconnect) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
111
129
|
this.connect().catch(() => {
|
|
112
130
|
// Reconnection failed - will retry on next close event
|
|
113
131
|
});
|
|
114
132
|
}, delay);
|
|
133
|
+
this._reconnectTimer.unref();
|
|
115
134
|
}
|
|
116
135
|
});
|
|
117
136
|
});
|
|
@@ -121,11 +140,68 @@ export class PluginClient extends EventEmitter {
|
|
|
121
140
|
*/
|
|
122
141
|
disconnect() {
|
|
123
142
|
this._shouldReconnect = false; // Prevent auto-reconnect
|
|
143
|
+
this._stopHeartbeat();
|
|
144
|
+
if (this._reconnectTimer) {
|
|
145
|
+
clearTimeout(this._reconnectTimer);
|
|
146
|
+
this._reconnectTimer = null;
|
|
147
|
+
}
|
|
124
148
|
if (this._ws) {
|
|
125
149
|
this._ws.close();
|
|
126
150
|
this._ws = null;
|
|
127
151
|
}
|
|
128
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Probe the current WebSocket with a ping/pong exchange.
|
|
155
|
+
*
|
|
156
|
+
* `readyState === OPEN` is insufficient for half-open TCP connections, so
|
|
157
|
+
* session reuse calls this before trusting a cached client.
|
|
158
|
+
*/
|
|
159
|
+
async checkConnection(timeoutMs = DEFAULT_CONNECTION_CHECK_TIMEOUT_MS) {
|
|
160
|
+
const socket = this._ws;
|
|
161
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
const openSocket = socket;
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
const probeState = {
|
|
167
|
+
settled: false,
|
|
168
|
+
timeout: null,
|
|
169
|
+
};
|
|
170
|
+
function finish(isAlive) {
|
|
171
|
+
if (probeState.settled) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
probeState.settled = true;
|
|
175
|
+
if (probeState.timeout) {
|
|
176
|
+
clearTimeout(probeState.timeout);
|
|
177
|
+
}
|
|
178
|
+
openSocket.off('pong', handlePong);
|
|
179
|
+
openSocket.off('close', handleDisconnect);
|
|
180
|
+
openSocket.off('error', handleDisconnect);
|
|
181
|
+
resolve(isAlive);
|
|
182
|
+
}
|
|
183
|
+
function handlePong() {
|
|
184
|
+
finish(true);
|
|
185
|
+
}
|
|
186
|
+
function handleDisconnect() {
|
|
187
|
+
finish(false);
|
|
188
|
+
}
|
|
189
|
+
probeState.timeout = setTimeout(() => { finish(false); }, timeoutMs);
|
|
190
|
+
openSocket.once('pong', handlePong);
|
|
191
|
+
openSocket.once('close', handleDisconnect);
|
|
192
|
+
openSocket.once('error', handleDisconnect);
|
|
193
|
+
try {
|
|
194
|
+
openSocket.ping(undefined, undefined, (error) => {
|
|
195
|
+
if (error) {
|
|
196
|
+
finish(false);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
finish(false);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
129
205
|
/**
|
|
130
206
|
* Send a command to the plugin and wait for response.
|
|
131
207
|
*
|
|
@@ -186,6 +262,55 @@ export class PluginClient extends EventEmitter {
|
|
|
186
262
|
isConnected() {
|
|
187
263
|
return this._ws?.readyState === WebSocket.OPEN;
|
|
188
264
|
}
|
|
265
|
+
_clearHeartbeatTimeout() {
|
|
266
|
+
if (this._heartbeatTimeout) {
|
|
267
|
+
clearTimeout(this._heartbeatTimeout);
|
|
268
|
+
this._heartbeatTimeout = null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
_stopHeartbeat() {
|
|
272
|
+
if (this._heartbeatTimer) {
|
|
273
|
+
clearInterval(this._heartbeatTimer);
|
|
274
|
+
this._heartbeatTimer = null;
|
|
275
|
+
}
|
|
276
|
+
this._clearHeartbeatTimeout();
|
|
277
|
+
}
|
|
278
|
+
_startHeartbeat() {
|
|
279
|
+
this._stopHeartbeat();
|
|
280
|
+
this._heartbeatTimer = setInterval(() => {
|
|
281
|
+
const socket = this._ws;
|
|
282
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// A previous ping is still unanswered. Terminate instead of letting an
|
|
286
|
+
// OPEN-but-half-dead socket stay in the session cache indefinitely.
|
|
287
|
+
if (this._heartbeatTimeout) {
|
|
288
|
+
socket.terminate();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
this._heartbeatTimeout = setTimeout(() => {
|
|
292
|
+
this._heartbeatTimeout = null;
|
|
293
|
+
if (this._ws === socket && socket.readyState === WebSocket.OPEN) {
|
|
294
|
+
socket.terminate();
|
|
295
|
+
}
|
|
296
|
+
}, this._heartbeatTimeoutMs);
|
|
297
|
+
this._heartbeatTimeout.unref();
|
|
298
|
+
try {
|
|
299
|
+
socket.ping(undefined, undefined, (error) => {
|
|
300
|
+
if (error && this._ws === socket && socket.readyState === WebSocket.OPEN) {
|
|
301
|
+
socket.terminate();
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
this._clearHeartbeatTimeout();
|
|
307
|
+
if (this._ws === socket && socket.readyState === WebSocket.OPEN) {
|
|
308
|
+
socket.terminate();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}, this._heartbeatIntervalMs);
|
|
312
|
+
this._heartbeatTimer.unref();
|
|
313
|
+
}
|
|
189
314
|
}
|
|
190
315
|
// Singleton instance
|
|
191
316
|
let pluginClient = null;
|
|
@@ -2,19 +2,23 @@ import { z } from 'zod';
|
|
|
2
2
|
export declare const ExecuteIPCCommandSchema: z.ZodObject<{
|
|
3
3
|
command: z.ZodString;
|
|
4
4
|
args: z.ZodOptional<z.ZodUnknown>;
|
|
5
|
+
windowId: z.ZodOptional<z.ZodString>;
|
|
5
6
|
appIdentifier: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
|
|
6
7
|
}, "strip", z.ZodTypeAny, {
|
|
7
8
|
command: string;
|
|
9
|
+
windowId?: string | undefined;
|
|
8
10
|
appIdentifier?: string | number | undefined;
|
|
9
11
|
args?: unknown;
|
|
10
12
|
}, {
|
|
11
13
|
command: string;
|
|
14
|
+
windowId?: string | undefined;
|
|
12
15
|
appIdentifier?: string | number | undefined;
|
|
13
16
|
args?: unknown;
|
|
14
17
|
}>;
|
|
15
18
|
export declare function executeIPCCommand(options: {
|
|
16
19
|
command: string;
|
|
17
20
|
args?: unknown;
|
|
21
|
+
windowId?: string;
|
|
18
22
|
appIdentifier?: string | number;
|
|
19
23
|
}): Promise<string>;
|
|
20
24
|
export declare const ManageIPCMonitoringSchema: z.ZodObject<{
|
|
@@ -121,21 +125,21 @@ export declare function resizeWindow(options: {
|
|
|
121
125
|
appIdentifier?: string | number;
|
|
122
126
|
}): Promise<string>;
|
|
123
127
|
export declare const ManageWindowSchema: z.ZodObject<{
|
|
124
|
-
action: z.ZodEnum<["list", "info", "resize"]>;
|
|
128
|
+
action: z.ZodEnum<["list", "info", "resize", "focus", "minimize", "maximize"]>;
|
|
125
129
|
windowId: z.ZodOptional<z.ZodString>;
|
|
126
130
|
width: z.ZodOptional<z.ZodNumber>;
|
|
127
131
|
height: z.ZodOptional<z.ZodNumber>;
|
|
128
132
|
logical: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
129
133
|
appIdentifier: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
|
|
130
134
|
}, "strip", z.ZodTypeAny, {
|
|
131
|
-
action: "list" | "info" | "resize";
|
|
135
|
+
action: "list" | "focus" | "info" | "resize" | "minimize" | "maximize";
|
|
132
136
|
logical: boolean;
|
|
133
137
|
windowId?: string | undefined;
|
|
134
138
|
appIdentifier?: string | number | undefined;
|
|
135
139
|
width?: number | undefined;
|
|
136
140
|
height?: number | undefined;
|
|
137
141
|
}, {
|
|
138
|
-
action: "list" | "info" | "resize";
|
|
142
|
+
action: "list" | "focus" | "info" | "resize" | "minimize" | "maximize";
|
|
139
143
|
windowId?: string | undefined;
|
|
140
144
|
appIdentifier?: string | number | undefined;
|
|
141
145
|
width?: number | undefined;
|
|
@@ -154,7 +158,7 @@ export declare const ManageWindowSchema: z.ZodObject<{
|
|
|
154
158
|
* @returns JSON string with the result
|
|
155
159
|
*/
|
|
156
160
|
export declare function manageWindow(options: {
|
|
157
|
-
action: 'list' | 'info' | 'resize';
|
|
161
|
+
action: 'list' | 'info' | 'resize' | 'focus' | 'minimize' | 'maximize';
|
|
158
162
|
windowId?: string;
|
|
159
163
|
width?: number;
|
|
160
164
|
height?: number;
|
|
@@ -1,24 +1,54 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ensureSessionAndConnect, getExistingPluginClient } from './plugin-client.js';
|
|
3
|
+
import { executeInWebview } from './webview-executor.js';
|
|
4
|
+
const BRIDGE_COMMAND_PREFIX = 'plugin:mcp-bridge|';
|
|
3
5
|
export const ExecuteIPCCommandSchema = z.object({
|
|
4
6
|
command: z.string(),
|
|
5
7
|
args: z.unknown().optional(),
|
|
8
|
+
windowId: z.string().optional().describe('Window label to target (defaults to "main") for app commands'),
|
|
6
9
|
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.'),
|
|
7
10
|
});
|
|
11
|
+
async function invokeBridgeCommand(command, args, appIdentifier) {
|
|
12
|
+
const client = await ensureSessionAndConnect(appIdentifier);
|
|
13
|
+
// Send IPC command via WebSocket to the mcp-bridge plugin
|
|
14
|
+
const response = await client.sendCommand({
|
|
15
|
+
command: 'invoke_tauri',
|
|
16
|
+
args: { command, args },
|
|
17
|
+
});
|
|
18
|
+
if (!response.success) {
|
|
19
|
+
return JSON.stringify({ success: false, error: response.error || 'Unknown error' });
|
|
20
|
+
}
|
|
21
|
+
return JSON.stringify({ success: true, result: response.data });
|
|
22
|
+
}
|
|
23
|
+
async function invokeAppCommand(command, args, windowId, appIdentifier) {
|
|
24
|
+
const script = `
|
|
25
|
+
return (async () => {
|
|
26
|
+
const internals = window.__TAURI_INTERNALS__;
|
|
27
|
+
|
|
28
|
+
if (!internals || typeof internals.invoke !== 'function') {
|
|
29
|
+
throw new Error('Tauri IPC is not available in this webview');
|
|
30
|
+
}
|
|
31
|
+
return await internals.invoke(${JSON.stringify(command)}, ${JSON.stringify(args ?? {})});
|
|
32
|
+
})();
|
|
33
|
+
`;
|
|
34
|
+
const result = await executeInWebview(script, windowId, appIdentifier);
|
|
35
|
+
const safeParse = (str) => {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(str);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return str;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
return JSON.stringify({ success: true, result: safeParse(result) });
|
|
44
|
+
}
|
|
8
45
|
export async function executeIPCCommand(options) {
|
|
46
|
+
const { command, args = {}, windowId, appIdentifier } = options;
|
|
9
47
|
try {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const client = await ensureSessionAndConnect(appIdentifier);
|
|
13
|
-
// Send IPC command via WebSocket to the mcp-bridge plugin
|
|
14
|
-
const response = await client.sendCommand({
|
|
15
|
-
command: 'invoke_tauri',
|
|
16
|
-
args: { command, args },
|
|
17
|
-
});
|
|
18
|
-
if (!response.success) {
|
|
19
|
-
return JSON.stringify({ success: false, error: response.error || 'Unknown error' });
|
|
48
|
+
if (command.startsWith(BRIDGE_COMMAND_PREFIX)) {
|
|
49
|
+
return await invokeBridgeCommand(command, args, appIdentifier);
|
|
20
50
|
}
|
|
21
|
-
return
|
|
51
|
+
return await invokeAppCommand(command, args, windowId, appIdentifier);
|
|
22
52
|
}
|
|
23
53
|
catch (error) {
|
|
24
54
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -271,17 +301,18 @@ export async function resizeWindow(options) {
|
|
|
271
301
|
}
|
|
272
302
|
}
|
|
273
303
|
export const ManageWindowSchema = z.object({
|
|
274
|
-
action: z.enum(['list', 'info', 'resize'])
|
|
275
|
-
|
|
276
|
-
windowId: z.string().optional()
|
|
277
|
-
|
|
304
|
+
action: z.enum(['list', 'info', 'resize', 'focus', 'minimize', 'maximize']).describe('Action: "list" all windows, get "info" for one window, "resize" a window, ' +
|
|
305
|
+
'"focus" a window, "minimize" a window, or "maximize" a window'),
|
|
306
|
+
windowId: z.string().optional().describe('Window label to target (defaults to "main"). Required for "info", ' +
|
|
307
|
+
'optional for "resize", "focus", "minimize", "maximize"'),
|
|
278
308
|
width: z.number().int().positive().optional()
|
|
279
309
|
.describe('Width in pixels (required for "resize" action)'),
|
|
280
310
|
height: z.number().int().positive().optional()
|
|
281
311
|
.describe('Height in pixels (required for "resize" action)'),
|
|
282
312
|
logical: z.boolean().optional().default(true)
|
|
283
313
|
.describe('Use logical pixels (true, default) or physical pixels (false). Only for "resize"'),
|
|
284
|
-
appIdentifier: z.union([z.string(), z.number()]).optional().describe('App port or bundle ID to target. Defaults to the only connected app
|
|
314
|
+
appIdentifier: z.union([z.string(), z.number()]).optional().describe('App port or bundle ID to target. Defaults to the only connected app ' +
|
|
315
|
+
'or the default app if multiple are connected.'),
|
|
285
316
|
});
|
|
286
317
|
/**
|
|
287
318
|
* Unified window management function.
|
|
@@ -323,6 +354,30 @@ export async function manageWindow(options) {
|
|
|
323
354
|
}
|
|
324
355
|
return resizeWindow({ width, height, windowId, logical, appIdentifier });
|
|
325
356
|
}
|
|
357
|
+
case 'focus':
|
|
358
|
+
case 'minimize':
|
|
359
|
+
case 'maximize': {
|
|
360
|
+
try {
|
|
361
|
+
const client = await ensureSessionAndConnect(appIdentifier);
|
|
362
|
+
const response = await client.sendCommand({
|
|
363
|
+
command: 'set_window_state',
|
|
364
|
+
args: { action, windowId: windowId ?? 'main' },
|
|
365
|
+
});
|
|
366
|
+
if (!response.success) {
|
|
367
|
+
// Translate unknown command error to version mismatch message
|
|
368
|
+
if (response.error && response.error.includes('Unknown command')) {
|
|
369
|
+
throw new Error(`manage_window action "${action}" needs tauri-plugin-mcp-bridge 0.13.0 or newer. ` +
|
|
370
|
+
'The connected app is running an older plugin. Update the plugin in Cargo.toml and rebuild the app.');
|
|
371
|
+
}
|
|
372
|
+
throw new Error(response.error || 'Unknown error');
|
|
373
|
+
}
|
|
374
|
+
return JSON.stringify(response.data);
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
378
|
+
throw new Error(`Failed to ${action} window: ${message}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
326
381
|
default: {
|
|
327
382
|
throw new Error(`Unknown action: ${action}`);
|
|
328
383
|
}
|
|
@@ -1,26 +1,139 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Find
|
|
2
|
+
* Find elements using various selector strategies and return structured data
|
|
3
3
|
*
|
|
4
4
|
* @param {Object} params
|
|
5
5
|
* @param {string} params.selector - Element selector, ref ID (e.g., "ref=e3"), or text
|
|
6
6
|
* @param {string} params.strategy - Selector strategy: 'css', 'xpath', or 'text'
|
|
7
|
+
* @param {number|null} params.nth - Zero-based index to pick one of several matching elements
|
|
8
|
+
* @param {number} params.limit - Maximum number of matches to return
|
|
9
|
+
* @param {string[]} params.properties - Computed CSS properties to include
|
|
10
|
+
* @param {boolean} params.visibleOnly - Return only rendered elements
|
|
11
|
+
* @param {boolean} params.includeHtml - Include outerHTML for each match
|
|
7
12
|
*/
|
|
8
13
|
(function(params) {
|
|
9
|
-
const { selector, strategy } = params;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
const { selector, strategy, nth, limit, properties, visibleOnly, includeHtml } = params;
|
|
15
|
+
|
|
16
|
+
function getAccessibleName(element) {
|
|
17
|
+
try {
|
|
18
|
+
if (window.ariaApi && typeof window.ariaApi.getName === 'function') {
|
|
19
|
+
return window.ariaApi.getName(element) || null;
|
|
20
|
+
}
|
|
21
|
+
} catch (e) {
|
|
22
|
+
// Ignore aria-api errors
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getAttributes(element) {
|
|
28
|
+
const attrs = {
|
|
29
|
+
id: element.id || null,
|
|
30
|
+
class: element.className || null,
|
|
31
|
+
role: element.getAttribute('role') || null,
|
|
32
|
+
'aria-label': element.getAttribute('aria-label') || null,
|
|
33
|
+
'data-testid': element.getAttribute('data-testid') || null,
|
|
34
|
+
href: element.getAttribute('href') || null,
|
|
35
|
+
type: element.getAttribute('type') || null,
|
|
36
|
+
disabled: element.hasAttribute('disabled') ? '' : null,
|
|
37
|
+
};
|
|
38
|
+
// Remove null values
|
|
39
|
+
const result = {};
|
|
40
|
+
for (const key in attrs) {
|
|
41
|
+
if (attrs[key] !== null) {
|
|
42
|
+
result[key] = attrs[key];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getStyles(element) {
|
|
49
|
+
if (!properties || properties.length === 0) return null;
|
|
50
|
+
const computed = window.getComputedStyle(element);
|
|
51
|
+
const result = {};
|
|
52
|
+
for (let i = 0; i < properties.length; i++) {
|
|
53
|
+
const prop = properties[i];
|
|
54
|
+
result[prop] = computed.getPropertyValue(prop);
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isInteractive(element) {
|
|
60
|
+
return element.matches && element.matches(window.__MCP__.INTERACTIVE_SELECTOR);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Get all candidates first
|
|
64
|
+
var allMatches = window.__MCP__.resolveAll(selector, strategy);
|
|
65
|
+
|
|
66
|
+
// Filter by visibility if requested
|
|
67
|
+
var matches = visibleOnly
|
|
68
|
+
? allMatches.filter(function(el) { return window.__MCP__.isRendered(el); })
|
|
69
|
+
: allMatches;
|
|
70
|
+
|
|
71
|
+
const totalMatches = matches.length;
|
|
72
|
+
|
|
73
|
+
// Apply nth if specified
|
|
74
|
+
if (typeof nth === 'number') {
|
|
75
|
+
if (!matches[nth]) {
|
|
76
|
+
return JSON.stringify({
|
|
77
|
+
selector: selector,
|
|
78
|
+
strategy: strategy,
|
|
79
|
+
totalMatches: totalMatches,
|
|
80
|
+
returned: 0,
|
|
81
|
+
truncated: false,
|
|
82
|
+
matches: [],
|
|
83
|
+
error: 'nth=' + nth + ' is out of range, ' + totalMatches + ' matches for ' + selector
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
matches = [matches[nth]];
|
|
23
87
|
}
|
|
24
88
|
|
|
25
|
-
|
|
89
|
+
// Apply limit
|
|
90
|
+
const truncated = matches.length > limit;
|
|
91
|
+
if (truncated) {
|
|
92
|
+
matches = matches.slice(0, limit);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const returned = matches.length;
|
|
96
|
+
|
|
97
|
+
const results = matches.map(function(element, index) {
|
|
98
|
+
const rect = element.getBoundingClientRect();
|
|
99
|
+
const ref = window.__MCP__.reverseRefs && window.__MCP__.reverseRefs.has(element)
|
|
100
|
+
? Array.from(window.__MCP__.reverseRefs.entries()).find(([k, v]) => v === element)[0]
|
|
101
|
+
: null;
|
|
102
|
+
|
|
103
|
+
const match = {
|
|
104
|
+
nth: typeof nth === 'number' ? nth : (index + (typeof nth !== 'number' ? 0 : 0)),
|
|
105
|
+
ref: ref,
|
|
106
|
+
tag: element.tagName.toLowerCase(),
|
|
107
|
+
text: (element.textContent || '').trim(),
|
|
108
|
+
accessibleName: getAccessibleName(element),
|
|
109
|
+
attributes: getAttributes(element),
|
|
110
|
+
rect: {
|
|
111
|
+
x: Math.round(rect.left),
|
|
112
|
+
y: Math.round(rect.top),
|
|
113
|
+
width: Math.round(rect.width),
|
|
114
|
+
height: Math.round(rect.height),
|
|
115
|
+
},
|
|
116
|
+
visible: window.__MCP__.isRendered(element),
|
|
117
|
+
interactive: isInteractive(element),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const styles = getStyles(element);
|
|
121
|
+
if (styles) match.styles = styles;
|
|
122
|
+
|
|
123
|
+
if (includeHtml) {
|
|
124
|
+
const html = element.outerHTML;
|
|
125
|
+
match.html = html.length > 2000 ? html.substring(0, 2000) + '…' : html;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return match;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return JSON.stringify({
|
|
132
|
+
selector: selector,
|
|
133
|
+
strategy: strategy,
|
|
134
|
+
totalMatches: totalMatches,
|
|
135
|
+
returned: returned,
|
|
136
|
+
truncated: truncated,
|
|
137
|
+
matches: results,
|
|
138
|
+
});
|
|
26
139
|
})
|