Skip to main content
Glama

xcode_build

Build Xcode projects or workspaces with specified schemes using the active or provided destination. Automates compilation processes directly within Xcode for development workflows.

Instructions

Build a specific Xcode project or workspace with the specified scheme. If destination is not provided, uses the currently active destination. ⏱️ Can take minutes to hours - do not timeout.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
xcodeprojYesAbsolute path to the .xcodeproj file to build (or .xcworkspace if available) - e.g., /path/to/project.xcodeproj
schemeYesName of the scheme to build
destinationNoBuild destination (optional - uses active destination if not provided)

Implementation Reference

  • Primary handler function for xcode_build tool execution. Handles project validation, Xcode automation via JXA to set scheme/destination and start build, build log monitoring/parsing, error/warning extraction, and result formatting.
    export class BuildTools {
      public static async build(
        projectPath: string, 
        schemeName: string, 
        destination: string | null = null, 
        openProject: OpenProjectCallback
      ): Promise<McpResult> {
        const validationError = PathValidator.validateProjectPath(projectPath);
        if (validationError) return validationError;
    
        await openProject(projectPath);
    
        // Normalize the scheme name for better matching
        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}` }] };
          }
    
        if (destination) {
          // Normalize the destination name for better matching
          const normalizedDestination = ParameterNormalizer.normalizeDestinationName(destination);
          
          const setDestinationScript = `
            (function() {
              ${getWorkspaceByPathScript(projectPath)}
              
              const destinations = workspace.runDestinations();
              const destinationNames = destinations.map(dest => dest.name());
              
              // Try exact match first
              let targetDestination = destinations.find(dest => dest.name() === ${JSON.stringify(normalizedDestination)});
              
              // If not found, try original name
              if (!targetDestination) {
                targetDestination = destinations.find(dest => dest.name() === ${JSON.stringify(destination)});
              }
              
              if (!targetDestination) {
                throw new Error('Destination not found. Available: ' + JSON.stringify(destinationNames));
              }
              
              workspace.activeRunDestination = targetDestination;
              return 'Destination set to ' + targetDestination.name();
            })()
          `;
          
          try {
            await JXAExecutor.execute(setDestinationScript);
          } 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 {
                // Extract available destinations from error message if present
                let availableDestinations: string[] = [];
                if (errorMessage.includes('Available:')) {
                  const availablePart = errorMessage.split('Available: ')[1];
                  // Find the JSON array part
                  const jsonMatch = availablePart?.match(/\[.*?\]/);
                  if (jsonMatch) {
                    try {
                      availableDestinations = JSON.parse(jsonMatch[0]);
                    } catch {
                      availableDestinations = await this._getAvailableDestinations(projectPath);
                    }
                  }
                } else {
                  availableDestinations = await this._getAvailableDestinations(projectPath);
                }
                  
                // Try to find a close match with fuzzy matching
                const bestMatch = ParameterNormalizer.findBestMatch(destination, availableDestinations);
                let guidance = ErrorHelper.getDestinationNotFoundGuidance(destination, availableDestinations);
                
                if (bestMatch && bestMatch !== destination) {
                  guidance += `\n• Did you mean '${bestMatch}'?`;
                }
                
                return { content: [{ type: 'text', text: ErrorHelper.createErrorWithGuidance(`Destination '${destination}' not found`, guidance) }] };
              } catch {
                return { content: [{ type: 'text', text: ErrorHelper.createErrorWithGuidance(`Destination '${destination}' not found`, ErrorHelper.getDestinationNotFoundGuidance(destination)) }] };
              }
            }
            
            return { content: [{ type: 'text', text: `Failed to set destination '${destination}': ${errorMessage}` }] };
          }
        }
    
        const buildScript = `
          (function() {
            ${getWorkspaceByPathScript(projectPath)}
            
            workspace.build();
            
            return 'Build started';
          })()
        `;
        
        const buildStartTime = Date.now();
        
        try {
          await JXAExecutor.execute(buildScript);
          
          // Check for and handle "replace existing build" alert
          await this._handleReplaceExistingBuildAlert();
        } 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);
          return { content: [{ type: 'text', text: `Failed to start build: ${errorMessage}` }] };
        }
    
        Logger.info('Waiting for new build log to appear after build start...');
        
        let attempts = 0;
        let newLog = null;
        const initialWaitAttempts = 3600; // 1 hour max to wait for build log
    
        while (attempts < initialWaitAttempts) {
          const currentLog = await BuildLogParser.getLatestBuildLog(projectPath);
          
          if (currentLog) {
            const logTime = currentLog.mtime.getTime();
            const buildTime = buildStartTime;
            Logger.debug(`Checking log: ${currentLog.path}, log time: ${logTime}, build time: ${buildTime}, diff: ${logTime - buildTime}ms`);
            
            if (logTime > buildTime) {
              newLog = currentLog;
              Logger.info(`Found new build log created after build start: ${newLog.path}`);
              break;
            }
          } else {
            Logger.debug(`No build log found yet, attempt ${attempts + 1}/${initialWaitAttempts}`);
          }
          
          await new Promise(resolve => setTimeout(resolve, 1000));
          attempts++;
        }
    
        if (!newLog) {
          return { content: [{ type: 'text', text: ErrorHelper.createErrorWithGuidance(`Build started but no new build log appeared within ${initialWaitAttempts} seconds`, ErrorHelper.getBuildLogNotFoundGuidance()) }] };
        }
    
        Logger.info(`Monitoring build completion for log: ${newLog.path}`);
        
        attempts = 0;
        const maxAttempts = 3600; // 1 hour max for build completion
        let lastLogSize = 0;
        let stableCount = 0;
    
        while (attempts < maxAttempts) {
          try {
            const logStats = await stat(newLog.path);
            const currentLogSize = logStats.size;
            
            if (currentLogSize === lastLogSize) {
              stableCount++;
              if (stableCount >= 1) {
                Logger.debug(`Log stable for ${stableCount}s, trying to parse...`);
                const results = await BuildLogParser.parseBuildLog(newLog.path);
                Logger.debug(`Parse result has ${results.errors.length} errors, ${results.warnings.length} warnings`);
                const isParseFailure = results.errors.some(error => 
                  typeof error === 'string' && error.includes('XCLogParser failed to parse the build log.')
                );
                if (results && !isParseFailure) {
                  Logger.info(`Build completed, log parsed successfully: ${newLog.path}`);
                  break;
                }
              }
            } else {
              lastLogSize = currentLogSize;
              stableCount = 0;
            }
          } catch (error) {
            const currentLog = await BuildLogParser.getLatestBuildLog(projectPath);
            if (currentLog && currentLog.path !== newLog.path && currentLog.mtime.getTime() > buildStartTime) {
              Logger.debug(`Build log changed to: ${currentLog.path}`);
              newLog = currentLog;
              lastLogSize = 0;
              stableCount = 0;
            }
          }
          
          await new Promise(resolve => setTimeout(resolve, 1000));
          attempts++;
        }
    
        if (attempts >= maxAttempts) {
          return { content: [{ type: 'text', text: `Build timed out after ${maxAttempts} seconds` }] };
        }
        
        const results = await BuildLogParser.parseBuildLog(newLog.path);
        
        let message = '';
        const schemeInfo = schemeName ? ` for scheme '${schemeName}'` : '';
        const destInfo = destination ? ` and destination '${destination}'` : '';
        
        Logger.info(`Build completed${schemeInfo}${destInfo} - ${results.errors.length} errors, ${results.warnings.length} warnings, status: ${results.buildStatus || 'unknown'}`);
        
        // Handle stopped/interrupted builds
        if (results.buildStatus === 'stopped') {
          message = `⏹️ BUILD INTERRUPTED${schemeInfo}${destInfo}\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${schemeInfo}${destInfo} (${results.errors.length} errors)\n\nERRORS:\n`;
          results.errors.forEach(error => {
            message += `  • ${error}\n`;
            Logger.error('Build error:', error);
          });
          throw new McpError(
            ErrorCode.InternalError,
            message
          );
        } else if (results.warnings.length > 0) {
          message = `⚠️ BUILD COMPLETED WITH WARNINGS${schemeInfo}${destInfo} (${results.warnings.length} warnings)\n\nWARNINGS:\n`;
          results.warnings.forEach(warning => {
            message += `  • ${warning}\n`;
            Logger.warn('Build warning:', warning);
          });
        } else {
          message = `✅ BUILD SUCCESSFUL${schemeInfo}${destInfo}`;
        }
    
        return { content: [{ type: 'text', text: message }] };
      }
  • Dispatch handler in XcodeServer's CallToolRequestSchema that validates parameters and delegates to BuildTools.build
    case 'xcode_build':
      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.build(
        args.xcodeproj as string, 
        args.scheme as string, 
        (args.destination as string) || null, 
        this.openProject.bind(this)
      );
  • Tool definition including name, description, and input schema/JSON Schema validation for xcode_build
    {
      name: 'xcode_build',
      description: 'Build a specific Xcode project or workspace with the specified scheme. If destination is not provided, uses the currently active destination. ⏱️ Can take minutes to hours - do not timeout.',
      inputSchema: {
        type: 'object',
        properties: {
          xcodeproj: {
            type: 'string',
            description: preferredXcodeproj 
              ? `Absolute path to the .xcodeproj file to build (or .xcworkspace if available) - defaults to ${preferredXcodeproj}`
              : 'Absolute path to the .xcodeproj file to build (or .xcworkspace if available) - e.g., /path/to/project.xcodeproj',
          },
          scheme: {
            type: 'string',
            description: preferredScheme 
              ? `Name of the scheme to build - defaults to ${preferredScheme}`
              : 'Name of the scheme to build',
          },
          destination: {
            type: 'string',
            description: 'Build destination (optional - uses active destination if not provided)',
          },
        },
        required: [
          ...(!preferredXcodeproj ? ['xcodeproj'] : []),
          ...(!preferredScheme ? ['scheme'] : [])
        ],
      },
    },
  • Registration of all tools including xcode_build via ListToolsRequestSchema handler using getToolDefinitions()
    this.server.setRequestHandler(ListToolsRequestSchema, async () => {
      const toolOptions: {
        includeClean: boolean;
        preferredScheme?: string;
        preferredXcodeproj?: string;
      } = { includeClean: this.includeClean };
      
      if (this.preferredScheme) toolOptions.preferredScheme = this.preferredScheme;
      if (this.preferredXcodeproj) toolOptions.preferredXcodeproj = this.preferredXcodeproj;
      
      const toolDefinitions = getToolDefinitions(toolOptions);
      return {
        tools: toolDefinitions.map(tool => ({
          name: tool.name,
          description: tool.description,
          inputSchema: tool.inputSchema
        })),
      };
    });
  • Tool definitions array where xcode_build is registered/defined for use in ListTools response.
    const tools: ToolDefinition[] = [
      {
        name: 'xcode_open_project',
        description: 'Open an Xcode project or workspace',
        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',
            },
          },
          required: preferredXcodeproj ? [] : ['xcodeproj'],
        },
      },
      {
        name: 'xcode_close_project',
        description: 'Close the currently active Xcode project or workspace (automatically stops any running actions first)',
        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',
            },
          },
          required: preferredXcodeproj ? [] : ['xcodeproj'],
        },
      },
      {
        name: 'xcode_build',
        description: 'Build a specific Xcode project or workspace with the specified scheme. If destination is not provided, uses the currently active destination. ⏱️ Can take minutes to hours - do not timeout.',
        inputSchema: {
          type: 'object',
          properties: {
            xcodeproj: {
              type: 'string',
              description: preferredXcodeproj 
                ? `Absolute path to the .xcodeproj file to build (or .xcworkspace if available) - defaults to ${preferredXcodeproj}`
                : 'Absolute path to the .xcodeproj file to build (or .xcworkspace if available) - e.g., /path/to/project.xcodeproj',
            },
            scheme: {
              type: 'string',
              description: preferredScheme 
                ? `Name of the scheme to build - defaults to ${preferredScheme}`
                : 'Name of the scheme to build',
            },
            destination: {
              type: 'string',
              description: 'Build destination (optional - uses active destination if not provided)',
            },
          },
          required: [
            ...(!preferredXcodeproj ? ['xcodeproj'] : []),
            ...(!preferredScheme ? ['scheme'] : [])
          ],
        },
      },

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