@hypothesi/tauri-mcp-server 0.1.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/LICENSE +39 -0
- package/dist/config.js +45 -0
- package/dist/driver/app-discovery.js +148 -0
- package/dist/driver/plugin-client.js +208 -0
- package/dist/driver/plugin-commands.js +142 -0
- package/dist/driver/protocol.js +7 -0
- package/dist/driver/scripts/find-element.js +48 -0
- package/dist/driver/scripts/focus.js +17 -0
- package/dist/driver/scripts/get-styles.js +41 -0
- package/dist/driver/scripts/html2canvas-loader.js +86 -0
- package/dist/driver/scripts/index.js +94 -0
- package/dist/driver/scripts/interact.js +103 -0
- package/dist/driver/scripts/keyboard.js +76 -0
- package/dist/driver/scripts/swipe.js +88 -0
- package/dist/driver/scripts/wait-for.js +44 -0
- package/dist/driver/session-manager.js +121 -0
- package/dist/driver/webview-executor.js +334 -0
- package/dist/driver/webview-interactions.js +213 -0
- package/dist/index.js +80 -0
- package/dist/manager/cli.js +128 -0
- package/dist/manager/config.js +142 -0
- package/dist/manager/docs.js +213 -0
- package/dist/manager/mobile.js +83 -0
- package/dist/monitor/logs.js +98 -0
- package/dist/tools-registry.js +292 -0
- package/package.json +60 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getPluginClient, connectPlugin } from './plugin-client.js';
|
|
3
|
+
import { buildScreenshotScript } from './scripts/html2canvas-loader.js';
|
|
4
|
+
/**
|
|
5
|
+
* WebView Executor - Native IPC-based JavaScript execution
|
|
6
|
+
*
|
|
7
|
+
* This module provides native Tauri IPC-based execution,
|
|
8
|
+
* enabling cross-platform support (Linux, Windows, macOS) without external dependencies.
|
|
9
|
+
*
|
|
10
|
+
* Communication flow:
|
|
11
|
+
* MCP Server (Node.js) → plugin-client (WebSocket) → mcp-bridge plugin → Tauri Webview
|
|
12
|
+
*/
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Auto-Initialization System
|
|
15
|
+
// ============================================================================
|
|
16
|
+
let isInitialized = false;
|
|
17
|
+
/**
|
|
18
|
+
* Ensures the MCP server is fully initialized and ready to use.
|
|
19
|
+
* This is called automatically by all tool functions.
|
|
20
|
+
*
|
|
21
|
+
* Initialization includes:
|
|
22
|
+
* - Connecting to the plugin WebSocket
|
|
23
|
+
* - Console capture is already initialized by bridge.js in the Tauri app
|
|
24
|
+
*
|
|
25
|
+
* This function is idempotent - calling it multiple times is safe.
|
|
26
|
+
*/
|
|
27
|
+
export async function ensureReady() {
|
|
28
|
+
if (isInitialized) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// Connect to the plugin
|
|
32
|
+
await connectPlugin();
|
|
33
|
+
isInitialized = true;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Reset initialization state (useful for testing or reconnecting).
|
|
37
|
+
*/
|
|
38
|
+
export function resetInitialization() {
|
|
39
|
+
isInitialized = false;
|
|
40
|
+
}
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Core Execution Functions
|
|
43
|
+
// ============================================================================
|
|
44
|
+
/**
|
|
45
|
+
* Execute JavaScript in the Tauri webview using native IPC via WebSocket.
|
|
46
|
+
*
|
|
47
|
+
* @param script - JavaScript code to execute in the webview context
|
|
48
|
+
* @returns Result of the script execution as a string
|
|
49
|
+
*/
|
|
50
|
+
export async function executeInWebview(script) {
|
|
51
|
+
try {
|
|
52
|
+
// Ensure we're fully initialized
|
|
53
|
+
await ensureReady();
|
|
54
|
+
const client = getPluginClient();
|
|
55
|
+
// Send script directly - Rust handles wrapping and IPC callbacks.
|
|
56
|
+
// Use 7s timeout (longer than Rust's 5s) so errors return before Node times out.
|
|
57
|
+
const response = await client.sendCommand({
|
|
58
|
+
command: 'execute_js',
|
|
59
|
+
args: { script },
|
|
60
|
+
}, 7000);
|
|
61
|
+
// console.log('executeInWebview response:', JSON.stringify(response));
|
|
62
|
+
if (!response.success) {
|
|
63
|
+
throw new Error(response.error || 'Unknown execution error');
|
|
64
|
+
}
|
|
65
|
+
// Parse and return the result
|
|
66
|
+
const result = response.data;
|
|
67
|
+
// console.log('executeInWebview result data:', result, 'type:', typeof result);
|
|
68
|
+
if (result === null || result === undefined) {
|
|
69
|
+
return 'null';
|
|
70
|
+
}
|
|
71
|
+
if (typeof result === 'string') {
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
return JSON.stringify(result);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
78
|
+
throw new Error(`WebView execution failed: ${message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Execute async JavaScript in the webview with timeout support.
|
|
83
|
+
*
|
|
84
|
+
* @param script - JavaScript code to execute (can use await)
|
|
85
|
+
* @param timeout - Timeout in milliseconds (default: 5000)
|
|
86
|
+
* @returns Result of the script execution
|
|
87
|
+
*/
|
|
88
|
+
export async function executeAsyncInWebview(script, timeout = 5000) {
|
|
89
|
+
const wrappedScript = `
|
|
90
|
+
return (async () => {
|
|
91
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
92
|
+
setTimeout(() => reject(new Error('Script execution timeout')), ${timeout});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const scriptPromise = (async () => {
|
|
96
|
+
${script}
|
|
97
|
+
})();
|
|
98
|
+
|
|
99
|
+
return await Promise.race([scriptPromise, timeoutPromise]);
|
|
100
|
+
})();
|
|
101
|
+
`;
|
|
102
|
+
return executeInWebview(wrappedScript);
|
|
103
|
+
}
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// Console Log Capture System
|
|
106
|
+
// ============================================================================
|
|
107
|
+
/**
|
|
108
|
+
* Initialize console log capture in the webview.
|
|
109
|
+
* This intercepts console methods and stores logs in memory.
|
|
110
|
+
*
|
|
111
|
+
* NOTE: Console capture is now automatically initialized by bridge.js when the
|
|
112
|
+
* Tauri app starts. This function is kept for backwards compatibility and will
|
|
113
|
+
* simply return early if capture is already initialized.
|
|
114
|
+
*/
|
|
115
|
+
export async function initializeConsoleCapture() {
|
|
116
|
+
const script = `
|
|
117
|
+
if (!window.__MCP_CONSOLE_LOGS__) {
|
|
118
|
+
window.__MCP_CONSOLE_LOGS__ = [];
|
|
119
|
+
const originalConsole = { ...console };
|
|
120
|
+
|
|
121
|
+
['log', 'debug', 'info', 'warn', 'error'].forEach(level => {
|
|
122
|
+
console[level] = function(...args) {
|
|
123
|
+
window.__MCP_CONSOLE_LOGS__.push({
|
|
124
|
+
level: level,
|
|
125
|
+
message: args.map(a => {
|
|
126
|
+
try {
|
|
127
|
+
return typeof a === 'object' ? JSON.stringify(a) : String(a);
|
|
128
|
+
} catch(e) {
|
|
129
|
+
return String(a);
|
|
130
|
+
}
|
|
131
|
+
}).join(' '),
|
|
132
|
+
timestamp: Date.now()
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Keep original console behavior
|
|
136
|
+
originalConsole[level].apply(console, args);
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return 'Console capture initialized';
|
|
141
|
+
}
|
|
142
|
+
return 'Console capture already initialized';
|
|
143
|
+
`;
|
|
144
|
+
return executeInWebview(script);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Retrieve captured console logs with optional filtering.
|
|
148
|
+
*
|
|
149
|
+
* @param filter - Optional regex pattern to filter log messages
|
|
150
|
+
* @param since - Optional ISO timestamp to filter logs after this time
|
|
151
|
+
* @returns Formatted console logs as string
|
|
152
|
+
*/
|
|
153
|
+
export async function getConsoleLogs(filter, since) {
|
|
154
|
+
const filterStr = filter ? filter.replace(/'/g, '\\\'') : '';
|
|
155
|
+
const sinceStr = since || '';
|
|
156
|
+
const script = `
|
|
157
|
+
const logs = window.__MCP_CONSOLE_LOGS__ || [];
|
|
158
|
+
let filtered = logs;
|
|
159
|
+
|
|
160
|
+
if ('${sinceStr}') {
|
|
161
|
+
const sinceTime = new Date('${sinceStr}').getTime();
|
|
162
|
+
filtered = filtered.filter(l => l.timestamp > sinceTime);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if ('${filterStr}') {
|
|
166
|
+
try {
|
|
167
|
+
const regex = new RegExp('${filterStr}', 'i');
|
|
168
|
+
filtered = filtered.filter(l => regex.test(l.message));
|
|
169
|
+
} catch(e) {
|
|
170
|
+
throw new Error('Invalid filter regex: ' + e.message);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return filtered.map(l =>
|
|
175
|
+
'[ ' + new Date(l.timestamp).toISOString() + ' ] [ ' + l.level.toUpperCase() + ' ] ' + l.message
|
|
176
|
+
).join('\\n');
|
|
177
|
+
`;
|
|
178
|
+
return executeInWebview(script);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Clear all captured console logs.
|
|
182
|
+
*/
|
|
183
|
+
export async function clearConsoleLogs() {
|
|
184
|
+
const script = `
|
|
185
|
+
window.__MCP_CONSOLE_LOGS__ = [];
|
|
186
|
+
return 'Console logs cleared';
|
|
187
|
+
`;
|
|
188
|
+
return executeInWebview(script);
|
|
189
|
+
}
|
|
190
|
+
// ============================================================================
|
|
191
|
+
// Screenshot Functionality
|
|
192
|
+
// ============================================================================
|
|
193
|
+
/**
|
|
194
|
+
* Capture a screenshot of the entire webview.
|
|
195
|
+
*
|
|
196
|
+
* @param format - Image format: 'png' or 'jpeg'
|
|
197
|
+
* @param quality - JPEG quality (0-100), only used for jpeg format
|
|
198
|
+
* @returns Base64-encoded image data URL
|
|
199
|
+
*/
|
|
200
|
+
export async function captureScreenshot(format = 'png', quality = 90) {
|
|
201
|
+
// Primary implementation: Use native platform-specific APIs
|
|
202
|
+
// - macOS: WKWebView takeSnapshot
|
|
203
|
+
// - Windows: WebView2 CapturePreview
|
|
204
|
+
// - Linux: Chromium/WebKit screenshot APIs
|
|
205
|
+
try {
|
|
206
|
+
// Ensure we're fully initialized
|
|
207
|
+
await ensureReady();
|
|
208
|
+
const client = getPluginClient();
|
|
209
|
+
// Use longer timeout (15s) for native screenshot - the Rust code waits up to 10s
|
|
210
|
+
const response = await client.sendCommand({
|
|
211
|
+
command: 'capture_native_screenshot',
|
|
212
|
+
args: {
|
|
213
|
+
format,
|
|
214
|
+
quality,
|
|
215
|
+
},
|
|
216
|
+
}, 15000);
|
|
217
|
+
if (response.success && response.data) {
|
|
218
|
+
// The native command returns a base64 data URL
|
|
219
|
+
const dataUrl = response.data;
|
|
220
|
+
// Validate that we got a real data URL
|
|
221
|
+
if (dataUrl && dataUrl.startsWith('data:image/')) {
|
|
222
|
+
return `Webview screenshot captured (native):\n\n`;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// If we get here, native returned but with invalid data - throw to trigger fallback
|
|
226
|
+
throw new Error(response.error || 'Native screenshot returned invalid data');
|
|
227
|
+
}
|
|
228
|
+
catch (nativeError) {
|
|
229
|
+
// Log the native error for debugging, then fall back
|
|
230
|
+
const nativeMsg = nativeError instanceof Error ? nativeError.message : String(nativeError);
|
|
231
|
+
console.error(`Native screenshot failed: ${nativeMsg}, falling back to html2canvas`);
|
|
232
|
+
}
|
|
233
|
+
// Fallback 1: Use html2canvas library for high-quality DOM rendering
|
|
234
|
+
// The library is bundled from node_modules, not loaded from CDN
|
|
235
|
+
const html2canvasScript = buildScreenshotScript(format, quality);
|
|
236
|
+
// Fallback: Try Screen Capture API if available
|
|
237
|
+
// Note: This script is wrapped by executeAsyncInWebview, so we don't need an IIFE
|
|
238
|
+
const screenCaptureScript = `
|
|
239
|
+
// Check if Screen Capture API is available
|
|
240
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) {
|
|
241
|
+
throw new Error('Screen Capture API not available');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Request screen capture permission and get the stream
|
|
245
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
246
|
+
video: {
|
|
247
|
+
displaySurface: 'window',
|
|
248
|
+
cursor: 'never'
|
|
249
|
+
},
|
|
250
|
+
audio: false
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Get the video track
|
|
254
|
+
const videoTrack = stream.getVideoTracks()[0];
|
|
255
|
+
if (!videoTrack) {
|
|
256
|
+
throw new Error('No video track available');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Create a video element to display the stream
|
|
260
|
+
const video = document.createElement('video');
|
|
261
|
+
video.srcObject = stream;
|
|
262
|
+
video.autoplay = true;
|
|
263
|
+
|
|
264
|
+
// Wait for the video to load metadata
|
|
265
|
+
await new Promise((resolve, reject) => {
|
|
266
|
+
video.onloadedmetadata = resolve;
|
|
267
|
+
video.onerror = reject;
|
|
268
|
+
setTimeout(() => reject(new Error('Video load timeout')), 5000);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Play the video
|
|
272
|
+
await video.play();
|
|
273
|
+
|
|
274
|
+
// Create canvas to capture the frame
|
|
275
|
+
const canvas = document.createElement('canvas');
|
|
276
|
+
const ctx = canvas.getContext('2d');
|
|
277
|
+
|
|
278
|
+
// Set canvas dimensions to match video
|
|
279
|
+
canvas.width = video.videoWidth;
|
|
280
|
+
canvas.height = video.videoHeight;
|
|
281
|
+
|
|
282
|
+
// Draw the video frame to canvas
|
|
283
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
284
|
+
|
|
285
|
+
// Stop all tracks to release the capture
|
|
286
|
+
stream.getTracks().forEach(track => track.stop());
|
|
287
|
+
|
|
288
|
+
// Convert to data URL with specified format and quality
|
|
289
|
+
const mimeType = '${format}' === 'jpeg' ? 'image/jpeg' : 'image/png';
|
|
290
|
+
return canvas.toDataURL(mimeType, ${quality / 100});
|
|
291
|
+
`;
|
|
292
|
+
try {
|
|
293
|
+
// Try html2canvas second (after native APIs)
|
|
294
|
+
const result = await executeAsyncInWebview(html2canvasScript, 10000); // Longer timeout for library loading
|
|
295
|
+
// Validate that we got a real data URL, not 'null' or empty
|
|
296
|
+
if (result && result !== 'null' && result.startsWith('data:image/')) {
|
|
297
|
+
return `Webview screenshot captured:\n\n`;
|
|
298
|
+
}
|
|
299
|
+
throw new Error(`html2canvas returned invalid result: ${result?.substring(0, 100) || 'null'}`);
|
|
300
|
+
}
|
|
301
|
+
catch (html2canvasError) {
|
|
302
|
+
try {
|
|
303
|
+
// Fallback to Screen Capture API
|
|
304
|
+
const result = await executeAsyncInWebview(screenCaptureScript);
|
|
305
|
+
// Validate that we got a real data URL
|
|
306
|
+
if (result && result.startsWith('data:image/')) {
|
|
307
|
+
return `Webview screenshot captured (via Screen Capture API):\n\n`;
|
|
308
|
+
}
|
|
309
|
+
throw new Error(`Screen Capture API returned invalid result: ${result?.substring(0, 50) || 'null'}`);
|
|
310
|
+
}
|
|
311
|
+
catch (screenCaptureError) {
|
|
312
|
+
// All methods failed - throw a proper error
|
|
313
|
+
const html2canvasMsg = html2canvasError instanceof Error ? html2canvasError.message : 'html2canvas failed';
|
|
314
|
+
const screenCaptureMsg = screenCaptureError instanceof Error ? screenCaptureError.message : 'Screen Capture API failed';
|
|
315
|
+
throw new Error('Screenshot capture failed. Native API not available, ' +
|
|
316
|
+
`html2canvas error: ${html2canvasMsg}, ` +
|
|
317
|
+
`Screen Capture API error: ${screenCaptureMsg}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// ============================================================================
|
|
322
|
+
// Schemas for Validation
|
|
323
|
+
// ============================================================================
|
|
324
|
+
export const ExecuteScriptSchema = z.object({
|
|
325
|
+
script: z.string().describe('JavaScript code to execute in the webview'),
|
|
326
|
+
});
|
|
327
|
+
export const GetConsoleLogsSchema = z.object({
|
|
328
|
+
filter: z.string().optional().describe('Regex or keyword to filter logs'),
|
|
329
|
+
since: z.string().optional().describe('ISO timestamp to filter logs since'),
|
|
330
|
+
});
|
|
331
|
+
export const CaptureScreenshotSchema = z.object({
|
|
332
|
+
format: z.enum(['png', 'jpeg']).optional().default('png').describe('Image format'),
|
|
333
|
+
quality: z.number().min(0).max(100).optional().describe('JPEG quality (0-100)'),
|
|
334
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { executeInWebview, captureScreenshot, getConsoleLogs as getConsoleLogsFromCapture } from './webview-executor.js';
|
|
3
|
+
import { SCRIPTS, buildScript, buildTypeScript, buildKeyEventScript } from './scripts/index.js';
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Schemas
|
|
6
|
+
// ============================================================================
|
|
7
|
+
export const InteractSchema = z.object({
|
|
8
|
+
action: z.enum(['click', 'double-click', 'long-press', 'scroll', 'swipe'])
|
|
9
|
+
.describe('Type of interaction to perform'),
|
|
10
|
+
selector: z.string().optional().describe('CSS selector for the element to interact with'),
|
|
11
|
+
x: z.number().optional().describe('X coordinate for direct coordinate interaction'),
|
|
12
|
+
y: z.number().optional().describe('Y coordinate for direct coordinate interaction'),
|
|
13
|
+
duration: z.number().optional()
|
|
14
|
+
.describe('Duration in ms for long-press or swipe (default: 500ms for long-press, 300ms for swipe)'),
|
|
15
|
+
scrollX: z.number().optional().describe('Horizontal scroll amount in pixels (positive = right)'),
|
|
16
|
+
scrollY: z.number().optional().describe('Vertical scroll amount in pixels (positive = down)'),
|
|
17
|
+
// Swipe-specific parameters
|
|
18
|
+
fromX: z.number().optional().describe('Starting X coordinate for swipe'),
|
|
19
|
+
fromY: z.number().optional().describe('Starting Y coordinate for swipe'),
|
|
20
|
+
toX: z.number().optional().describe('Ending X coordinate for swipe'),
|
|
21
|
+
toY: z.number().optional().describe('Ending Y coordinate for swipe'),
|
|
22
|
+
});
|
|
23
|
+
export const ScreenshotSchema = z.object({
|
|
24
|
+
format: z.enum(['png', 'jpeg']).optional().default('png').describe('Image format'),
|
|
25
|
+
quality: z.number().min(0).max(100).optional().describe('JPEG quality (0-100, only for jpeg format)'),
|
|
26
|
+
});
|
|
27
|
+
export const KeyboardSchema = z.object({
|
|
28
|
+
action: z.enum(['type', 'press', 'down', 'up'])
|
|
29
|
+
.describe('Keyboard action type: "type" for typing text into an element, "press/down/up" for key events'),
|
|
30
|
+
selector: z.string().optional().describe('CSS selector for element to type into (required for "type" action)'),
|
|
31
|
+
text: z.string().optional().describe('Text to type (required for "type" action)'),
|
|
32
|
+
key: z.string().optional().describe('Key to press (required for "press/down/up" actions, e.g., "Enter", "a", "Escape")'),
|
|
33
|
+
modifiers: z.array(z.enum(['Control', 'Alt', 'Shift', 'Meta'])).optional().describe('Modifier keys to hold'),
|
|
34
|
+
});
|
|
35
|
+
export const WaitForSchema = z.object({
|
|
36
|
+
type: z.enum(['selector', 'text', 'ipc-event']).describe('What to wait for'),
|
|
37
|
+
value: z.string().describe('Selector, text content, or IPC event name to wait for'),
|
|
38
|
+
timeout: z.number().optional().default(5000).describe('Timeout in milliseconds (default: 5000ms)'),
|
|
39
|
+
});
|
|
40
|
+
export const GetStylesSchema = z.object({
|
|
41
|
+
selector: z.string().describe('CSS selector for element(s) to get styles from'),
|
|
42
|
+
properties: z.array(z.string()).optional().describe('Specific CSS properties to retrieve. If omitted, returns all computed styles'),
|
|
43
|
+
multiple: z.boolean().optional().default(false)
|
|
44
|
+
.describe('Whether to get styles for all matching elements (true) or just the first (false)'),
|
|
45
|
+
});
|
|
46
|
+
export const ExecuteJavaScriptSchema = z.object({
|
|
47
|
+
script: z.string().describe('JavaScript code to execute in the webview context'),
|
|
48
|
+
args: z.array(z.unknown()).optional().describe('Arguments to pass to the script'),
|
|
49
|
+
});
|
|
50
|
+
export const FocusElementSchema = z.object({
|
|
51
|
+
selector: z.string().describe('CSS selector for element to focus'),
|
|
52
|
+
});
|
|
53
|
+
export const FindElementSchema = z.object({
|
|
54
|
+
selector: z.string(),
|
|
55
|
+
strategy: z.enum(['css', 'xpath', 'text']).default('css'),
|
|
56
|
+
});
|
|
57
|
+
export const GetConsoleLogsSchema = z.object({
|
|
58
|
+
filter: z.string().optional().describe('Regex or keyword to filter logs'),
|
|
59
|
+
since: z.string().optional().describe('ISO timestamp to filter logs since'),
|
|
60
|
+
});
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Implementation Functions
|
|
63
|
+
// ============================================================================
|
|
64
|
+
export async function interact(options) {
|
|
65
|
+
const { action, selector, x, y, duration, scrollX, scrollY, fromX, fromY, toX, toY } = options;
|
|
66
|
+
// Handle swipe action separately since it has different logic
|
|
67
|
+
if (action === 'swipe') {
|
|
68
|
+
return performSwipe(fromX, fromY, toX, toY, duration);
|
|
69
|
+
}
|
|
70
|
+
const script = buildScript(SCRIPTS.interact, {
|
|
71
|
+
action,
|
|
72
|
+
selector: selector ?? null,
|
|
73
|
+
x: x ?? null,
|
|
74
|
+
y: y ?? null,
|
|
75
|
+
duration: duration ?? 500,
|
|
76
|
+
scrollX: scrollX ?? 0,
|
|
77
|
+
scrollY: scrollY ?? 0,
|
|
78
|
+
});
|
|
79
|
+
try {
|
|
80
|
+
return await executeInWebview(script);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
84
|
+
throw new Error(`Interaction failed: ${message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function performSwipe(fromX, fromY, toX, toY, duration = 300) {
|
|
88
|
+
if (fromX === undefined || fromY === undefined || toX === undefined || toY === undefined) {
|
|
89
|
+
throw new Error('Swipe action requires fromX, fromY, toX, and toY coordinates');
|
|
90
|
+
}
|
|
91
|
+
const script = buildScript(SCRIPTS.swipe, { fromX, fromY, toX, toY, duration });
|
|
92
|
+
try {
|
|
93
|
+
return await executeInWebview(script);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
97
|
+
throw new Error(`Swipe failed: ${message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export async function screenshot(quality, format = 'png') {
|
|
101
|
+
// Use the native screenshot function from webview-executor
|
|
102
|
+
return captureScreenshot(format, quality);
|
|
103
|
+
}
|
|
104
|
+
export async function keyboard(action, selectorOrKey, textOrModifiers, modifiers) {
|
|
105
|
+
// Handle the different parameter combinations based on action
|
|
106
|
+
if (action === 'type') {
|
|
107
|
+
const selector = selectorOrKey;
|
|
108
|
+
const text = textOrModifiers;
|
|
109
|
+
if (!selector || !text) {
|
|
110
|
+
throw new Error('Type action requires both selector and text parameters');
|
|
111
|
+
}
|
|
112
|
+
const script = buildTypeScript(selector, text);
|
|
113
|
+
try {
|
|
114
|
+
return await executeInWebview(script);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
118
|
+
throw new Error(`Type action failed: ${message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// For press/down/up actions: key is required, modifiers optional
|
|
122
|
+
const key = selectorOrKey;
|
|
123
|
+
const mods = Array.isArray(textOrModifiers) ? textOrModifiers : modifiers;
|
|
124
|
+
if (!key) {
|
|
125
|
+
throw new Error(`${action} action requires a key parameter`);
|
|
126
|
+
}
|
|
127
|
+
const script = buildKeyEventScript(action, key, mods || []);
|
|
128
|
+
try {
|
|
129
|
+
return await executeInWebview(script);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
+
throw new Error(`Keyboard action failed: ${message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export async function waitFor(type, value, timeout = 5000) {
|
|
137
|
+
const script = buildScript(SCRIPTS.waitFor, { type, value, timeout });
|
|
138
|
+
try {
|
|
139
|
+
return await executeInWebview(script);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
143
|
+
throw new Error(`Wait failed: ${message}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export async function getStyles(selector, properties, multiple = false) {
|
|
147
|
+
const script = buildScript(SCRIPTS.getStyles, {
|
|
148
|
+
selector,
|
|
149
|
+
properties: properties || [],
|
|
150
|
+
multiple,
|
|
151
|
+
});
|
|
152
|
+
try {
|
|
153
|
+
return await executeInWebview(script);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
157
|
+
throw new Error(`Get styles failed: ${message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export async function executeJavaScript(script, args) {
|
|
161
|
+
// If args are provided, we need to inject them into the script context
|
|
162
|
+
const wrappedScript = args && args.length > 0
|
|
163
|
+
? `
|
|
164
|
+
(function() {
|
|
165
|
+
const args = ${JSON.stringify(args)};
|
|
166
|
+
return (${script}).apply(null, args);
|
|
167
|
+
})();
|
|
168
|
+
`
|
|
169
|
+
: script;
|
|
170
|
+
try {
|
|
171
|
+
const result = await executeInWebview(wrappedScript);
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
176
|
+
throw new Error(`JavaScript execution failed: ${message}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
export async function focusElement(selector) {
|
|
180
|
+
const script = buildScript(SCRIPTS.focus, { selector });
|
|
181
|
+
try {
|
|
182
|
+
return await executeInWebview(script);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
186
|
+
throw new Error(`Focus failed: ${message}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Find an element using various selector strategies.
|
|
191
|
+
*/
|
|
192
|
+
export async function findElement(selector, strategy) {
|
|
193
|
+
const script = buildScript(SCRIPTS.findElement, { selector, strategy });
|
|
194
|
+
try {
|
|
195
|
+
return await executeInWebview(script);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
199
|
+
throw new Error(`Find element failed: ${message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Get console logs from the webview.
|
|
204
|
+
*/
|
|
205
|
+
export async function getConsoleLogs(filter, since) {
|
|
206
|
+
try {
|
|
207
|
+
return await getConsoleLogsFromCapture(filter, since);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
211
|
+
throw new Error(`Failed to get console logs: ${message}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
// Import the single source of truth for all tools
|
|
10
|
+
import { TOOLS, TOOL_MAP } from './tools-registry.js';
|
|
11
|
+
/* eslint-disable no-process-exit */
|
|
12
|
+
// Read version from package.json
|
|
13
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const packageJson = JSON.parse(readFileSync(join(currentDir, '..', 'package.json'), 'utf-8'));
|
|
15
|
+
const VERSION = packageJson.version;
|
|
16
|
+
// Initialize server
|
|
17
|
+
const server = new Server({
|
|
18
|
+
name: 'mcp-server-tauri',
|
|
19
|
+
version: VERSION,
|
|
20
|
+
}, {
|
|
21
|
+
capabilities: {
|
|
22
|
+
tools: {},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
// Handle connection errors gracefully - don't crash on broken pipe
|
|
26
|
+
server.onerror = (error) => {
|
|
27
|
+
// Ignore broken pipe errors - they happen when the client disconnects
|
|
28
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
29
|
+
if (message.includes('broken pipe') || message.includes('EPIPE')) {
|
|
30
|
+
// Client disconnected, exit gracefully
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
// For other errors, log to stderr (will be captured by MCP client)
|
|
34
|
+
console.error('[MCP Server Error]', message);
|
|
35
|
+
};
|
|
36
|
+
// Handle connection close - exit gracefully
|
|
37
|
+
server.onclose = () => {
|
|
38
|
+
process.exit(0);
|
|
39
|
+
};
|
|
40
|
+
// Tool list handler - generated from registry
|
|
41
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
42
|
+
return {
|
|
43
|
+
tools: TOOLS.map((tool) => {
|
|
44
|
+
return {
|
|
45
|
+
name: tool.name,
|
|
46
|
+
description: tool.description,
|
|
47
|
+
inputSchema: zodToJsonSchema(tool.schema),
|
|
48
|
+
};
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
// Tool call handler - generated from registry
|
|
53
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
54
|
+
try {
|
|
55
|
+
const tool = TOOL_MAP.get(request.params.name);
|
|
56
|
+
if (!tool) {
|
|
57
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
58
|
+
}
|
|
59
|
+
const output = await tool.handler(request.params.arguments);
|
|
60
|
+
return { content: [{ type: 'text', text: output }] };
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
66
|
+
isError: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// Start server
|
|
71
|
+
async function main() {
|
|
72
|
+
const transport = new StdioServerTransport();
|
|
73
|
+
await server.connect(transport);
|
|
74
|
+
// Don't log to stderr - it interferes with MCP protocol
|
|
75
|
+
}
|
|
76
|
+
main().catch(() => {
|
|
77
|
+
// Don't log errors to stderr - just exit silently
|
|
78
|
+
// The error will be in the MCP response if needed
|
|
79
|
+
process.exit(1);
|
|
80
|
+
});
|