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)}`);
      }
    }
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It effectively communicates key behavioral traits: it performs both build and run operations (implying mutation/execution), and it warns that the process 'can run indefinitely - do not timeout', which is crucial for agent planning. However, it lacks details on permissions, error handling, or output format.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise—two sentences with zero waste. The first sentence states the core purpose, and the second provides critical behavioral context (timeout warning). Every word earns its place, and it is front-loaded with essential information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (build and run operations with potential indefinite execution), no annotations, and no output schema, the description does well by covering the core action and a key behavioral warning. However, it lacks details on what the tool returns (e.g., success/failure, logs) or error conditions, leaving some gaps for an agent to operate fully.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all three parameters thoroughly. The description does not add any additional meaning or context about the parameters beyond what the schema provides, such as examples or constraints not in the schema. Baseline 3 is appropriate when the schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action ('Build and run') and target resource ('a specific project with the specified scheme'), distinguishing it from siblings like 'xcode_build' (build only) and 'xcode_test' (test only). The inclusion of the emoji and timeout note adds practical context without diluting the core purpose.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage context by specifying it builds and runs a project with a scheme, which naturally differentiates it from tools like 'xcode_clean' or 'xcode_get_schemes'. However, it does not explicitly state when to use this tool versus alternatives like 'xcode_debug' or 'xcode_test', nor does it mention prerequisites or exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

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

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