start_session
Launch a Tauri app with configurable features, devtools, and timeout. Optionally wait until the application is ready.
Instructions
Start session (launch Tauri app)
Input 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
- The handler function for start_session. It calls tauriManager.launch(args) to launch the Tauri app, then optionally sets a window title prefix from env var TAURI_MCP_WINDOW_PREFIX. Returns the launch result as JSON.
start_session: async (args: { wait_for_ready?: boolean; timeout_secs?: number; features?: string[]; devtools?: boolean }) => { const result = await tauriManager.launch(args); // Apply window title prefix after successful launch if env var is set if (result.status === 'launched') { const prefix = process.env.TAURI_MCP_WINDOW_PREFIX; if (prefix) { try { await socketManager.setTitlePrefix(prefix); } catch { // Best-effort — app may not have finished initializing windows yet } } } return { content: [ { type: 'text' as const, text: JSON.stringify(result, null, 2), }, ], }; }, - The Zod schema definition for start_session, defining input parameters: wait_for_ready (optional boolean), timeout_secs (optional number), features (optional string array), and devtools (optional boolean).
start_session: { name: 'start_session', description: 'Start session (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'), }), }, - packages/tauri-mcp/src/server.ts:14-17 (registration)Registration of start_session in the DEFAULT_ESSENTIAL_TOOLS array, which determines the set of tools available to clients unless overridden by the ESSENTIAL_TOOLS env var.
const DEFAULT_ESSENTIAL_TOOLS = [ 'get_session_status', 'start_session', 'stop_session', - The launch() method on TauriManager called by the start_session handler. It handles port detection/allocation, checks for existing running app instances (idempotent), spawns 'pnpm tauri dev', optionally waits for the app to be ready, and returns a LaunchResult.
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); } } // Fallback to TAURI_MCP_FEATURES env if no features specified if (features.length === 0 && process.env.TAURI_MCP_FEATURES) { features = process.env.TAURI_MCP_FEATURES.split(',').map(f => f.trim()).filter(Boolean); console.error(`[tauri-mcp] Using default features from TAURI_MCP_FEATURES: ${features.join(',')}`); } 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) // Auto-kill stale app instead of throwing — the socket is worktree-scoped, // so any app responding on it belongs to this same worktree's stale session const externalAppRunning = await this.checkExternalAppRunning(); if (externalAppRunning) { console.error('[tauri-mcp] Stale external app detected on socket, force-stopping...'); await this.forceStopExternalApp(); // Verify the socket is gone after force-stop const stillAlive = await this.checkExternalAppRunning(); if (stillAlive) { throw new Error( 'Failed to stop the stale Tauri app instance on the socket. ' + 'The process may require manual termination. ' + 'Try: lsof -t ' + this.getSocketPath() + ' | xargs kill -9' ); } console.error('[tauri-mcp] Stale app cleared, proceeding with launch'); } // 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' : '', // Pass window prefix for worktree identification ...(process.env.TAURI_MCP_WINDOW_PREFIX ? { TAURI_MCP_WINDOW_PREFIX: process.env.TAURI_MCP_WINDOW_PREFIX } : {}), // Keep VITE_PORT for backwards compatibility VITE_PORT: this.vitePort.toString(), }, detached: process.platform !== 'win32', shell: process.platform === 'win32', }); // 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.launchedAt = null; this.cleanupSocketFile(); }); if (waitForReady) { try { await this.waitForReady(timeoutSecs); const errors = this.parseBackendLogs(this.outputBuffer); const hasErrors = errors.some(e => e.level === 'error'); this.launchedAt = Date.now(); 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.launchedAt = Date.now(); 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', }, }; } - Error message in socket.ts referencing start_session: when the socket file doesn't exist (ENOENT), the error advises using start_session first.
client.on('error', (err) => { clearTimeout(timeout); if ((err as NodeJS.ErrnoException).code === 'ENOENT') { reject(new Error('App not running. Use start_session first.')); } else if ((err as NodeJS.ErrnoException).code === 'ECONNREFUSED') { reject(new Error('App is starting up. Please wait and try again.')); } else { reject(new Error(`Socket error: ${err.message}`)); } }); client.on('close', () => { if (settled) return; clearTimeout(timeout); reject(new Error(data ? 'Connection closed with incomplete response' : 'Connection closed without response')); }); }); }