launch_app
Launch a Tauri desktop application for automated testing and management. Configure launch options like devtools, timeout, and features to control app behavior during execution.
Instructions
Launch Tauri app
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| wait_for_ready | No | Wait for ready | |
| timeout_secs | No | Timeout seconds | |
| features | No | Cargo features to enable | |
| devtools | No | Open devtools on launch |
Implementation Reference
- Tool schema definition for launch_app - defines the tool name, description, and input parameters using Zod schema (wait_for_ready, timeout_secs, features, devtools)launch_app: { name: 'launch_app', description: 'Launch Tauri app', inputSchema: z.object({ wait_for_ready: z.boolean().optional().describe('Wait for ready'), timeout_secs: z.number().optional().describe('Timeout seconds'), features: z.array(z.string()).optional().describe('Cargo features to enable'), devtools: z.boolean().optional().describe('Open devtools on launch'), }), },
- Tool handler for launch_app - receives args and calls tauriManager.launch(), then formats the result as MCP content responselaunch_app: async (args: { wait_for_ready?: boolean; timeout_secs?: number; features?: string[]; devtools?: boolean }) => { const result = await tauriManager.launch(args); return { content: [ { type: 'text' as const, text: JSON.stringify(result, null, 2), }, ], }; },
- Core launch implementation in TauriManager - handles idempotency checks, external app detection, port allocation, bundler detection, spawns the tauri dev process, waits for socket readiness, and verifies the app is readyasync launch(options: LaunchOptions = {}): Promise<LaunchResult> { const waitForReady = options.wait_for_ready ?? true; const devtools = options.devtools ?? false; // Handle features as string or array (MCP may pass string) let features: string[] = []; if (options.features) { if (Array.isArray(options.features)) { features = options.features; } else if (typeof options.features === 'string') { features = (options.features as string).split(',').map(f => f.trim()).filter(Boolean); } } if (!this.appConfig) { throw new Error('No Tauri app detected. Make sure src-tauri/Cargo.toml exists.'); } // Idempotent: if already running (managed by this instance), return current status if (this.process) { const errors = this.parseBackendLogs(this.outputBuffer); const backendHealth = errors.some(e => e.level === 'error') ? 'error' as const : 'healthy' as const; return { status: 'already_running', message: 'App is already running', port: this.vitePort, portOverrideApplied: true, // Was applied on initial launch buildHealth: { frontend: 'unknown', // Will be determined by frontend logs backend: backendHealth, }, errors: errors.filter(e => e.level === 'error'), }; } // Check if another instance already has an app running (external process) // This prevents duplicate launches that cause connection issues const externalAppRunning = await this.checkExternalAppRunning(); if (externalAppRunning) { throw new Error( 'Another Tauri app instance is already running and responding on the socket. ' + 'This can happen when launch_app is called from a different MCP session. ' + 'Please call stop_app first to terminate the existing app, then try launch_app again.' ); } // Determine timeout based on build cache existence // Fresh build: 300 seconds (5 minutes), Incremental build: 60 seconds const hasCachedBuild = this.hasBuildCache(); const defaultTimeout = hasCachedBuild ? 60 : 300; const timeoutSecs = options.timeout_secs ?? defaultTimeout; console.error(`[tauri-mcp] Build cache ${hasCachedBuild ? 'found' : 'not found'}, using ${timeoutSecs}s timeout`); // Reset detected paths this.detectedPipePath = null; this.detectedUnixSocketPath = null; // Clean up stale socket file (Unix only) if (process.platform !== 'win32') { const socketPath = this.getSocketPath(); if (fs.existsSync(socketPath)) { fs.unlinkSync(socketPath); } } // Dynamic port allocation with bundler detection const tauriConfig = this.readTauriConfig(); let portOverrideApplied = false; const warnings: string[] = []; let configOverride: string | null = null; if (tauriConfig?.beforeDevCommand) { const bundlerType = this.detectBundlerType(tauriConfig.beforeDevCommand); if (bundlerType === 'vite') { // Vite supported - apply dynamic port this.vitePort = await this.findAvailablePort(); const modifiedCommand = this.injectPortToCommand(tauriConfig.beforeDevCommand, this.vitePort); configOverride = JSON.stringify({ build: { beforeDevCommand: modifiedCommand, devUrl: `http://localhost:${this.vitePort}`, }, }); portOverrideApplied = true; console.error(`[tauri-mcp] Vite detected. Using dynamic port ${this.vitePort}`); } else if (bundlerType === 'webpack') { // Webpack - try port override with warning this.vitePort = await this.findAvailablePort(); const modifiedCommand = this.injectPortToCommand(tauriConfig.beforeDevCommand, this.vitePort); configOverride = JSON.stringify({ build: { beforeDevCommand: modifiedCommand, devUrl: `http://localhost:${this.vitePort}`, }, }); portOverrideApplied = true; warnings.push(`Webpack detected. Port override may not work correctly. Using port ${this.vitePort}`); console.error(`[tauri-mcp] Webpack detected. Attempting dynamic port ${this.vitePort} (may not work)`); } else { // Unknown bundler - use default configuration this.vitePort = this.detectExistingPort() ?? 1420; warnings.push( `Unknown bundler in beforeDevCommand: "${tauriConfig.beforeDevCommand}". ` + `Dynamic port override not applied. Using default port ${this.vitePort}. ` + `If running multiple apps, port conflicts may occur.` ); console.error(`[tauri-mcp] Unknown bundler. Using default port ${this.vitePort}`); } } else { // No beforeDevCommand - use default configuration this.vitePort = this.detectExistingPort() ?? 1420; warnings.push('No beforeDevCommand found in tauri.conf.json. Using default port configuration.'); console.error(`[tauri-mcp] No beforeDevCommand. Using default port ${this.vitePort}`); } console.error(`[tauri-mcp] Launching app with Vite port ${this.vitePort}...`); // Build tauri dev command with optional features const tauriArgs = ['tauri', 'dev']; if (features.length > 0) { tauriArgs.push('--features', features.join(',')); } // Add config override for dynamic port if (configOverride) { tauriArgs.push('--config', configOverride); } console.error(`[tauri-mcp] Command: pnpm ${tauriArgs.join(' ')}`); console.error(`[tauri-mcp] Socket will be at: ${this.getSocketPath()}`); this.process = spawn('pnpm', tauriArgs, { cwd: this.appConfig.appDir, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, // Use absolute appDir as project root for Rust plugin - this is where socket will be created TAURI_MCP_PROJECT_ROOT: path.resolve(this.appConfig.appDir), TAURI_MCP_DEVTOOLS: devtools ? '1' : '', // Keep VITE_PORT for backwards compatibility VITE_PORT: this.vitePort.toString(), }, detached: false, shell: process.platform === 'win32', }); this.status = 'starting'; // Reset output buffer for this launch this.outputBuffer = []; this.process.stdout?.on('data', (data) => { const line = data.toString().trim(); console.error(`[tauri stdout] ${line}`); this.outputBuffer.push(`[stdout] ${line}`); // Keep only last 100 lines if (this.outputBuffer.length > 100) this.outputBuffer.shift(); // Parse Rust rebuild trigger this.parseRustRebuildTrigger(line); }); this.process.stderr?.on('data', (data) => { const line = data.toString().trim(); console.error(`[tauri stderr] ${line}`); this.outputBuffer.push(`[stderr] ${line}`); if (this.outputBuffer.length > 100) this.outputBuffer.shift(); // Parse Rust rebuild trigger this.parseRustRebuildTrigger(line); }); this.process.on('exit', (code) => { console.error(`[tauri-mcp] Process exited with code ${code}`); this.outputBuffer.push(`[exit] Process exited with code ${code}`); this.process = null; this.status = 'not_running'; }); if (waitForReady) { try { await this.waitForReady(timeoutSecs); const errors = this.parseBackendLogs(this.outputBuffer); const hasErrors = errors.some(e => e.level === 'error'); this.status = 'running'; return { status: hasErrors ? 'build_error' : 'launched', message: hasErrors ? 'App started with build errors' : 'App is ready', port: this.vitePort, portOverrideApplied, warnings: warnings.length > 0 ? warnings : undefined, buildHealth: { frontend: 'unknown', // Will be determined by frontend logs backend: hasErrors ? 'error' : 'healthy', }, errors: hasErrors ? errors.filter(e => e.level === 'error') : undefined, }; } catch (e) { // Timeout or crash - still return useful info const errors = this.parseBackendLogs(this.outputBuffer); return { status: 'build_error', message: e instanceof Error ? e.message : 'Build failed', port: this.vitePort, portOverrideApplied, warnings: warnings.length > 0 ? warnings : undefined, buildHealth: { frontend: 'unknown', backend: 'error', }, errors: errors.filter(e => e.level === 'error'), }; } } this.status = 'running'; return { status: 'launched', message: 'App launched (not waiting for ready)', port: this.vitePort, portOverrideApplied, warnings: warnings.length > 0 ? warnings : undefined, buildHealth: { frontend: 'unknown', backend: 'unknown', }, }; }
- packages/tauri-mcp/src/server.ts:14-23 (registration)Tool registration - launch_app is listed in DEFAULT_ESSENTIAL_TOOLS array, which determines which tools are exposed to the MCP clientconst DEFAULT_ESSENTIAL_TOOLS = [ 'app_status', 'launch_app', 'stop_app', 'snapshot', 'click', 'fill', 'screenshot', 'navigate', ];
- packages/tauri-mcp/src/server.ts:68-109 (registration)List tools handler - converts toolSchemas (including launch_app) to MCP Tool format and registers them with the server, applying optional filtering based on ESSENTIAL_TOOLS environment variablethis.server.setRequestHandler(ListToolsRequestSchema, async () => { const allSchemas = Object.values(toolSchemas); // Filter tools if ESSENTIAL_TOOLS is set const filteredSchemas = essentialTools ? allSchemas.filter((schema) => essentialTools.has(schema.name)) : allSchemas; const tools: Tool[] = filteredSchemas.map((schema) => { const properties: Record<string, object> = {}; const required: string[] = []; const shape = schema.inputSchema.shape as Record<string, unknown>; for (const [key, zodValue] of Object.entries(shape)) { const zodSchema = zodValue as { _def?: { typeName?: string; description?: string }; description?: string; isOptional?: () => boolean }; properties[key] = { type: this.getZodType(zodSchema), description: zodSchema._def?.description || zodSchema.description || '', }; // Check if required (not optional) if (!zodSchema.isOptional?.()) { const typeName = zodSchema._def?.typeName; if (typeName !== 'ZodOptional') { required.push(key); } } } return { name: schema.name, description: schema.description, inputSchema: { type: 'object' as const, properties, required: required.length > 0 ? required : undefined, }, }; }); return { tools }; });