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'] : [])
          ],
        },
      },
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 adds critical context: the tool can be long-running ('Can take minutes to hours'), includes a timeout warning ('do not timeout'), and specifies default behavior for missing parameters. However, it lacks details on 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 front-loaded with the core purpose, followed by important behavioral notes. Every sentence adds value: the first defines the action, the second covers parameter defaults, and the third provides critical performance context. No wasted words.

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?

For a build tool with no annotations and no output schema, the description does well by covering purpose, parameter behavior, and critical performance warnings. However, it lacks information about what the tool returns (e.g., build logs, success/failure status) or error conditions, leaving some gaps in completeness.

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%, providing full documentation for all parameters. The description adds minimal value beyond the schema, only reinforcing the optionality of 'destination' and its default behavior. This meets the baseline for high schema coverage.

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 target ('Xcode project or workspace with the specified scheme'), distinguishing it from siblings like xcode_clean (cleaning) or xcode_test (testing). It precisely defines the verb and resource without being tautological.

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 provides clear context for when to use this tool (building projects/workspaces with schemes) and mentions a default behavior for optional parameters ('uses the currently active destination'), but does not explicitly differentiate when to use alternatives like xcode_build_and_run or xcode_test, which are closely related siblings.

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