@hypothesi/tauri-mcp-server 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -28
- package/dist/driver/plugin-commands.js +0 -15
- package/dist/driver/session-manager.js +28 -3
- package/dist/driver/webview-executor.js +35 -10
- package/dist/driver/webview-interactions.js +27 -3
- package/dist/index.js +27 -1
- package/dist/manager/mobile.js +0 -44
- package/dist/monitor/logs.js +15 -3
- package/dist/prompts-registry.js +109 -1
- package/dist/tools-registry.js +31 -170
- package/package.json +1 -1
- package/dist/manager/cli.js +0 -128
- package/dist/manager/config.js +0 -142
- package/dist/manager/docs.js +0 -213
package/README.md
CHANGED
|
@@ -13,10 +13,8 @@ A **Model Context Protocol (MCP) server** that enables AI assistants like Claude
|
|
|
13
13
|
|----------|-------------|
|
|
14
14
|
| 🎯 **UI Automation** | Screenshots, clicks, typing, scrolling, element finding |
|
|
15
15
|
| 🔍 **IPC Monitoring** | Capture and inspect Tauri IPC calls in real-time |
|
|
16
|
-
| 📱 **Mobile Dev** |
|
|
17
|
-
|
|
|
18
|
-
| ⚙️ **Configuration** | Read/write Tauri config files with validation |
|
|
19
|
-
| 📋 **Logs** | Stream Android logcat, iOS device logs, system logs |
|
|
16
|
+
| 📱 **Mobile Dev** | List Android emulators & iOS simulators |
|
|
17
|
+
| 📋 **Logs** | Stream console, Android logcat, iOS, and system logs |
|
|
20
18
|
|
|
21
19
|
## Quick Start
|
|
22
20
|
|
|
@@ -54,51 +52,38 @@ npx -y install-mcp @hypothesi/tauri-mcp-server --client claude-code
|
|
|
54
52
|
|
|
55
53
|
Supported clients: `claude-code`, `cursor`, `windsurf`, `vscode`, `cline`, `roo-cline`, `claude`, `zed`, `goose`, `warp`, `codex`
|
|
56
54
|
|
|
57
|
-
## Available Tools
|
|
55
|
+
## Available Tools (16 total)
|
|
58
56
|
|
|
59
57
|
### UI Automation
|
|
60
58
|
|
|
61
59
|
| Tool | Description |
|
|
62
60
|
|------|-------------|
|
|
63
|
-
| `
|
|
64
|
-
| `tauri_list_windows` | List all open webview windows |
|
|
61
|
+
| `tauri_driver_session` | Start/stop/status automation session |
|
|
65
62
|
| `tauri_webview_find_element` | Find elements by selector |
|
|
66
|
-
| `
|
|
63
|
+
| `tauri_read_logs` | Read console, Android, iOS, or system logs |
|
|
64
|
+
| `tauri_webview_interact` | Click, scroll, swipe, focus, long-press |
|
|
65
|
+
| `tauri_webview_screenshot` | Capture webview screenshots |
|
|
67
66
|
| `tauri_webview_keyboard` | Type text or send key events |
|
|
68
67
|
| `tauri_webview_wait_for` | Wait for elements, text, or events |
|
|
69
68
|
| `tauri_webview_get_styles` | Get computed CSS styles |
|
|
70
69
|
| `tauri_webview_execute_js` | Execute JavaScript in webview |
|
|
71
|
-
| `
|
|
72
|
-
| `tauri_driver_get_console_logs` | Get browser console logs |
|
|
73
|
-
| `tauri_read_platform_logs` | Read Android/iOS/system logs |
|
|
74
|
-
| `tauri_driver_session` | Start/stop automation session |
|
|
70
|
+
| `tauri_list_windows` | List all open webview windows |
|
|
75
71
|
|
|
76
72
|
### IPC & Plugin
|
|
77
73
|
|
|
78
74
|
| Tool | Description |
|
|
79
75
|
|------|-------------|
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `tauri_plugin_emit_event` | Emit custom events |
|
|
76
|
+
| `tauri_ipc_execute_command` | Execute Tauri IPC commands |
|
|
77
|
+
| `tauri_ipc_get_backend_state` | Get app metadata and state |
|
|
78
|
+
| `tauri_ipc_monitor` | Start/stop IPC monitoring |
|
|
79
|
+
| `tauri_ipc_get_captured` | Get captured IPC traffic |
|
|
80
|
+
| `tauri_ipc_emit_event` | Emit custom events |
|
|
86
81
|
|
|
87
82
|
### Mobile Development
|
|
88
83
|
|
|
89
84
|
| Tool | Description |
|
|
90
85
|
|------|-------------|
|
|
91
86
|
| `tauri_list_devices` | List Android devices and iOS simulators |
|
|
92
|
-
| `tauri_launch_emulator` | Launch Android AVD or iOS Simulator |
|
|
93
|
-
|
|
94
|
-
### Project Management
|
|
95
|
-
|
|
96
|
-
| Tool | Description |
|
|
97
|
-
|------|-------------|
|
|
98
|
-
| `tauri_run_command` | Run any Tauri CLI command |
|
|
99
|
-
| `tauri_read_config` | Read Tauri config files |
|
|
100
|
-
| `tauri_write_config` | Write config files with validation |
|
|
101
|
-
| `tauri_get_docs` | Fetch Tauri documentation |
|
|
102
87
|
|
|
103
88
|
## Links
|
|
104
89
|
|
|
@@ -24,21 +24,6 @@ export async function executeIPCCommand(command, args = {}) {
|
|
|
24
24
|
return JSON.stringify({ success: false, error: message });
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
export const GetWindowInfoSchema = z.object({});
|
|
28
|
-
export async function getWindowInfo() {
|
|
29
|
-
try {
|
|
30
|
-
const result = await executeIPCCommand('plugin:mcp-bridge|get_window_info');
|
|
31
|
-
const parsed = JSON.parse(result);
|
|
32
|
-
if (!parsed.success) {
|
|
33
|
-
throw new Error(parsed.error || 'Unknown error');
|
|
34
|
-
}
|
|
35
|
-
return JSON.stringify(parsed.result);
|
|
36
|
-
}
|
|
37
|
-
catch (error) {
|
|
38
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
-
throw new Error(`Failed to get window info: ${message}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
27
|
// Combined schema for managing IPC monitoring
|
|
43
28
|
export const ManageIPCMonitoringSchema = z.object({
|
|
44
29
|
action: z.enum(['start', 'stop']).describe('Action to perform: start or stop IPC monitoring'),
|
|
@@ -1,7 +1,7 @@
|
|
|
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 } from './plugin-client.js';
|
|
4
|
+
import { resetPluginClient, getPluginClient } from './plugin-client.js';
|
|
5
5
|
import { resetInitialization } from './webview-executor.js';
|
|
6
6
|
/**
|
|
7
7
|
* Session Manager - Native IPC-based session management
|
|
@@ -18,7 +18,7 @@ import { resetInitialization } from './webview-executor.js';
|
|
|
18
18
|
// Schemas
|
|
19
19
|
// ============================================================================
|
|
20
20
|
export const ManageDriverSessionSchema = z.object({
|
|
21
|
-
action: z.enum(['start', 'stop']).describe('Action to perform: start or stop the session'),
|
|
21
|
+
action: z.enum(['start', 'stop', 'status']).describe('Action to perform: start or stop the session, or check status'),
|
|
22
22
|
host: z.string().optional().describe('Host address to connect to (e.g., 192.168.1.100). Falls back to MCP_BRIDGE_HOST or TAURI_DEV_HOST env vars'),
|
|
23
23
|
port: z.number().optional().describe('Port to connect to (default: 9223)'),
|
|
24
24
|
});
|
|
@@ -26,7 +26,8 @@ export const ManageDriverSessionSchema = z.object({
|
|
|
26
26
|
// Module State
|
|
27
27
|
// ============================================================================
|
|
28
28
|
// AppDiscovery instance - recreated when host changes
|
|
29
|
-
|
|
29
|
+
// Track current session info
|
|
30
|
+
let appDiscovery = null, currentSession = null;
|
|
30
31
|
function getAppDiscovery(host) {
|
|
31
32
|
if (!appDiscovery || appDiscovery.host !== host) {
|
|
32
33
|
appDiscovery = new AppDiscovery(host);
|
|
@@ -63,6 +64,24 @@ async function tryConnect(host, port) {
|
|
|
63
64
|
* @param port - Optional port number (defaults to 9223)
|
|
64
65
|
*/
|
|
65
66
|
export async function manageDriverSession(action, host, port) {
|
|
67
|
+
// Handle status action
|
|
68
|
+
if (action === 'status') {
|
|
69
|
+
const client = getPluginClient();
|
|
70
|
+
if (client.isConnected() && currentSession) {
|
|
71
|
+
return JSON.stringify({
|
|
72
|
+
connected: true,
|
|
73
|
+
app: currentSession.name,
|
|
74
|
+
host: currentSession.host,
|
|
75
|
+
port: currentSession.port,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return JSON.stringify({
|
|
79
|
+
connected: false,
|
|
80
|
+
app: null,
|
|
81
|
+
host: null,
|
|
82
|
+
port: null,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
66
85
|
if (action === 'start') {
|
|
67
86
|
// Reset any existing plugin client to ensure fresh connection
|
|
68
87
|
resetPluginClient();
|
|
@@ -72,6 +91,7 @@ export async function manageDriverSession(action, host, port) {
|
|
|
72
91
|
if (configuredHost !== 'localhost' && configuredHost !== '127.0.0.1') {
|
|
73
92
|
try {
|
|
74
93
|
const session = await tryConnect('localhost', configuredPort);
|
|
94
|
+
currentSession = session;
|
|
75
95
|
return `Session started with app: ${session.name} (localhost:${session.port})`;
|
|
76
96
|
}
|
|
77
97
|
catch {
|
|
@@ -81,6 +101,7 @@ export async function manageDriverSession(action, host, port) {
|
|
|
81
101
|
// Strategy 2: Try the configured/provided host
|
|
82
102
|
try {
|
|
83
103
|
const session = await tryConnect(configuredHost, configuredPort);
|
|
104
|
+
currentSession = session;
|
|
84
105
|
return `Session started with app: ${session.name} (${session.host}:${session.port})`;
|
|
85
106
|
}
|
|
86
107
|
catch {
|
|
@@ -94,6 +115,7 @@ export async function manageDriverSession(action, host, port) {
|
|
|
94
115
|
// Reset client again to connect to discovered port
|
|
95
116
|
resetPluginClient();
|
|
96
117
|
const session = await tryConnect('localhost', firstApp.port);
|
|
118
|
+
currentSession = session;
|
|
97
119
|
return `Session started with app: ${session.name} (localhost:${session.port})`;
|
|
98
120
|
}
|
|
99
121
|
catch {
|
|
@@ -104,10 +126,12 @@ export async function manageDriverSession(action, host, port) {
|
|
|
104
126
|
try {
|
|
105
127
|
resetPluginClient();
|
|
106
128
|
const session = await tryConnect(configuredHost, configuredPort);
|
|
129
|
+
currentSession = session;
|
|
107
130
|
return `Session started with app: ${session.name} (${session.host}:${session.port})`;
|
|
108
131
|
}
|
|
109
132
|
catch {
|
|
110
133
|
// All attempts failed
|
|
134
|
+
currentSession = null;
|
|
111
135
|
return `Session started (native IPC mode - no Tauri app found at localhost or ${configuredHost}:${configuredPort})`;
|
|
112
136
|
}
|
|
113
137
|
}
|
|
@@ -117,5 +141,6 @@ export async function manageDriverSession(action, host, port) {
|
|
|
117
141
|
}
|
|
118
142
|
resetPluginClient();
|
|
119
143
|
resetInitialization();
|
|
144
|
+
currentSession = null;
|
|
120
145
|
return 'Session stopped';
|
|
121
146
|
}
|
|
@@ -206,16 +206,41 @@ export async function clearConsoleLogs() {
|
|
|
206
206
|
`;
|
|
207
207
|
return executeInWebview(script);
|
|
208
208
|
}
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Parse a data URL to extract the base64 data and mime type.
|
|
211
|
+
*/
|
|
212
|
+
function parseDataUrl(dataUrl) {
|
|
213
|
+
const match = dataUrl.match(/^data:(image\/(?:png|jpeg));base64,(.+)$/);
|
|
214
|
+
if (!match) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
return { mimeType: match[1], data: match[2] };
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Build screenshot result with image content and optional text context.
|
|
221
|
+
*/
|
|
222
|
+
function buildScreenshotResult(dataUrl, method, windowContext) {
|
|
223
|
+
const parsed = parseDataUrl(dataUrl);
|
|
224
|
+
if (!parsed) {
|
|
225
|
+
throw new Error(`Invalid data URL format: ${dataUrl.substring(0, 50)}...`);
|
|
226
|
+
}
|
|
227
|
+
const content = [];
|
|
228
|
+
// Add context text if there's window info or warnings
|
|
229
|
+
let contextText = `Screenshot captured via ${method}`;
|
|
211
230
|
if (windowContext) {
|
|
212
|
-
|
|
231
|
+
contextText += ` in window "${windowContext.windowLabel}"`;
|
|
213
232
|
if (windowContext.warning) {
|
|
214
|
-
|
|
233
|
+
contextText += `\n\n⚠️ ${windowContext.warning}`;
|
|
215
234
|
}
|
|
216
235
|
}
|
|
217
|
-
|
|
218
|
-
|
|
236
|
+
content.push({ type: 'text', text: contextText });
|
|
237
|
+
// Add the image content
|
|
238
|
+
content.push({
|
|
239
|
+
type: 'image',
|
|
240
|
+
data: parsed.data,
|
|
241
|
+
mimeType: parsed.mimeType,
|
|
242
|
+
});
|
|
243
|
+
return { content };
|
|
219
244
|
}
|
|
220
245
|
/**
|
|
221
246
|
* Prepares the html2canvas script for screenshot capture.
|
|
@@ -242,7 +267,7 @@ async function prepareHtml2canvasScript(format, quality) {
|
|
|
242
267
|
* Capture a screenshot of the entire webview.
|
|
243
268
|
*
|
|
244
269
|
* @param options - Screenshot options (format, quality, windowId)
|
|
245
|
-
* @returns
|
|
270
|
+
* @returns Screenshot result with image content
|
|
246
271
|
*/
|
|
247
272
|
export async function captureScreenshot(options = {}) {
|
|
248
273
|
const { format = 'png', quality = 90, windowId } = options;
|
|
@@ -272,7 +297,7 @@ export async function captureScreenshot(options = {}) {
|
|
|
272
297
|
throw new Error('Native screenshot returned invalid data');
|
|
273
298
|
}
|
|
274
299
|
// Build response with window context
|
|
275
|
-
return
|
|
300
|
+
return buildScreenshotResult(dataUrl, 'native API', response.windowContext);
|
|
276
301
|
}
|
|
277
302
|
catch (nativeError) {
|
|
278
303
|
// Log the native error for debugging, then fall back
|
|
@@ -343,7 +368,7 @@ export async function captureScreenshot(options = {}) {
|
|
|
343
368
|
const result = await executeAsyncInWebview(html2canvasScript, undefined, 10000); // Longer timeout for library loading
|
|
344
369
|
// Validate that we got a real data URL, not 'null' or empty
|
|
345
370
|
if (result && result !== 'null' && result.startsWith('data:image/')) {
|
|
346
|
-
return
|
|
371
|
+
return buildScreenshotResult(result, 'html2canvas');
|
|
347
372
|
}
|
|
348
373
|
throw new Error(`html2canvas returned invalid result: ${result?.substring(0, 100) || 'null'}`);
|
|
349
374
|
}
|
|
@@ -353,7 +378,7 @@ export async function captureScreenshot(options = {}) {
|
|
|
353
378
|
const result = await executeAsyncInWebview(screenCaptureScript);
|
|
354
379
|
// Validate that we got a real data URL
|
|
355
380
|
if (result && result.startsWith('data:image/')) {
|
|
356
|
-
return
|
|
381
|
+
return buildScreenshotResult(result, 'Screen Capture API');
|
|
357
382
|
}
|
|
358
383
|
throw new Error(`Screen Capture API returned invalid result: ${result?.substring(0, 50) || 'null'}`);
|
|
359
384
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
2
4
|
import { executeInWebview, executeInWebviewWithContext, captureScreenshot, getConsoleLogs as getConsoleLogsFromCapture, } from './webview-executor.js';
|
|
3
5
|
import { SCRIPTS, buildScript, buildTypeScript, buildKeyEventScript } from './scripts/index.js';
|
|
4
6
|
// ============================================================================
|
|
@@ -15,7 +17,7 @@ export const WindowTargetSchema = z.object({
|
|
|
15
17
|
// Schemas
|
|
16
18
|
// ============================================================================
|
|
17
19
|
export const InteractSchema = WindowTargetSchema.extend({
|
|
18
|
-
action: z.enum(['click', 'double-click', 'long-press', 'scroll', 'swipe'])
|
|
20
|
+
action: z.enum(['click', 'double-click', 'long-press', 'scroll', 'swipe', 'focus'])
|
|
19
21
|
.describe('Type of interaction to perform'),
|
|
20
22
|
selector: z.string().optional().describe('CSS selector for the element to interact with'),
|
|
21
23
|
x: z.number().optional().describe('X coordinate for direct coordinate interaction'),
|
|
@@ -32,6 +34,7 @@ export const InteractSchema = WindowTargetSchema.extend({
|
|
|
32
34
|
export const ScreenshotSchema = WindowTargetSchema.extend({
|
|
33
35
|
format: z.enum(['png', 'jpeg']).optional().default('png').describe('Image format'),
|
|
34
36
|
quality: z.number().min(0).max(100).optional().describe('JPEG quality (0-100, only for jpeg format)'),
|
|
37
|
+
filePath: z.string().optional().describe('File path to save the screenshot to instead of returning as base64'),
|
|
35
38
|
});
|
|
36
39
|
export const KeyboardSchema = WindowTargetSchema.extend({
|
|
37
40
|
action: z.enum(['type', 'press', 'down', 'up'])
|
|
@@ -76,6 +79,13 @@ export async function interact(options) {
|
|
|
76
79
|
if (action === 'swipe') {
|
|
77
80
|
return performSwipe({ fromX, fromY, toX, toY, duration, windowId });
|
|
78
81
|
}
|
|
82
|
+
// Handle focus action
|
|
83
|
+
if (action === 'focus') {
|
|
84
|
+
if (!selector) {
|
|
85
|
+
throw new Error('Focus action requires a selector');
|
|
86
|
+
}
|
|
87
|
+
return focusElement({ selector, windowId });
|
|
88
|
+
}
|
|
79
89
|
const script = buildScript(SCRIPTS.interact, {
|
|
80
90
|
action,
|
|
81
91
|
selector: selector ?? null,
|
|
@@ -108,9 +118,23 @@ async function performSwipe(options) {
|
|
|
108
118
|
}
|
|
109
119
|
}
|
|
110
120
|
export async function screenshot(options = {}) {
|
|
111
|
-
const { quality, format = 'png', windowId } = options;
|
|
121
|
+
const { quality, format = 'png', windowId, filePath } = options;
|
|
112
122
|
// Use the native screenshot function from webview-executor
|
|
113
|
-
|
|
123
|
+
const result = await captureScreenshot({ format, quality, windowId });
|
|
124
|
+
// If filePath is provided, write to file instead of returning base64
|
|
125
|
+
if (filePath) {
|
|
126
|
+
// Find the image content in the result
|
|
127
|
+
const imageContent = result.content.find((c) => { return c.type === 'image'; });
|
|
128
|
+
if (!imageContent || imageContent.type !== 'image') {
|
|
129
|
+
throw new Error('Screenshot capture failed: no image data');
|
|
130
|
+
}
|
|
131
|
+
// Decode base64 and write to file
|
|
132
|
+
const buffer = Buffer.from(imageContent.data, 'base64');
|
|
133
|
+
const resolvedPath = resolve(filePath);
|
|
134
|
+
await writeFile(resolvedPath, buffer);
|
|
135
|
+
return { filePath: resolvedPath, format };
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
114
138
|
}
|
|
115
139
|
export async function keyboard(options) {
|
|
116
140
|
const { action, selectorOrKey, textOrModifiers, modifiers, windowId } = options;
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,32 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
52
52
|
}),
|
|
53
53
|
};
|
|
54
54
|
});
|
|
55
|
+
/**
|
|
56
|
+
* Convert a ToolResult to MCP content array.
|
|
57
|
+
* Handles string (legacy), single content, and content arrays.
|
|
58
|
+
*/
|
|
59
|
+
function toolResultToContent(result) {
|
|
60
|
+
// Legacy string result - convert to text content
|
|
61
|
+
if (typeof result === 'string') {
|
|
62
|
+
return [{ type: 'text', text: result }];
|
|
63
|
+
}
|
|
64
|
+
// Array of content items
|
|
65
|
+
if (Array.isArray(result)) {
|
|
66
|
+
return result.map(contentToMcp);
|
|
67
|
+
}
|
|
68
|
+
// Single content item
|
|
69
|
+
return [contentToMcp(result)];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Convert a single ToolContent to MCP format.
|
|
73
|
+
*/
|
|
74
|
+
function contentToMcp(content) {
|
|
75
|
+
if (content.type === 'text') {
|
|
76
|
+
return { type: 'text', text: content.text };
|
|
77
|
+
}
|
|
78
|
+
// Image content
|
|
79
|
+
return { type: 'image', data: content.data, mimeType: content.mimeType };
|
|
80
|
+
}
|
|
55
81
|
// Tool call handler - generated from registry
|
|
56
82
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
57
83
|
try {
|
|
@@ -60,7 +86,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
60
86
|
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
61
87
|
}
|
|
62
88
|
const output = await tool.handler(request.params.arguments);
|
|
63
|
-
return { content:
|
|
89
|
+
return { content: toolResultToContent(output) };
|
|
64
90
|
}
|
|
65
91
|
catch (error) {
|
|
66
92
|
const message = error instanceof Error ? error.message : String(error);
|
package/dist/manager/mobile.js
CHANGED
|
@@ -37,47 +37,3 @@ export async function listDevices() {
|
|
|
37
37
|
]);
|
|
38
38
|
return { android, ios };
|
|
39
39
|
}
|
|
40
|
-
export const LaunchEmulatorSchema = z.object({
|
|
41
|
-
platform: z.enum(['android', 'ios']),
|
|
42
|
-
name: z.string().describe('Name of the AVD or Simulator'),
|
|
43
|
-
});
|
|
44
|
-
export async function launchEmulator(platform, name) {
|
|
45
|
-
if (platform === 'android') {
|
|
46
|
-
try {
|
|
47
|
-
// Launch Android Emulator - Try to spawn, but immediately await to catch ENOENT
|
|
48
|
-
await execa('emulator', ['-avd', name], {
|
|
49
|
-
detached: true,
|
|
50
|
-
stdio: 'ignore',
|
|
51
|
-
timeout: 1000, // Just check if it spawns
|
|
52
|
-
});
|
|
53
|
-
return `Launching Android AVD: ${name}`;
|
|
54
|
-
}
|
|
55
|
-
catch (error) {
|
|
56
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
57
|
-
throw new Error(`Failed to launch Android emulator: ${message}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
else if (platform === 'ios') {
|
|
61
|
-
// Check if we're on macOS first
|
|
62
|
-
if (process.platform !== 'darwin') {
|
|
63
|
-
throw new Error('iOS simulators are only available on macOS');
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
// Launch iOS Simulator
|
|
67
|
-
await execa('xcrun', ['simctl', 'boot', name]);
|
|
68
|
-
await execa('open', ['-a', 'Simulator']);
|
|
69
|
-
return `Booted iOS Simulator: ${name}`;
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
-
// Provide more helpful error messages
|
|
74
|
-
if (message.includes('xcrun: error')) {
|
|
75
|
-
throw new Error('Xcode is not installed. Please install Xcode from the App Store to use iOS simulators.');
|
|
76
|
-
}
|
|
77
|
-
throw new Error(`Failed to launch iOS simulator: ${message}`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
throw new Error(`Unsupported platform: ${platform}. Use 'android' or 'ios'.`);
|
|
82
|
-
}
|
|
83
|
-
}
|
package/dist/monitor/logs.js
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { execa } from 'execa';
|
|
3
|
+
import { getConsoleLogs } from '../driver/webview-interactions.js';
|
|
3
4
|
export const ReadLogsSchema = z.object({
|
|
4
|
-
source: z.enum(['android', 'ios', 'system'])
|
|
5
|
+
source: z.enum(['console', 'android', 'ios', 'system'])
|
|
6
|
+
.describe('Log source: "console" for webview JS logs, "android" for logcat, "ios" for simulator, "system" for desktop'),
|
|
5
7
|
lines: z.number().default(50),
|
|
6
8
|
filter: z.string().optional().describe('Regex or keyword to filter logs'),
|
|
7
9
|
since: z.string().optional().describe('ISO timestamp to filter logs since (e.g. 2023-10-27T10:00:00Z)'),
|
|
10
|
+
windowId: z.string().optional().describe('Window label for console logs (defaults to "main")'),
|
|
8
11
|
});
|
|
9
|
-
export async function readLogs(
|
|
12
|
+
export async function readLogs(options) {
|
|
13
|
+
const { source, lines = 50, filter, since, windowId } = options;
|
|
10
14
|
try {
|
|
11
15
|
let output = '';
|
|
16
|
+
// Handle console logs (webview JS logs)
|
|
17
|
+
if (source === 'console') {
|
|
18
|
+
return await getConsoleLogs({ filter, since, windowId });
|
|
19
|
+
}
|
|
12
20
|
if (source === 'android') {
|
|
21
|
+
// Find adb - check ANDROID_HOME first, then fall back to PATH
|
|
22
|
+
// eslint-disable-next-line no-process-env
|
|
23
|
+
const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
|
|
24
|
+
const adbPath = androidHome ? `${androidHome}/platform-tools/adb` : 'adb';
|
|
13
25
|
const args = ['logcat', '-d'];
|
|
14
26
|
if (since) {
|
|
15
27
|
// adb logcat -T expects "MM-DD HH:MM:SS.mmm"
|
|
@@ -26,7 +38,7 @@ export async function readLogs(source, lines, filter, since) {
|
|
|
26
38
|
else {
|
|
27
39
|
args.push('-t', lines.toString());
|
|
28
40
|
}
|
|
29
|
-
const { stdout } = await execa(
|
|
41
|
+
const { stdout } = await execa(adbPath, args, { timeout: 10000 });
|
|
30
42
|
output = stdout;
|
|
31
43
|
}
|
|
32
44
|
else if (source === 'ios') {
|
package/dist/prompts-registry.js
CHANGED
|
@@ -8,7 +8,7 @@ Please follow these steps:
|
|
|
8
8
|
|
|
9
9
|
1. **Start a session** - Use \`tauri_driver_session\` with action "start" to connect to the running Tauri app
|
|
10
10
|
|
|
11
|
-
2. **Get console logs** - Use \`
|
|
11
|
+
2. **Get console logs** - Use \`tauri_read_logs\` with source "console" to retrieve JavaScript errors or warnings
|
|
12
12
|
|
|
13
13
|
3. **Analyze the errors** - Look at the error messages, stack traces, and identify:
|
|
14
14
|
- What type of error it is (TypeError, ReferenceError, SyntaxError, etc.)
|
|
@@ -24,6 +24,96 @@ 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 so I can use these AI development tools.
|
|
28
|
+
|
|
29
|
+
## Prerequisites
|
|
30
|
+
|
|
31
|
+
- This is a **Tauri v2** project (check for \`src-tauri/\` directory and \`tauri.conf.json\`)
|
|
32
|
+
- If this is NOT a Tauri project, stop and let the user know this setup only applies to Tauri apps
|
|
33
|
+
|
|
34
|
+
## Setup Steps
|
|
35
|
+
|
|
36
|
+
### Step 1: Add the Rust Plugin
|
|
37
|
+
|
|
38
|
+
Add the plugin to \`src-tauri/Cargo.toml\` dependencies:
|
|
39
|
+
|
|
40
|
+
\`\`\`toml
|
|
41
|
+
[dependencies]
|
|
42
|
+
tauri-plugin-mcp-bridge = "0.2"
|
|
43
|
+
\`\`\`
|
|
44
|
+
|
|
45
|
+
Or run from the \`src-tauri\` directory:
|
|
46
|
+
\`\`\`bash
|
|
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
|
+
|
|
56
|
+
\`\`\`rust
|
|
57
|
+
let mut builder = tauri::Builder::default();
|
|
58
|
+
// ... existing plugins ...
|
|
59
|
+
|
|
60
|
+
#[cfg(debug_assertions)]
|
|
61
|
+
{
|
|
62
|
+
builder = builder.plugin(tauri_plugin_mcp_bridge::init());
|
|
63
|
+
}
|
|
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
|
+
\`\`\`
|
|
81
|
+
|
|
82
|
+
**This is required** - without it, the MCP bridge cannot communicate with the webview.
|
|
83
|
+
|
|
84
|
+
### Step 4: Add Plugin Permissions
|
|
85
|
+
|
|
86
|
+
Add the plugin permission to \`src-tauri/capabilities/default.json\` (create the file if it doesn't exist):
|
|
87
|
+
|
|
88
|
+
\`\`\`json
|
|
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
|
+
\`\`\`
|
|
99
|
+
|
|
100
|
+
If the file already exists, just add \`"mcp-bridge:default"\` to the existing permissions array.
|
|
101
|
+
|
|
102
|
+
## Verification
|
|
103
|
+
|
|
104
|
+
After setup:
|
|
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
|
|
109
|
+
|
|
110
|
+
## Notes
|
|
111
|
+
|
|
112
|
+
- The plugin only runs in debug builds (\`#[cfg(debug_assertions)]\`) so it won't affect production
|
|
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()\`
|
|
115
|
+
|
|
116
|
+
Please examine the project structure and make the necessary changes to set up the MCP bridge plugin.`;
|
|
27
117
|
/**
|
|
28
118
|
* Complete registry of all available prompts
|
|
29
119
|
*/
|
|
@@ -47,6 +137,24 @@ export const PROMPTS = [
|
|
|
47
137
|
];
|
|
48
138
|
},
|
|
49
139
|
},
|
|
140
|
+
{
|
|
141
|
+
name: 'setup',
|
|
142
|
+
description: 'Set up the MCP Bridge plugin in a Tauri project. ' +
|
|
143
|
+
'Guides through adding the Rust crate, registering the plugin, enabling withGlobalTauri, ' +
|
|
144
|
+
'and adding permissions. Use this when starting with a new Tauri project.',
|
|
145
|
+
arguments: [],
|
|
146
|
+
handler: () => {
|
|
147
|
+
return [
|
|
148
|
+
{
|
|
149
|
+
role: 'user',
|
|
150
|
+
content: {
|
|
151
|
+
type: 'text',
|
|
152
|
+
text: SETUP_PROMPT,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
},
|
|
157
|
+
},
|
|
50
158
|
];
|
|
51
159
|
/**
|
|
52
160
|
* Create a Map for fast prompt lookup by name
|