Skip to main content
Glama
ios.ts32.9 kB
/** * @fileoverview iOS Tools for MCP Mobile Server * * This module provides comprehensive iOS development tools for the MCP protocol, * enabling AI agents to interact with iOS Simulator, Xcode, and the complete * iOS development ecosystem. All tools are macOS-specific and require Xcode. * * @module tools/ios * @category Core Tools * * Key Features: * - iOS Simulator management (list, boot, shutdown, erase, clone) * - Deep link and URL testing * - Screenshot and video recording * - Xcode project integration (build, list schemes) * - Runtime management (list iOS versions, install runtimes) * - macOS platform validation * * @platform darwin (macOS only) * @requires Xcode Command Line Tools * * @example * ```typescript * const iosTools = createIOSTools(globalProcessMap); * const simulatorsTool = iosTools.get('ios_list_simulators'); * const result = await simulatorsTool.handler({}); * console.log(result.data.simulators); * ``` */ import { z } from 'zod'; import { processExecutor } from '../utils/process.js'; import path from 'path'; import fs from 'fs/promises'; import { spawn } from 'child_process'; /** * Global map tracking active video recording processes. * Maps recording IDs to process IDs. * * @type {Map<string, number>} */ let activeRecordings: Map<string, number>; /** * Zod validation schema for iOS simulator action tools. * Used by boot, shutdown, erase operations. * * @type {z.ZodObject} * @property {string} udid - Simulator UDID (Universally Unique Identifier) */ const IosSimulatorActionSchema = z.object({ udid: z.string().min(1), }); /** * Zod validation schema for ios_simulator_open_url tool. * Validates deep link and URL opening parameters. * * @type {z.ZodObject} * @property {string} udid - Simulator UDID * @property {string} url - URL to open (must be valid URL format) */ const IosSimulatorOpenUrlSchema = z.object({ udid: z.string().min(1), url: z.string().url(), }); /** * Zod validation schema for ios_simulator_screenshot tool. * * @type {z.ZodObject} * @property {string} udid - Simulator UDID * @property {string} path - Output file path for screenshot */ const IosSimulatorScreenshotSchema = z.object({ udid: z.string().min(1), path: z.string().min(1), }); /** * Zod validation schema for ios_simulator_record tool. * Validates video recording parameters. * * @type {z.ZodObject} * @property {string} udid - Simulator UDID * @property {string} path - Output file path for video * @property {number} duration - Recording duration in seconds (1-300, default: 30) */ const IosSimulatorRecordSchema = z.object({ udid: z.string().min(1), path: z.string().min(1), duration: z.number().min(1).max(300).default(30), }); /** * Zod validation schema for ios_xcodebuild tool. * Validates Xcode build parameters. * * @type {z.ZodObject} * @property {string} [workspace] - Xcode workspace file path (.xcworkspace) * @property {string} [project] - Xcode project file path (.xcodeproj) * @property {string} scheme - Build scheme name * @property {string} configuration - Build configuration (default: "Debug") * @property {string} [destination] - Build destination (e.g., "platform=iOS Simulator,name=iPhone 14") */ const IosXcodeBuildSchema = z.object({ workspace: z.string().optional(), project: z.string().optional(), scheme: z.string().min(1), configuration: z.string().default('Debug'), destination: z.string().optional(), }); /** * Zod validation schema for ios_xcode_list tool. * Validates parameters for listing Xcode schemes and targets. * * @type {z.ZodObject} * @property {string} [project] - Xcode project file path * @property {string} [workspace] - Xcode workspace file path */ const IosXcodeListSchema = z.object({ project: z.string().optional(), workspace: z.string().optional(), }); /** * Validates that the current platform is macOS (darwin). * iOS tools require macOS with Xcode installed. * * @returns {void} * @throws {Error} If current platform is not macOS * * @example * ```typescript * checkMacOS(); // Throws on Windows/Linux * ``` */ const checkMacOS = (): void => { if (process.platform !== 'darwin') { throw new Error(`iOS development tools only work on macOS. Current platform: ${process.platform}`); } }; /** * Validates iOS Simulator UDID format. * UDIDs must match UUID v4 format (8-4-4-4-12 hexadecimal pattern). * * @param {string} udid - Simulator UDID to validate * @returns {boolean} True if valid UDID format, false otherwise * * @example * ```typescript * validateUDID('12345678-1234-1234-1234-123456789ABC'); // true * validateUDID('invalid-udid'); // false * ``` */ const validateUDID = (udid: string): boolean => { const uuidPattern = /^[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}$/i; return uuidPattern.test(udid); }; /** * Validates Xcode scheme name format. * Scheme names can only contain alphanumeric characters, underscores, and dashes. * * @param {scheme} scheme - Scheme name to validate * @returns {boolean} True if valid scheme name, false otherwise * * @example * ```typescript * validateSchemeName('MyApp-Prod'); // true * validateSchemeName('Invalid Scheme!'); // false * ``` */ const validateSchemeName = (scheme: string): boolean => { const schemePattern = /^[a-zA-Z0-9_-]+$/; return schemePattern.test(scheme); }; /** * Creates and configures all iOS tools for the MCP Mobile Server. * * This factory function initializes and returns a comprehensive set of iOS development tools, * providing complete iOS Simulator and Xcode integration for AI agents via the MCP protocol. * **macOS Only** - All tools require macOS (darwin) platform with Xcode Command Line Tools. * * **Tools Created:** * - `ios_list_simulators`: List all available iOS simulators with status * - `ios_boot_simulator`: Boot (start) an iOS simulator * - `ios_shutdown_simulator`: Shutdown a running simulator * - `ios_erase_simulator`: Erase simulator content and settings * - `ios_clone_simulator`: Clone an existing simulator * - `ios_simulator_open_url`: Open URL or deep link in simulator * - `ios_simulator_screenshot`: Capture simulator screenshot * - `ios_simulator_record`: Record simulator video * - `ios_list_runtimes`: List available iOS runtime versions * - `ios_install_runtime`: Install new iOS runtime version * - `ios_xcodebuild`: Build Xcode project or workspace * - `ios_xcode_list`: List available schemes and targets * * **Features:** * - Automatic macOS platform validation for all tools * - UDID format validation (UUID v4) * - Xcode scheme name validation * - Video recording with duration control (1-300 seconds) * - Process lifecycle tracking for recordings * - Support for both Xcode projects (.xcodeproj) and workspaces (.xcworkspace) * - Comprehensive error handling with detailed messages * * @param {Map<string, number>} globalProcessMap - Global map tracking all active processes * @returns {Map<string, any>} Map of tool names to tool configurations * * @example * ```typescript * const globalProcesses = new Map(); * const tools = createIOSTools(globalProcesses); * * // List simulators * const listSim = tools.get('ios_list_simulators'); * const sims = await listSim.handler({}); * console.log(sims.data.simulators); * * // Boot simulator * const bootSim = tools.get('ios_boot_simulator'); * await bootSim.handler({ * udid: '12345678-1234-1234-1234-123456789ABC' * }); * * // Take screenshot * const screenshot = tools.get('ios_simulator_screenshot'); * await screenshot.handler({ * udid: '12345678-1234-1234-1234-123456789ABC', * path: '/tmp/screenshot.png' * }); * * // Build Xcode project * const build = tools.get('ios_xcodebuild'); * await build.handler({ * project: 'MyApp.xcodeproj', * scheme: 'MyApp', * configuration: 'Debug', * destination: 'platform=iOS Simulator,name=iPhone 14' * }); * ``` * * @throws {Error} If platform is not macOS (darwin) * @throws {Error} If Xcode Command Line Tools are not installed * @throws {Error} If validation schemas fail for any tool parameters * * @platform darwin (macOS only) * @requires Xcode Command Line Tools * * @see {@link https://developer.apple.com/documentation/xcode|Xcode Documentation} * @see {@link https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/Simulators.html|iOS Simulator Guide} */ export function createIOSTools(globalProcessMap: Map<string, number>): Map<string, any> { // Initialize local recordings tracking if not provided activeRecordings = globalProcessMap; const tools = new Map<string, any>(); // iOS Simulator Management - List simulators tools.set('ios_list_simulators', { name: 'ios_list_simulators', description: 'List available iOS simulators', inputSchema: { type: 'object', properties: {}, required: [] }, handler: async () => { checkMacOS(); const result = await processExecutor.execute('xcrun', ['simctl', 'list', 'devices', '--json']); if (result.exitCode !== 0) { throw new Error(`Failed to list iOS simulators: ${result.stderr}`); } let devicesData; try { devicesData = JSON.parse(result.stdout); } catch (parseError) { throw new Error(`Failed to parse simulator list JSON: ${parseError}`); } const simulators = []; const devices = devicesData.devices || {}; for (const [runtime, deviceList] of Object.entries(devices)) { if (Array.isArray(deviceList)) { for (const device of deviceList as any[]) { simulators.push({ udid: device.udid, name: device.name, state: device.state, runtime: runtime.replace('com.apple.CoreSimulator.SimRuntime.', ''), deviceTypeIdentifier: device.deviceTypeIdentifier, isAvailable: device.isAvailable || false, }); } } } return { success: true, data: { simulators, totalCount: simulators.length, bootedCount: simulators.filter((s: any) => s.state === 'Booted').length, availableCount: simulators.filter((s: any) => s.isAvailable).length, }, }; } }); // iOS Simulator Management - Boot simulator tools.set('ios_boot_simulator', { name: 'ios_boot_simulator', description: 'Boot an iOS simulator', inputSchema: { type: 'object', properties: { udid: { type: 'string', minLength: 1, description: 'Simulator UDID' } }, required: ['udid'] }, handler: async (args: any) => { checkMacOS(); const validation = IosSimulatorActionSchema.safeParse(args); if (!validation.success) { throw new Error(`Invalid request: ${validation.error.message}`); } const { udid } = validation.data; // Validate UDID format if (!validateUDID(udid)) { throw new Error(`Invalid simulator UDID format. UDID must be in format XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX: ${udid}`); } const result = await processExecutor.execute('xcrun', ['simctl', 'boot', udid], { timeout: 60000, // 1 minute timeout for simulator boot }); // Note: simctl boot returns exit code 164 if simulator is already booted, which is not an error if (result.exitCode !== 0 && result.exitCode !== 164) { throw new Error(`Failed to boot iOS simulator: ${result.stderr}`); } const isAlreadyBooted = result.exitCode === 164; return { success: true, data: { udid, status: isAlreadyBooted ? 'already_booted' : 'booted', message: isAlreadyBooted ? 'Simulator was already booted' : 'Simulator booted successfully', output: result.stdout, }, }; } }); // iOS Simulator Management - Shutdown simulator tools.set('ios_shutdown_simulator', { name: 'ios_shutdown_simulator', description: 'Shutdown an iOS simulator', inputSchema: { type: 'object', properties: { udid: { type: 'string', minLength: 1, description: 'Simulator UDID' } }, required: ['udid'] }, handler: async (args: any) => { checkMacOS(); const validation = IosSimulatorActionSchema.safeParse(args); if (!validation.success) { throw new Error(`Invalid request: ${validation.error.message}`); } const { udid } = validation.data; // Validate UDID format if (!validateUDID(udid)) { throw new Error(`Invalid simulator UDID format. UDID must be in format XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX: ${udid}`); } // Stop any active recordings for this simulator if (activeRecordings.has(udid)) { const pid = activeRecordings.get(udid); try { process.kill(pid!, 'SIGTERM'); activeRecordings.delete(udid); } catch { // Process might already be dead activeRecordings.delete(udid); } } const result = await processExecutor.execute('xcrun', ['simctl', 'shutdown', udid], { timeout: 30000, // 30 seconds timeout for shutdown }); // Note: simctl shutdown returns exit code 164 if simulator is already shut down if (result.exitCode !== 0 && result.exitCode !== 164) { throw new Error(`Failed to shutdown iOS simulator: ${result.stderr}`); } const wasAlreadyShutdown = result.exitCode === 164; return { success: true, data: { udid, status: wasAlreadyShutdown ? 'already_shutdown' : 'shutdown', message: wasAlreadyShutdown ? 'Simulator was already shut down' : 'Simulator shut down successfully', output: result.stdout, }, }; } }); // iOS Simulator Management - Erase simulator tools.set('ios_erase_simulator', { name: 'ios_erase_simulator', description: 'Erase all data from an iOS simulator', inputSchema: { type: 'object', properties: { udid: { type: 'string', minLength: 1, description: 'Simulator UDID' } }, required: ['udid'] }, handler: async (args: any) => { checkMacOS(); const validation = IosSimulatorActionSchema.safeParse(args); if (!validation.success) { throw new Error(`Invalid request: ${validation.error.message}`); } const { udid } = validation.data; // Validate UDID format if (!validateUDID(udid)) { throw new Error(`Invalid simulator UDID format. UDID must be in format XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX: ${udid}`); } const result = await processExecutor.execute('xcrun', ['simctl', 'erase', udid], { timeout: 120000, // 2 minutes timeout for erase }); if (result.exitCode !== 0) { throw new Error(`Failed to erase iOS simulator: ${result.stderr}`); } return { success: true, data: { udid, status: 'erased', message: 'Simulator data erased successfully', output: result.stdout, }, }; } }); // iOS Simulator Utilities - Open URL tools.set('ios_open_url', { name: 'ios_open_url', description: 'Open a URL in an iOS simulator', inputSchema: { type: 'object', properties: { udid: { type: 'string', minLength: 1, description: 'Simulator UDID' }, url: { type: 'string', format: 'uri', description: 'URL to open in the simulator' } }, required: ['udid', 'url'] }, handler: async (args: any) => { checkMacOS(); const validation = IosSimulatorOpenUrlSchema.safeParse(args); if (!validation.success) { throw new Error(`Invalid request: ${validation.error.message}`); } const { udid, url } = validation.data; // Validate UDID format if (!validateUDID(udid)) { throw new Error(`Invalid simulator UDID format. UDID must be in format XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX: ${udid}`); } // Validate URL format try { new URL(url); } catch { throw new Error(`Invalid URL format. URL must be a valid HTTP/HTTPS or custom scheme URL: ${url}`); } const result = await processExecutor.execute('xcrun', ['simctl', 'openurl', udid, url]); if (result.exitCode !== 0) { throw new Error(`Failed to open URL in simulator: ${result.stderr}`); } return { success: true, data: { udid, url, status: 'opened', message: 'URL opened successfully in simulator', output: result.stdout, }, }; } }); // iOS Simulator Utilities - Take screenshot tools.set('ios_take_screenshot', { name: 'ios_take_screenshot', description: 'Take a screenshot of an iOS simulator', inputSchema: { type: 'object', properties: { udid: { type: 'string', minLength: 1, description: 'Simulator UDID' }, path: { type: 'string', minLength: 1, description: 'Absolute path to save screenshot' } }, required: ['udid', 'path'] }, handler: async (args: any) => { checkMacOS(); const validation = IosSimulatorScreenshotSchema.safeParse(args); if (!validation.success) { throw new Error(`Invalid request: ${validation.error.message}`); } const { udid, path: screenshotPath } = validation.data; // Validate UDID format if (!validateUDID(udid)) { throw new Error(`Invalid simulator UDID format. UDID must be in format XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX: ${udid}`); } // Validate screenshot path - must be absolute and not contain dangerous patterns if (!path.isAbsolute(screenshotPath)) { throw new Error(`Screenshot path must be absolute. Path must start with /: ${screenshotPath}`); } // Security check - prevent path traversal and access to sensitive directories const normalizedPath = path.normalize(screenshotPath); const dangerousPaths = ['/etc', '/usr', '/System', '/private', '/var']; if (dangerousPaths.some(dangerous => normalizedPath.startsWith(dangerous))) { throw new Error(`Access to this path is not allowed. Path access denied for security reasons: ${normalizedPath}`); } // Ensure file has image extension const allowedExtensions = ['.png', '.jpg', '.jpeg']; const extension = path.extname(screenshotPath).toLowerCase(); if (!allowedExtensions.includes(extension)) { throw new Error(`Screenshot file must have image extension. Allowed extensions: ${allowedExtensions.join(', ')}. Got: ${extension}`); } // Create directory if it doesn't exist const directory = path.dirname(screenshotPath); try { await fs.mkdir(directory, { recursive: true }); } catch (mkdirError) { throw new Error(`Failed to create screenshot directory: ${mkdirError}`); } const result = await processExecutor.execute('xcrun', ['simctl', 'io', udid, 'screenshot', screenshotPath]); if (result.exitCode !== 0) { throw new Error(`Failed to capture screenshot: ${result.stderr}`); } // Verify screenshot was created let fileStats; try { fileStats = await fs.stat(screenshotPath); } catch { throw new Error(`Screenshot file was not created. Expected file: ${screenshotPath}`); } return { success: true, data: { udid, screenshotPath, fileSize: fileStats.size, status: 'captured', message: 'Screenshot captured successfully', output: result.stdout, }, }; } }); // iOS Simulator Utilities - Record video tools.set('ios_record_video', { name: 'ios_record_video', description: 'Record video of an iOS simulator (starts background recording)', inputSchema: { type: 'object', properties: { udid: { type: 'string', minLength: 1, description: 'Simulator UDID' }, path: { type: 'string', minLength: 1, description: 'Absolute path to save video' }, duration: { type: 'number', minimum: 1, maximum: 300, description: 'Recording duration in seconds (max 5 minutes)' } }, required: ['udid', 'path'] }, handler: async (args: any) => { checkMacOS(); const validation = IosSimulatorRecordSchema.safeParse(args); if (!validation.success) { throw new Error(`Invalid request: ${validation.error.message}`); } const { udid, path: videoPath, duration = 30 } = validation.data; // Validate UDID format if (!validateUDID(udid)) { throw new Error(`Invalid simulator UDID format. UDID must be in format XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX: ${udid}`); } // Check if recording is already active for this simulator if (activeRecordings.has(udid)) { throw new Error(`Recording already active for this simulator. Active recording PID: ${activeRecordings.get(udid)}`); } // Validate video path if (!path.isAbsolute(videoPath)) { throw new Error(`Video path must be absolute. Path must start with /: ${videoPath}`); } // Security check for path const normalizedPath = path.normalize(videoPath); const dangerousPaths = ['/etc', '/usr', '/System', '/private', '/var']; if (dangerousPaths.some(dangerous => normalizedPath.startsWith(dangerous))) { throw new Error(`Access to this path is not allowed. Path access denied for security reasons: ${normalizedPath}`); } // Ensure file has video extension const allowedExtensions = ['.mov', '.mp4']; const extension = path.extname(videoPath).toLowerCase(); if (!allowedExtensions.includes(extension)) { throw new Error(`Video file must have video extension. Allowed extensions: ${allowedExtensions.join(', ')}. Got: ${extension}`); } // Validate duration if (duration < 1 || duration > 300) { // Max 5 minutes throw new Error(`Duration must be between 1 and 300 seconds. Got duration: ${duration}`); } // Create directory if it doesn't exist const directory = path.dirname(videoPath); try { await fs.mkdir(directory, { recursive: true }); } catch (mkdirError) { throw new Error(`Failed to create video directory: ${mkdirError}`); } // Start video recording in background const recordProcess = spawn('xcrun', ['simctl', 'io', udid, 'recordVideo', '--timeout', duration.toString(), videoPath], { detached: true, stdio: 'ignore', }); // Track the recording process activeRecordings.set(udid, recordProcess.pid!); // Handle process exit recordProcess.on('exit', (code) => { activeRecordings.delete(udid); }); // Auto-cleanup after duration + buffer time setTimeout(() => { if (activeRecordings.has(udid)) { try { process.kill(recordProcess.pid!, 'SIGTERM'); activeRecordings.delete(udid); } catch { activeRecordings.delete(udid); } } }, (duration + 5) * 1000); // Add 5 seconds buffer // Unref to allow parent process to exit recordProcess.unref(); return { success: true, data: { udid, videoPath, duration, pid: recordProcess.pid, status: 'recording_started', message: `Video recording started for ${duration} seconds`, }, }; } }); // iOS Xcode Integration - List schemes tools.set('ios_list_schemes', { name: 'ios_list_schemes', description: 'List Xcode schemes for a project or workspace', inputSchema: { type: 'object', properties: { project: { type: 'string', description: 'Path to .xcodeproj file' }, workspace: { type: 'string', description: 'Path to .xcworkspace file' } } }, handler: async (args: any) => { checkMacOS(); const validation = IosXcodeListSchema.safeParse(args); if (!validation.success) { throw new Error(`Invalid request: ${validation.error.message}`); } const { project, workspace } = validation.data; if (!project && !workspace) { // Try to auto-detect project/workspace in current directory const cwd = process.cwd(); const files = await fs.readdir(cwd); let detectedWorkspace = files.find(f => f.endsWith('.xcworkspace')); let detectedProject = files.find(f => f.endsWith('.xcodeproj')); if (!detectedWorkspace && !detectedProject) { throw new Error('No Xcode project or workspace found. Either provide project/workspace parameter or run from directory containing .xcodeproj/.xcworkspace'); } } const xcode_args = ['-list']; if (workspace) { xcode_args.push('-workspace', workspace); } else if (project) { xcode_args.push('-project', project); } const result = await processExecutor.execute('xcodebuild', xcode_args, { timeout: 30000, // 30 seconds timeout }); if (result.exitCode !== 0) { throw new Error(`Failed to list Xcode schemes: ${result.stderr}`); } // Parse xcodebuild -list output const lines = result.stdout.split('\n'); const schemes = []; const targets = []; const configurations = []; let currentSection = ''; for (const line of lines) { const trimmed = line.trim(); if (trimmed.includes('Schemes:')) { currentSection = 'schemes'; continue; } else if (trimmed.includes('Targets:')) { currentSection = 'targets'; continue; } else if (trimmed.includes('Build Configurations:')) { currentSection = 'configurations'; continue; } else if (trimmed === '') { currentSection = ''; continue; } if (currentSection && trimmed && !trimmed.startsWith('Information about project')) { switch (currentSection) { case 'schemes': schemes.push(trimmed); break; case 'targets': targets.push(trimmed); break; case 'configurations': configurations.push(trimmed); break; } } } return { success: true, data: { project: project || 'auto-detected', workspace: workspace || 'auto-detected', schemes, targets, configurations, rawOutput: result.stdout, }, }; } }); // iOS Xcode Integration - Build project tools.set('ios_build_project', { name: 'ios_build_project', description: 'Build an iOS Xcode project or workspace', inputSchema: { type: 'object', properties: { workspace: { type: 'string', description: 'Path to .xcworkspace file' }, project: { type: 'string', description: 'Path to .xcodeproj file' }, scheme: { type: 'string', minLength: 1, description: 'Build scheme name' }, configuration: { type: 'string', description: 'Build configuration (Debug, Release, etc.)' }, destination: { type: 'string', description: 'Build destination (e.g., platform=iOS Simulator,name=iPhone 15)' } }, required: ['scheme'] }, handler: async (args: any) => { checkMacOS(); const validation = IosXcodeBuildSchema.safeParse(args); if (!validation.success) { throw new Error(`Invalid request: ${validation.error.message}`); } const { workspace, project, scheme, configuration = 'Debug', destination } = validation.data; if (!workspace && !project) { throw new Error('Either workspace or project must be specified. Provide either workspace or project parameter'); } // Validate scheme name if (!validateSchemeName(scheme)) { throw new Error(`Invalid scheme name format. Scheme name can only contain alphanumeric characters, underscores, and dashes: ${scheme}`); } const xcode_args = []; if (workspace) { xcode_args.push('-workspace', workspace); } else if (project) { xcode_args.push('-project', project); } xcode_args.push('-scheme', scheme); if (configuration) { xcode_args.push('-configuration', configuration); } if (destination) { xcode_args.push('-destination', destination); } xcode_args.push('build'); const result = await processExecutor.execute('xcodebuild', xcode_args, { timeout: 600000, // 10 minutes timeout for builds }); const success = result.exitCode === 0; return { success, data: { workspace: workspace || null, project: project || null, scheme, configuration: configuration || 'Debug', destination: destination || null, exitCode: result.exitCode, buildSucceeded: success, duration: result.duration, output: result.stdout, errors: result.stderr, }, }; } }); // iOS Xcode Integration - Run tests tools.set('ios_run_tests', { name: 'ios_run_tests', description: 'Run tests for an iOS Xcode project or workspace', inputSchema: { type: 'object', properties: { workspace: { type: 'string', description: 'Path to .xcworkspace file' }, project: { type: 'string', description: 'Path to .xcodeproj file' }, scheme: { type: 'string', minLength: 1, description: 'Test scheme name' }, configuration: { type: 'string', description: 'Build configuration (Debug, Release, etc.)' }, destination: { type: 'string', description: 'Test destination (e.g., platform=iOS Simulator,name=iPhone 15)' } }, required: ['scheme'] }, handler: async (args: any) => { checkMacOS(); const validation = IosXcodeBuildSchema.safeParse(args); if (!validation.success) { throw new Error(`Invalid request: ${validation.error.message}`); } const { workspace, project, scheme, configuration = 'Debug', destination } = validation.data; if (!workspace && !project) { throw new Error('Either workspace or project must be specified. Provide either workspace or project parameter'); } // Validate scheme name if (!validateSchemeName(scheme)) { throw new Error(`Invalid scheme name format. Scheme name can only contain alphanumeric characters, underscores, and dashes: ${scheme}`); } const xcode_args = []; if (workspace) { xcode_args.push('-workspace', workspace); } else if (project) { xcode_args.push('-project', project); } xcode_args.push('-scheme', scheme); if (configuration) { xcode_args.push('-configuration', configuration); } if (destination) { xcode_args.push('-destination', destination); } xcode_args.push('test'); const result = await processExecutor.execute('xcodebuild', xcode_args, { timeout: 900000, // 15 minutes timeout for tests }); // Parse test results from output const testPassed = result.stdout.includes('Test Succeeded') || result.exitCode === 0; const testFailed = result.stdout.includes('Test Failed') || result.exitCode !== 0; // Extract basic test statistics const testSummaryRegex = /Tests run: (\d+), Failures: (\d+), Errors: (\d+), Skipped: (\d+)/; const match = result.stdout.match(testSummaryRegex); const testStats = match && match.length >= 5 ? { totalTests: parseInt(match[1] || '0', 10), failures: parseInt(match[2] || '0', 10), errors: parseInt(match[3] || '0', 10), skipped: parseInt(match[4] || '0', 10), } : null; const success = result.exitCode === 0; return { success, data: { workspace: workspace || null, project: project || null, scheme, configuration: configuration || 'Debug', destination: destination || null, exitCode: result.exitCode, testsPassed: testPassed, testsFailed: testFailed, testStatistics: testStats, duration: result.duration, output: result.stdout, errors: result.stderr, }, }; } }); return tools; }

Implementation Reference

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/cristianoaredes/mcp-mobile-server'

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