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