Skip to main content
Glama
XcodeServer.ts49.4 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, CallToolResult, } from '@modelcontextprotocol/sdk/types.js'; import { BuildTools } from './tools/BuildTools.js'; import { ProjectTools } from './tools/ProjectTools.js'; import { InfoTools } from './tools/InfoTools.js'; import { XCResultTools } from './tools/XCResultTools.js'; import { PathValidator } from './utils/PathValidator.js'; import { EnvironmentValidator } from './utils/EnvironmentValidator.js'; import { Logger } from './utils/Logger.js'; import type { EnvironmentValidation, ToolLimitations, McpResult } from './types/index.js'; import { getToolDefinitions } from './shared/toolDefinitions.js'; export class XcodeServer { public server: Server; public currentProjectPath: string | null = null; private environmentValidation: EnvironmentValidation | null = null; private isValidated = false; private canOperateInDegradedMode = false; private includeClean: boolean; private preferredScheme: string | undefined; private preferredXcodeproj: string | undefined; constructor(options: { includeClean?: boolean; preferredScheme?: string; preferredXcodeproj?: string; } = {}) { this.includeClean = options.includeClean ?? true; this.preferredScheme = options.preferredScheme; this.preferredXcodeproj = options.preferredXcodeproj; // Log preferred values if set if (this.preferredScheme) { Logger.info(`Using preferred scheme: ${this.preferredScheme}`); } if (this.preferredXcodeproj) { Logger.info(`Using preferred xcodeproj: ${this.preferredXcodeproj}`); } this.server = new Server( { name: 'xcode-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); } /** * Validates the environment and sets up the server accordingly */ public async validateEnvironment(): Promise<EnvironmentValidation> { if (this.isValidated && this.environmentValidation) { return this.environmentValidation; } try { this.environmentValidation = await EnvironmentValidator.validateEnvironment(); this.isValidated = true; this.canOperateInDegradedMode = this.environmentValidation.overall.canOperateInDegradedMode; // Log validation results const validationStatus = this.environmentValidation.overall.valid ? 'PASSED' : this.canOperateInDegradedMode ? 'DEGRADED' : 'FAILED'; Logger.info('Environment Validation:', validationStatus); if (!this.environmentValidation.overall.valid) { Logger.warn('Environment issues detected:'); [...this.environmentValidation.overall.criticalFailures, ...this.environmentValidation.overall.nonCriticalFailures].forEach(component => { const result = this.environmentValidation![component]; if (result && 'valid' in result) { const validationResult = result as import('./types/index.js').EnvironmentValidationResult; Logger.warn(` ${component}: ${validationResult.message || 'Status unknown'}`); } }); } return this.environmentValidation; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); Logger.error('Environment validation failed:', errorMessage); // Create minimal validation result for graceful degradation this.environmentValidation = { overall: { valid: false, canOperateInDegradedMode: false, criticalFailures: ['validation'], nonCriticalFailures: [] } }; this.isValidated = true; return this.environmentValidation; } } /** * Checks if a tool operation should be blocked due to environment issues */ public async validateToolOperation(toolName: string): Promise<McpResult | null> { // Health check tool should never be blocked if (toolName === 'xcode_health_check') { return null; } const validation = await this.validateEnvironment(); if (validation.overall.valid) { return null; // All good } // Check for critical failures that prevent all operations if (!validation.overall.canOperateInDegradedMode) { const criticalFailures = validation.overall.criticalFailures .map(component => { const result = validation[component]; if (result && 'valid' in result) { const validationResult = result as import('./types/index.js').EnvironmentValidationResult; return validationResult.message || 'Unknown failure'; } return 'Unknown failure'; }) .filter(Boolean) .join(', '); return { content: [{ type: 'text', text: `❌ Cannot execute ${toolName}: Critical environment failures detected.\n\n${criticalFailures}\n\nPlease run the 'xcode_health_check' tool for detailed recovery instructions.` }] }; } // Check for specific tool limitations in degraded mode const limitations = this.getToolLimitations(toolName, validation); if (limitations.blocked) { return { content: [{ type: 'text', text: `❌ Cannot execute ${toolName}: ${limitations.reason}\n\nRecovery instructions:\n${limitations.instructions?.map(i => `• ${i}`).join('\n') || ''}` }] }; } // Issue warning for degraded functionality but allow operation if (limitations.degraded) { Logger.warn(`${toolName} operating in degraded mode - ${limitations.reason}`); } return null; // Operation can proceed } /** * Determines tool limitations based on environment validation */ private getToolLimitations(toolName: string, validation: EnvironmentValidation): ToolLimitations { // Health check tool should never be limited if (toolName === 'xcode_health_check') { return { blocked: false, degraded: false }; } 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', 'xcode_get_run_destinations', 'xcode_get_workspace_info', 'xcode_get_projects']; const xcresultTools = ['xcresult_browse', 'xcresult_browser_get_console', 'xcresult_summary', 'xcresult_get_screenshot', 'xcresult_get_ui_hierarchy', 'xcresult_get_ui_element', 'xcresult_list_attachments', 'xcresult_export_attachment']; // Check Xcode availability if (xcodeTools.includes(toolName) && !validation.xcode?.valid) { return { blocked: true, degraded: false, reason: 'Xcode is not properly installed or accessible', instructions: validation.xcode?.recoveryInstructions || [ 'Install Xcode from the Mac App Store', 'Launch Xcode once to complete installation' ] }; } // Check osascript availability if (xcodeTools.includes(toolName) && !validation.osascript?.valid) { return { blocked: true, degraded: false, reason: 'JavaScript for Automation (JXA) is not available', instructions: validation.osascript?.recoveryInstructions || [ 'This tool requires macOS', 'Ensure osascript is available' ] }; } // Build tools have additional dependencies and warnings if (buildTools.includes(toolName)) { if (!validation.xclogparser?.valid) { return { blocked: false, degraded: true, reason: 'XCLogParser not available - build results will have limited detail', instructions: validation.xclogparser?.recoveryInstructions || [ 'Install XCLogParser with: brew install xclogparser' ] }; } if (!validation.permissions?.valid && !validation.permissions?.degradedMode?.available) { return { blocked: true, degraded: false, reason: 'Automation permissions not granted', instructions: validation.permissions?.recoveryInstructions || [ 'Grant automation permissions in System Preferences' ] }; } } // XCResult tools only need xcresulttool (part of Xcode Command Line Tools) if (xcresultTools.includes(toolName)) { // Check if we can run xcresulttool - this is included with Xcode Command Line Tools if (!validation.xcode?.valid) { return { blocked: true, degraded: false, reason: 'XCResult tools require Xcode Command Line Tools for xcresulttool', instructions: [ 'Install Xcode Command Line Tools: xcode-select --install', 'Or install full Xcode from the Mac App Store' ] }; } } return { blocked: false, degraded: false }; } /** * Enhances error messages with configuration guidance */ public async enhanceErrorWithGuidance(error: Error | { message?: string }, _toolName: string): Promise<string | null> { const errorMessage = error.message || error.toString(); // Import ErrorHelper for common error patterns const { ErrorHelper } = await import('./utils/ErrorHelper.js'); const commonError = ErrorHelper.parseCommonErrors(error as Error); if (commonError) { return commonError; } // Additional configuration-specific error patterns if (errorMessage.includes('command not found')) { if (errorMessage.includes('xclogparser')) { return `❌ XCLogParser not found\n\n💡 To fix this:\n• Install XCLogParser: brew install xclogparser\n• Or download from: https://github.com/MobileNativeFoundation/XCLogParser\n\nNote: Build operations will work but with limited error details.`; } if (errorMessage.includes('osascript')) { return `❌ macOS scripting tools not available\n\n💡 This indicates a critical system issue:\n• This MCP server requires macOS\n• Ensure you're running on a Mac with system tools available\n• Try restarting your terminal`; } } if (errorMessage.includes('No such file or directory')) { if (errorMessage.includes('Xcode.app')) { return `❌ Xcode application not found\n\n💡 To fix this:\n• Install Xcode from the Mac App Store\n• Ensure Xcode is in /Applications/Xcode.app\n• Launch Xcode once to complete installation`; } } // Only convert actual operation timeouts, not build errors containing 'timeout:' or transport errors if ((errorMessage.includes(' timeout') || errorMessage.includes('timed out') || errorMessage.includes('timeout after')) && !errorMessage.includes('Body Timeout Error') && !errorMessage.includes('Transport error') && !errorMessage.includes('SSE error') && !errorMessage.includes('terminated') && !errorMessage.includes("'timeout:'") && !errorMessage.includes("timeout:' in call") && !errorMessage.includes('argument label') && !errorMessage.includes('TEST BUILD FAILED')) { return `❌ Operation timed out\n\n💡 This might indicate:\n• Xcode is not responding (try restarting Xcode)\n• System performance issues\n• Large project taking longer than expected\n• Network issues if downloading dependencies`; } return null; // No specific guidance available } private setupToolHandlers(): void { 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 })), }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request: any): Promise<CallToolResult> => { const { name, arguments: args = {} } = request.params as { name: string; arguments?: Record<string, unknown> }; // Apply preferred values if parameters not provided if (!args.xcodeproj && this.preferredXcodeproj) { args.xcodeproj = this.preferredXcodeproj; } if (!args.scheme && this.preferredScheme) { args.scheme = this.preferredScheme; } // Resolve relative paths to absolute paths if (args.xcodeproj && typeof args.xcodeproj === 'string') { const { resolvedPath, error } = PathValidator.resolveAndValidateProjectPath(args.xcodeproj as string, 'xcodeproj'); if (error) { return error; } args.xcodeproj = resolvedPath; } if (args.filePath && typeof args.filePath === 'string') { const path = await import('path'); if (!path.default.isAbsolute(args.filePath)) { args.filePath = path.default.resolve(process.cwd(), args.filePath); } } try { // Handle health check tool first (no environment validation needed) if (name === 'xcode_health_check') { const report = await EnvironmentValidator.createHealthCheckReport(); return { content: [{ type: 'text', text: report }] }; } // Validate environment for all other tools const validationError = await this.validateToolOperation(name); if (validationError) { return validationError; } switch (name) { case 'xcode_open_project': if (!args.xcodeproj) { throw new McpError( ErrorCode.InvalidParams, this.preferredXcodeproj ? `Missing required parameter: xcodeproj (no preferred value was applied)\n\n💡 Expected: absolute path to .xcodeproj or .xcworkspace file` : `Missing required parameter: xcodeproj\n\n💡 Expected: absolute path to .xcodeproj or .xcworkspace file` ); } const result = await ProjectTools.openProject(args.xcodeproj as string); if (result && 'content' in result && result.content?.[0] && 'text' in result.content[0]) { const textContent = result.content[0]; if (textContent.type === 'text' && typeof textContent.text === 'string') { if (!textContent.text.includes('Error') && !textContent.text.includes('does not exist')) { this.currentProjectPath = args.xcodeproj as string; } } } return result; case 'xcode_close_project': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } try { const validationError = PathValidator.validateProjectPath(args.xcodeproj as string); if (validationError) return validationError; const closeResult = await ProjectTools.closeProject(args.xcodeproj as string); this.currentProjectPath = null; return closeResult; } catch (closeError) { // Ensure close project never crashes the server Logger.error('Close project error (handled):', closeError); this.currentProjectPath = null; return { content: [{ type: 'text', text: 'Project close attempted - may have completed with dialogs' }] }; } 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) ); case 'xcode_clean': if (!this.includeClean) { throw new McpError(ErrorCode.MethodNotFound, `Clean tool is disabled`); } if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } return await BuildTools.clean(args.xcodeproj as string, this.openProject.bind(this)); case 'xcode_test': if (!args.xcodeproj) { throw new McpError( ErrorCode.InvalidParams, `Missing required parameter: xcodeproj\n\n💡 To fix this:\n• Specify the absolute path to your .xcodeproj or .xcworkspace file using the "xcodeproj" parameter\n• Example: /Users/username/MyApp/MyApp.xcodeproj\n• You can drag the project file from Finder to get the path` ); } if (!args.destination) { throw new McpError( ErrorCode.InvalidParams, `Missing required parameter: destination\n\n💡 To fix this:\n• Specify the test destination (e.g., "iPhone 15 Pro Simulator")\n• Use 'get-run-destinations' to see available destinations\n• Example: "iPad Air Simulator" or "iPhone 16 Pro"` ); } const testOptions: { testPlanPath?: string; selectedTests?: string[]; selectedTestClasses?: string[]; testTargetIdentifier?: string; testTargetName?: string; } = {}; if (args.test_plan_path) testOptions.testPlanPath = args.test_plan_path as string; if (args.selected_tests) testOptions.selectedTests = args.selected_tests as string[]; if (args.selected_test_classes) testOptions.selectedTestClasses = args.selected_test_classes as string[]; if (args.test_target_identifier) testOptions.testTargetIdentifier = args.test_target_identifier as string; if (args.test_target_name) testOptions.testTargetName = args.test_target_name as string; return await BuildTools.test( args.xcodeproj as string, args.destination as string, (args.command_line_arguments as string[]) || [], this.openProject.bind(this), Object.keys(testOptions).length > 0 ? testOptions : undefined ); 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) ); case 'xcode_debug': 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.debug( args.xcodeproj as string, args.scheme as string, args.skip_building as boolean, this.openProject.bind(this) ); case 'xcode_stop': if (!args.xcodeproj) { return { content: [{ type: 'text', text: 'Error: xcodeproj parameter is required' }] }; } return await BuildTools.stop(args.xcodeproj as string); case 'find_xcresults': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } return await BuildTools.findXCResults(args.xcodeproj as string); case 'xcode_get_schemes': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } return await ProjectTools.getSchemes(args.xcodeproj as string, this.openProject.bind(this)); case 'xcode_get_run_destinations': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } return await ProjectTools.getRunDestinations(args.xcodeproj as string, this.openProject.bind(this)); case 'xcode_set_active_scheme': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } if (!args.scheme_name) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: scheme_name`); } return await ProjectTools.setActiveScheme( args.xcodeproj as string, args.scheme_name as string, this.openProject.bind(this) ); case 'xcode_get_workspace_info': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } return await InfoTools.getWorkspaceInfo(args.xcodeproj as string, this.openProject.bind(this)); case 'xcode_get_projects': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } return await InfoTools.getProjects(args.xcodeproj as string, this.openProject.bind(this)); case 'xcode_open_file': if (!args.file_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: file_path`); } return await InfoTools.openFile(args.file_path as string, args.line_number as number); case 'xcresult_browse': if (!args.xcresult_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcresult_path`); } return await XCResultTools.xcresultBrowse( args.xcresult_path as string, args.test_id as string | undefined, args.include_console as boolean || false ); case 'xcresult_browser_get_console': if (!args.xcresult_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcresult_path`); } if (!args.test_id) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: test_id`); } return await XCResultTools.xcresultBrowserGetConsole( args.xcresult_path as string, args.test_id as string ); case 'xcresult_summary': if (!args.xcresult_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcresult_path`); } return await XCResultTools.xcresultSummary(args.xcresult_path as string); case 'xcresult_get_screenshot': if (!args.xcresult_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcresult_path`); } if (!args.test_id) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: test_id`); } if (args.timestamp === undefined) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: timestamp`); } return await XCResultTools.xcresultGetScreenshot( args.xcresult_path as string, args.test_id as string, args.timestamp as number ); case 'xcresult_get_ui_hierarchy': if (!args.xcresult_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcresult_path`); } if (!args.test_id) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: test_id`); } return await XCResultTools.xcresultGetUIHierarchy( args.xcresult_path as string, args.test_id as string, args.timestamp as number | undefined, args.full_hierarchy as boolean | undefined, args.raw_format as boolean | undefined ); case 'xcresult_get_ui_element': if (!args.hierarchy_json_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: hierarchy_json_path`); } if (args.element_index === undefined) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: element_index`); } return await XCResultTools.xcresultGetUIElement( args.hierarchy_json_path as string, args.element_index as number, args.include_children as boolean | undefined ); case 'xcresult_list_attachments': if (!args.xcresult_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcresult_path`); } if (!args.test_id) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: test_id`); } return await XCResultTools.xcresultListAttachments( args.xcresult_path as string, args.test_id as string ); case 'xcresult_export_attachment': if (!args.xcresult_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcresult_path`); } if (!args.test_id) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: test_id`); } if (args.attachment_index === undefined) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: attachment_index`); } return await XCResultTools.xcresultExportAttachment( args.xcresult_path as string, args.test_id as string, args.attachment_index as number, args.convert_to_json as boolean | undefined ); case 'xcode_get_test_targets': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } return await ProjectTools.getTestTargets(args.xcodeproj as string); case 'xcode_refresh_project': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } // Close and reopen the project to refresh it await ProjectTools.closeProject(args.xcodeproj as string); const refreshResult = await ProjectTools.openProjectAndWaitForLoad(args.xcodeproj as string); return { content: [{ type: 'text', text: `Project refreshed: ${refreshResult.content?.[0]?.type === 'text' ? refreshResult.content[0].text : 'Completed'}` }] }; default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } catch (error) { // Enhanced error handling that doesn't crash the server Logger.error(`Tool execution error for ${name}:`, error); // Check if it's a configuration-related error that we can provide guidance for const enhancedError = await this.enhanceErrorWithGuidance(error as Error, name); if (enhancedError) { return { content: [{ type: 'text', text: enhancedError }] }; } // For other errors, provide a helpful message but don't crash const errorMessage = error instanceof McpError ? error.message : error instanceof Error ? `Tool execution failed: ${error.message}` : `Tool execution failed: ${String(error)}`; return { content: [{ type: 'text', text: `❌ ${name} failed: ${errorMessage}` }] }; } }); } public async openProject(projectPath: string): Promise<McpResult> { const result = await ProjectTools.openProjectAndWaitForLoad(projectPath); if (result && 'content' in result && result.content?.[0] && 'text' in result.content[0]) { const textContent = result.content[0]; if (textContent.type === 'text' && typeof textContent.text === 'string') { if (!textContent.text.includes('❌') && !textContent.text.includes('Error') && !textContent.text.includes('does not exist')) { this.currentProjectPath = projectPath; } } } return result; } public async executeJXA(script: string): Promise<string> { const { JXAExecutor } = await import('./utils/JXAExecutor.js'); return JXAExecutor.execute(script); } public validateProjectPath(projectPath: string): McpResult | null { return PathValidator.validateProjectPath(projectPath); } public async findProjectDerivedData(projectPath: string): Promise<string | null> { const { BuildLogParser } = await import('./utils/BuildLogParser.js'); return BuildLogParser.findProjectDerivedData(projectPath); } public async getLatestBuildLog(projectPath: string) { const { BuildLogParser } = await import('./utils/BuildLogParser.js'); return BuildLogParser.getLatestBuildLog(projectPath); } // Direct method interfaces for testing/CLI compatibility public async build(projectPath: string, schemeName = 'Debug', destination: string | null = null): Promise<import('./types/index.js').McpResult> { const { BuildTools } = await import('./tools/BuildTools.js'); return BuildTools.build(projectPath, schemeName, destination, this.openProject.bind(this)); } public async clean(projectPath: string): Promise<import('./types/index.js').McpResult> { const { BuildTools } = await import('./tools/BuildTools.js'); return BuildTools.clean(projectPath, this.openProject.bind(this)); } public async test(projectPath: string, destination: string, commandLineArguments: string[] = []): Promise<import('./types/index.js').McpResult> { const { BuildTools } = await import('./tools/BuildTools.js'); return BuildTools.test(projectPath, destination, commandLineArguments, this.openProject.bind(this)); } public async run(projectPath: string, commandLineArguments: string[] = []): Promise<import('./types/index.js').McpResult> { const { BuildTools } = await import('./tools/BuildTools.js'); return BuildTools.run(projectPath, 'Debug', commandLineArguments, this.openProject.bind(this)); } public async debug(projectPath: string, scheme: string, skipBuilding = false): Promise<import('./types/index.js').McpResult> { const { BuildTools } = await import('./tools/BuildTools.js'); return BuildTools.debug(projectPath, scheme, skipBuilding, this.openProject.bind(this)); } public async stop(projectPath?: string): Promise<import('./types/index.js').McpResult> { if (!projectPath) { return { content: [{ type: 'text', text: 'Error: projectPath parameter is required' }] }; } const { BuildTools } = await import('./tools/BuildTools.js'); return BuildTools.stop(projectPath); } public async getSchemes(projectPath: string): Promise<import('./types/index.js').McpResult> { const { ProjectTools } = await import('./tools/ProjectTools.js'); return ProjectTools.getSchemes(projectPath, this.openProject.bind(this)); } public async getRunDestinations(projectPath: string): Promise<import('./types/index.js').McpResult> { const { ProjectTools } = await import('./tools/ProjectTools.js'); return ProjectTools.getRunDestinations(projectPath, this.openProject.bind(this)); } public async setActiveScheme(projectPath: string, schemeName: string): Promise<import('./types/index.js').McpResult> { const { ProjectTools } = await import('./tools/ProjectTools.js'); return ProjectTools.setActiveScheme(projectPath, schemeName, this.openProject.bind(this)); } public async getWorkspaceInfo(projectPath: string): Promise<import('./types/index.js').McpResult> { const { InfoTools } = await import('./tools/InfoTools.js'); return InfoTools.getWorkspaceInfo(projectPath, this.openProject.bind(this)); } public async getProjects(projectPath: string): Promise<import('./types/index.js').McpResult> { const { InfoTools } = await import('./tools/InfoTools.js'); return InfoTools.getProjects(projectPath, this.openProject.bind(this)); } public async openFile(filePath: string, lineNumber?: number): Promise<import('./types/index.js').McpResult> { const { InfoTools } = await import('./tools/InfoTools.js'); return InfoTools.openFile(filePath, lineNumber); } public async parseBuildLog(logPath: string, retryCount?: number, maxRetries?: number) { const { BuildLogParser } = await import('./utils/BuildLogParser.js'); return BuildLogParser.parseBuildLog(logPath, retryCount, maxRetries); } public async canParseLog(logPath: string): Promise<boolean> { const { BuildLogParser } = await import('./utils/BuildLogParser.js'); return BuildLogParser.canParseLog(logPath); } public async getCustomDerivedDataLocationFromXcodePreferences(): Promise<string | null> { const { BuildLogParser } = await import('./utils/BuildLogParser.js'); return BuildLogParser.getCustomDerivedDataLocationFromXcodePreferences(); } /** * Call a tool directly without going through the MCP protocol * This is used by the CLI to bypass the JSON-RPC layer */ public async callToolDirect(name: string, args: Record<string, unknown> = {}): Promise<CallToolResult> { // This is essentially the same logic as the CallToolRequestSchema handler // Resolve relative paths to absolute paths (this is actually handled by CLI now, but keep for safety) if (args.xcodeproj && typeof args.xcodeproj === 'string') { const { resolvedPath, error } = PathValidator.resolveAndValidateProjectPath(args.xcodeproj as string, 'xcodeproj'); if (error) { return error; } args.xcodeproj = resolvedPath; } if (args.filePath && typeof args.filePath === 'string') { const path = await import('path'); if (!path.default.isAbsolute(args.filePath)) { args.filePath = path.default.resolve(process.cwd(), args.filePath); } } try { // Handle health check tool first (no environment validation needed) if (name === 'xcode_health_check') { const report = await EnvironmentValidator.createHealthCheckReport(); return { content: [{ type: 'text', text: report }] }; } // Validate environment for all other tools const validationError = await this.validateToolOperation(name); if (validationError) { return validationError; } switch (name) { case 'xcode_open_project': if (!args.xcodeproj) { throw new McpError( ErrorCode.InvalidParams, `Missing required parameter: xcodeproj\n\n💡 Expected: absolute path to .xcodeproj or .xcworkspace file` ); } const result = await ProjectTools.openProject(args.xcodeproj as string); if (result && 'content' in result && result.content?.[0] && 'text' in result.content[0]) { const textContent = result.content[0]; if (textContent.type === 'text' && typeof textContent.text === 'string') { if (!textContent.text.includes('Error') && !textContent.text.includes('does not exist')) { this.currentProjectPath = args.xcodeproj as string; } } } return result; case 'xcode_close_project': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } try { const validationError = PathValidator.validateProjectPath(args.xcodeproj as string); if (validationError) return validationError; const closeResult = await ProjectTools.closeProject(args.xcodeproj as string); this.currentProjectPath = null; return closeResult; } catch (closeError) { // Ensure close project never crashes the server Logger.error('Close project error (handled):', closeError); this.currentProjectPath = null; return { content: [{ type: 'text', text: 'Project close attempted - may have completed with dialogs' }] }; } 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) ); case 'xcode_clean': if (!this.includeClean) { throw new McpError(ErrorCode.MethodNotFound, `Clean tool is disabled`); } if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } return await BuildTools.clean(args.xcodeproj as string, this.openProject.bind(this)); case 'xcode_test': if (!args.xcodeproj) { throw new McpError( ErrorCode.InvalidParams, `Missing required parameter: xcodeproj\n\n💡 To fix this:\n• Specify the absolute path to your .xcodeproj or .xcworkspace file using the "xcodeproj" parameter\n• Example: /Users/username/MyApp/MyApp.xcodeproj\n• You can drag the project file from Finder to get the path` ); } if (!args.destination) { throw new McpError( ErrorCode.InvalidParams, `Missing required parameter: destination\n\n💡 To fix this:\n• Specify the test destination (e.g., "iPhone 15 Pro Simulator")\n• Use 'get-run-destinations' to see available destinations\n• Example: "iPad Air Simulator" or "iPhone 16 Pro"` ); } return await BuildTools.test( args.xcodeproj as string, args.destination as string, (args.command_line_arguments as string[]) || [], this.openProject.bind(this) ); 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) ); case 'xcode_debug': 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.debug( args.xcodeproj as string, args.scheme as string, args.skip_building as boolean, this.openProject.bind(this) ); case 'xcode_stop': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } return await BuildTools.stop(args.xcodeproj as string); case 'find_xcresults': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } return await BuildTools.findXCResults(args.xcodeproj as string); case 'xcode_get_schemes': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } return await ProjectTools.getSchemes(args.xcodeproj as string, this.openProject.bind(this)); case 'xcode_get_run_destinations': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } return await ProjectTools.getRunDestinations(args.xcodeproj as string, this.openProject.bind(this)); case 'xcode_set_active_scheme': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } if (!args.scheme_name) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: scheme_name`); } return await ProjectTools.setActiveScheme( args.xcodeproj as string, args.scheme_name as string, this.openProject.bind(this) ); case 'xcode_get_workspace_info': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } return await InfoTools.getWorkspaceInfo(args.xcodeproj as string, this.openProject.bind(this)); case 'xcode_get_projects': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } return await InfoTools.getProjects(args.xcodeproj as string, this.openProject.bind(this)); case 'xcode_open_file': if (!args.file_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: file_path`); } return await InfoTools.openFile(args.file_path as string, args.line_number as number); case 'xcresult_browse': if (!args.xcresult_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcresult_path`); } return await XCResultTools.xcresultBrowse( args.xcresult_path as string, args.test_id as string | undefined, args.include_console as boolean || false ); case 'xcresult_browser_get_console': if (!args.xcresult_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcresult_path`); } if (!args.test_id) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: test_id`); } return await XCResultTools.xcresultBrowserGetConsole( args.xcresult_path as string, args.test_id as string ); case 'xcresult_summary': if (!args.xcresult_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcresult_path`); } return await XCResultTools.xcresultSummary(args.xcresult_path as string); case 'xcresult_get_screenshot': if (!args.xcresult_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcresult_path`); } if (!args.test_id) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: test_id`); } if (args.timestamp === undefined) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: timestamp`); } return await XCResultTools.xcresultGetScreenshot( args.xcresult_path as string, args.test_id as string, args.timestamp as number ); case 'xcresult_get_ui_hierarchy': if (!args.xcresult_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcresult_path`); } if (!args.test_id) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: test_id`); } return await XCResultTools.xcresultGetUIHierarchy( args.xcresult_path as string, args.test_id as string, args.timestamp as number | undefined, args.full_hierarchy as boolean | undefined, args.raw_format as boolean | undefined ); case 'xcresult_get_ui_element': if (!args.hierarchy_json_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: hierarchy_json_path`); } if (args.element_index === undefined) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: element_index`); } return await XCResultTools.xcresultGetUIElement( args.hierarchy_json_path as string, args.element_index as number, args.include_children as boolean | undefined ); case 'xcresult_list_attachments': if (!args.xcresult_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcresult_path`); } if (!args.test_id) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: test_id`); } return await XCResultTools.xcresultListAttachments( args.xcresult_path as string, args.test_id as string ); case 'xcresult_export_attachment': if (!args.xcresult_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcresult_path`); } if (!args.test_id) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: test_id`); } if (args.attachment_index === undefined) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: attachment_index`); } return await XCResultTools.xcresultExportAttachment( args.xcresult_path as string, args.test_id as string, args.attachment_index as number, args.convert_to_json as boolean | undefined ); case 'xcode_get_test_targets': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } return await ProjectTools.getTestTargets(args.xcodeproj as string); case 'xcode_refresh_project': if (!args.xcodeproj) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcodeproj`); } // Close and reopen the project to refresh it await ProjectTools.closeProject(args.xcodeproj as string); const refreshResult = await ProjectTools.openProjectAndWaitForLoad(args.xcodeproj as string); return { content: [{ type: 'text', text: `Project refreshed: ${refreshResult.content?.[0]?.type === 'text' ? refreshResult.content[0].text : 'Completed'}` }] }; default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } catch (error) { // Enhanced error handling that doesn't crash the server Logger.error(`Tool execution error for ${name}:`, error); // Check if it's a configuration-related error that we can provide guidance for const enhancedError = await this.enhanceErrorWithGuidance(error as Error, name); if (enhancedError) { return { content: [{ type: 'text', text: enhancedError }] }; } // For other errors, provide a helpful message but don't crash const errorMessage = error instanceof McpError ? error.message : error instanceof Error ? `Tool execution failed: ${error.message}` : `Tool execution failed: ${String(error)}`; return { content: [{ type: 'text', text: `❌ ${name} failed: ${errorMessage}\n\n💡 If this persists, try running 'xcode_health_check' to diagnose potential configuration issues.` }] }; } } }

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