Skip to main content
Glama

xcode_build_and_run

Build and run Xcode projects with specified schemes and command-line arguments to automate iOS/macOS development workflows.

Instructions

Build and run a specific project with the specified scheme. ⏱️ Can run indefinitely - do not timeout.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
xcodeprojYesAbsolute path to the .xcodeproj file (or .xcworkspace if available) - e.g., /path/to/project.xcodeproj
schemeYesName of the scheme to run
command_line_argumentsNoAdditional command line arguments

Implementation Reference

  • Core handler implementation for xcode_build_and_run tool. Sets scheme, executes workspace.run() via JXA, monitors completion by polling action status with AppleScript, parses build log with XCLogParser for errors/warnings, handles alerts.
    public static async run( projectPath: string, schemeName: string, commandLineArguments: string[] = [], openProject: OpenProjectCallback ): Promise<McpResult> { const validationError = PathValidator.validateProjectPath(projectPath); if (validationError) return validationError; await openProject(projectPath); // Set the scheme const normalizedSchemeName = ParameterNormalizer.normalizeSchemeName(schemeName); const setSchemeScript = ` (function() { ${getWorkspaceByPathScript(projectPath)} const schemes = workspace.schemes(); const schemeNames = schemes.map(scheme => scheme.name()); // Try exact match first let targetScheme = schemes.find(scheme => scheme.name() === ${JSON.stringify(normalizedSchemeName)}); // If not found, try original name if (!targetScheme) { targetScheme = schemes.find(scheme => scheme.name() === ${JSON.stringify(schemeName)}); } if (!targetScheme) { throw new Error('Scheme not found. Available: ' + JSON.stringify(schemeNames)); } workspace.activeScheme = targetScheme; return 'Scheme set to ' + targetScheme.name(); })() `; try { await JXAExecutor.execute(setSchemeScript); } catch (error) { const enhancedError = ErrorHelper.parseCommonErrors(error as Error); if (enhancedError) { return { content: [{ type: 'text', text: enhancedError }] }; } const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('not found')) { try { // Get available schemes const availableSchemes = await this._getAvailableSchemes(projectPath); // Try to find a close match with fuzzy matching const bestMatch = ParameterNormalizer.findBestMatch(schemeName, availableSchemes); let message = `❌ Scheme '${schemeName}' not found\n\nAvailable schemes:\n`; availableSchemes.forEach(scheme => { if (scheme === bestMatch) { message += ` • ${scheme} ← Did you mean this?\n`; } else { message += ` • ${scheme}\n`; } }); return { content: [{ type: 'text', text: message }] }; } catch { return { content: [{ type: 'text', text: ErrorHelper.createErrorWithGuidance(`Scheme '${schemeName}' not found`, ErrorHelper.getSchemeNotFoundGuidance(schemeName)) }] }; } } return { content: [{ type: 'text', text: `Failed to set scheme '${schemeName}': ${errorMessage}` }] }; } // Note: No longer need to track initial log since we use AppleScript completion detection const hasArgs = commandLineArguments && commandLineArguments.length > 0; const script = ` (function() { ${getWorkspaceByPathScript(projectPath)} ${hasArgs ? `const result = workspace.run({withCommandLineArguments: ${JSON.stringify(commandLineArguments)}});` : `const result = workspace.run();` } return \`Run started. Result ID: \${result.id()}\`; })() `; const runResult = await JXAExecutor.execute(script); // Extract the action ID from the result const actionIdMatch = runResult.match(/Result ID: (.+)/); const actionId = actionIdMatch ? actionIdMatch[1] : null; if (!actionId) { return { content: [{ type: 'text', text: `${runResult}\n\nError: Could not extract action ID from run result` }] }; } Logger.info(`Run started with action ID: ${actionId}`); // Check for and handle "replace existing build" alert await this._handleReplaceExistingBuildAlert(); // Monitor run completion using AppleScript instead of build log detection Logger.info(`Monitoring run completion using AppleScript for action ID: ${actionId}`); const maxRunTime = 3600000; // 1 hour safety timeout const runStartTime = Date.now(); let runCompleted = false; let monitoringSeconds = 0; while (!runCompleted && (Date.now() - runStartTime) < maxRunTime) { try { // Check run completion via AppleScript every 10 seconds const checkScript = ` (function() { ${getWorkspaceByPathScript(projectPath)} if (!workspace) return 'No workspace'; const actions = workspace.schemeActionResults(); for (let i = 0; i < actions.length; i++) { const action = actions[i]; if (action.id() === "${actionId}") { const status = action.status(); const completed = action.completed(); return status + ':' + completed; } } return 'Action not found'; })() `; const result = await JXAExecutor.execute(checkScript, 15000); const [status, completed] = result.split(':'); // Log progress every 2 minutes if (monitoringSeconds % 120 === 0) { Logger.info(`Run monitoring: ${Math.floor(monitoringSeconds/60)}min - Action ${actionId}: status=${status}, completed=${completed}`); } // For run actions, we need different completion logic than build/test // Run actions stay "running" even after successful app launch if (completed === 'true' && (status === 'failed' || status === 'cancelled' || status === 'error occurred')) { // Run failed/cancelled - this is a true completion runCompleted = true; Logger.info(`Run completed after ${Math.floor(monitoringSeconds/60)} minutes: status=${status}`); break; } else if (status === 'running' && monitoringSeconds >= 60) { // If still running after 60 seconds, assume the app launched successfully // We'll check for build errors in the log parsing step runCompleted = true; Logger.info(`Run appears successful after ${Math.floor(monitoringSeconds/60)} minutes (app likely launched)`); break; } else if (status === 'succeeded') { // This might happen briefly during transition, wait a bit more Logger.info(`Run status shows 'succeeded', waiting to see if it transitions to 'running'...`); } } catch (error) { Logger.warn(`Run monitoring error at ${Math.floor(monitoringSeconds/60)}min: ${error instanceof Error ? error.message : error}`); } // Wait 10 seconds before next check await new Promise(resolve => setTimeout(resolve, 10000)); monitoringSeconds += 10; } if (!runCompleted) { Logger.warn('Run monitoring reached 1-hour timeout - proceeding anyway'); } // Now find the build log that was created during this run const newLog = await BuildLogParser.getLatestBuildLog(projectPath); if (!newLog) { return { content: [{ type: 'text', text: `${runResult}\n\nNote: Run completed but no build log found (app may have launched without building)` }] }; } Logger.info(`Run completed, parsing build log: ${newLog.path}`); const results = await BuildLogParser.parseBuildLog(newLog.path); let message = `${runResult}\n\n`; Logger.info(`Run build completed - ${results.errors.length} errors, ${results.warnings.length} warnings, status: ${results.buildStatus || 'unknown'}`); // Handle stopped/interrupted builds if (results.buildStatus === 'stopped') { message += `⏹️ BUILD INTERRUPTED\n\nThe build was stopped or interrupted before completion.\n\n💡 This may happen when:\n • The build was cancelled manually\n • Xcode was closed during the build\n • System resources were exhausted\n\nTry running the build again to complete it.`; return { content: [{ type: 'text', text: message }] }; } if (results.errors.length > 0) { message += `❌ BUILD FAILED (${results.errors.length} errors)\n\nERRORS:\n`; results.errors.forEach(error => { message += ` • ${error}\n`; }); throw new McpError( ErrorCode.InternalError, message ); } else if (results.warnings.length > 0) { message += `⚠️ BUILD COMPLETED WITH WARNINGS (${results.warnings.length} warnings)\n\nWARNINGS:\n`; results.warnings.forEach(warning => { message += ` • ${warning}\n`; }); } else { message += '✅ BUILD SUCCESSFUL - App should be launching'; } return { content: [{ type: 'text', text: message }] }; }
  • MCP server dispatch handler that validates parameters and delegates to BuildTools.run()
    case 'xcode_build_and_run': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } if (!args.scheme) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: scheme`); } return await BuildTools.run( args.xcodeproj as string, args.scheme as string, (args.command_line_arguments as string[]) || [], this.openProject.bind(this) );
  • Input schema definition, description, and registration in the shared tool definitions used by both MCP server and CLI.
    { name: 'xcode_build_and_run', description: 'Build and run a specific project with the specified scheme. ⏱️ Can run indefinitely - do not timeout.', inputSchema: { type: 'object', properties: { xcodeproj: { type: 'string', description: preferredXcodeproj ? `Absolute path to the .xcodeproj file (or .xcworkspace if available) - defaults to ${preferredXcodeproj}` : 'Absolute path to the .xcodeproj file (or .xcworkspace if available) - e.g., /path/to/project.xcodeproj', }, scheme: { type: 'string', description: preferredScheme ? `Name of the scheme to run - defaults to ${preferredScheme}` : 'Name of the scheme to run', }, command_line_arguments: { type: 'array', items: { type: 'string' }, description: 'Additional command line arguments', }, }, required: [ ...(!preferredXcodeproj ? ['xcodeproj'] : []), ...(!preferredScheme ? ['scheme'] : []) ], },
  • Tool listed in buildTools array for environment validation and limitations checking.
    const buildTools = ['xcode_build', 'xcode_test', 'xcode_build_and_run', 'xcode_debug', 'xcode_clean']; const xcodeTools = [...buildTools, 'xcode_open_project', 'xcode_get_schemes', 'xcode_set_active_scheme',
  • Helper method called during run to automatically handle Xcode 'replace existing build' alerts using System Events and Xcode scripting.
    private static async _handleReplaceExistingBuildAlert(): Promise<void> { const alertScript = ` (function() { try { // Use System Events approach first as it's more reliable for sheet dialogs const systemEvents = Application('System Events'); const xcodeProcesses = systemEvents.processes.whose({name: 'Xcode'}); if (xcodeProcesses.length > 0) { const xcodeProcess = xcodeProcesses[0]; const windows = xcodeProcess.windows(); // Check for sheets in regular windows (most common case) for (let i = 0; i < windows.length; i++) { try { const window = windows[i]; const sheets = window.sheets(); if (sheets && sheets.length > 0) { const sheet = sheets[0]; const buttons = sheet.buttons(); // Look for Replace, Continue, OK, Yes buttons (in order of preference) const preferredButtons = ['Replace', 'Continue', 'OK', 'Yes', 'Stop and Replace']; for (const preferredButton of preferredButtons) { for (let b = 0; b < buttons.length; b++) { try { const button = buttons[b]; const buttonTitle = button.title(); if (buttonTitle === preferredButton) { button.click(); return 'Sheet alert handled: clicked ' + buttonTitle; } } catch (e) { // Continue to next button } } } // If no preferred button found, try partial matches for (let b = 0; b < buttons.length; b++) { try { const button = buttons[b]; const buttonTitle = button.title(); if (buttonTitle && ( buttonTitle.toLowerCase().includes('replace') || buttonTitle.toLowerCase().includes('continue') || buttonTitle.toLowerCase().includes('stop') || buttonTitle.toLowerCase() === 'ok' || buttonTitle.toLowerCase() === 'yes' )) { button.click(); return 'Sheet alert handled: clicked ' + buttonTitle + ' (partial match)'; } } catch (e) { // Continue to next button } } // Log available buttons for debugging const availableButtons = []; for (let b = 0; b < buttons.length; b++) { try { availableButtons.push(buttons[b].title()); } catch (e) { availableButtons.push('(unnamed)'); } } return 'Sheet found but no suitable button. Available: ' + JSON.stringify(availableButtons); } } catch (e) { // Continue to next window } } // Check for modal dialogs const dialogs = xcodeProcess.windows.whose({subrole: 'AXDialog'}); for (let d = 0; d < dialogs.length; d++) { try { const dialog = dialogs[d]; const buttons = dialog.buttons(); for (let b = 0; b < buttons.length; b++) { try { const button = buttons[b]; const buttonTitle = button.title(); if (buttonTitle && ( buttonTitle.toLowerCase().includes('replace') || buttonTitle.toLowerCase().includes('continue') || buttonTitle.toLowerCase().includes('stop') || buttonTitle.toLowerCase() === 'ok' || buttonTitle.toLowerCase() === 'yes' )) { button.click(); return 'Dialog alert handled: clicked ' + buttonTitle; } } catch (e) { // Continue to next button } } } catch (e) { // Continue to next dialog } } } // Fallback to Xcode app approach for embedded alerts const app = Application('Xcode'); const windows = app.windows(); for (let i = 0; i < windows.length; i++) { try { const window = windows[i]; const sheets = window.sheets(); if (sheets && sheets.length > 0) { const sheet = sheets[0]; const buttons = sheet.buttons(); for (let j = 0; j < buttons.length; j++) { try { const button = buttons[j]; const buttonName = button.name(); if (buttonName && ( buttonName.toLowerCase().includes('replace') || buttonName.toLowerCase().includes('continue') || buttonName.toLowerCase().includes('stop') || buttonName.toLowerCase() === 'ok' || buttonName.toLowerCase() === 'yes' )) { button.click(); return 'Xcode app sheet handled: clicked ' + buttonName; } } catch (e) { // Continue to next button } } } } catch (e) { // Continue to next window } } return 'No alert found'; } catch (error) { return 'Alert check failed: ' + error.message; } })() `; try { Logger.info('Running alert detection script...'); const result = await JXAExecutor.execute(alertScript); Logger.info(`Alert detection result: ${result}`); if (result && result !== 'No alert found') { Logger.info(`Alert handling: ${result}`); } else { Logger.info('No alerts detected'); } } catch (error) { // Don't fail the main operation if alert handling fails Logger.info(`Alert handling failed: ${error instanceof Error ? error.message : String(error)}`); } }

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/lapfelix/XcodeMCP'

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