Skip to main content
Glama

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
NameRequiredDescriptionDefault
wait_for_readyNoWait for ready
timeout_secsNoTimeout seconds
featuresNoCargo features to enable
devtoolsNoOpen 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 response
    launch_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 ready
    async 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', }, }; }
  • Tool registration - launch_app is listed in DEFAULT_ESSENTIAL_TOOLS array, which determines which tools are exposed to the MCP client
    const DEFAULT_ESSENTIAL_TOOLS = [ 'app_status', 'launch_app', 'stop_app', 'snapshot', 'click', 'fill', 'screenshot', 'navigate', ];
  • 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 variable
    this.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 }; });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DaveDev42/tauri-plugin-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server