Skip to main content
Glama

xcode_build

Automate Xcode project or workspace builds by specifying the scheme and optional destination. Simplifies build processes and extracts errors or warnings from logs.

Instructions

Build a specific Xcode project or workspace with the specified scheme. If destination is not provided, uses the currently active destination.

Input Schema

NameRequiredDescriptionDefault
destinationNoBuild destination (optional - uses active destination if not provided)
schemeYesName of the scheme to build
xcodeprojYesPath to the .xcodeproj file to build (or .xcworkspace if available) - supports both absolute (/path/to/project.xcodeproj) and relative (MyApp.xcodeproj) paths

Input Schema (JSON Schema)

{ "properties": { "destination": { "description": "Build destination (optional - uses active destination if not provided)", "type": "string" }, "scheme": { "description": "Name of the scheme to build", "type": "string" }, "xcodeproj": { "description": "Path to the .xcodeproj file to build (or .xcworkspace if available) - supports both absolute (/path/to/project.xcodeproj) and relative (MyApp.xcodeproj) paths", "type": "string" } }, "required": [ "xcodeproj", "scheme" ], "type": "object" }

Implementation Reference

  • Main handler implementation: validates project path, opens project in Xcode, sets scheme and optional destination via JXA AppleScript, triggers workspace.build(), monitors and parses the new build log with XCLogParser, reports build status with errors and warnings.
    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 }] }; }
  • Tool registration and dispatch in the main MCP server switch statement: 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) );
  • Input schema definition for the xcode_build tool, including parameters xcodeproj, scheme, and optional destination.
    { 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'] : []) ], }, },
  • Tool list registration: returns the tool definitions including xcode_build schema for MCP ListTools request.
    const toolDefinitions = getToolDefinitions(toolOptions); return { tools: toolDefinitions.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })), };

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