Skip to main content
Glama

XC-MCP: XCode CLI wrapper

by conorluddy
fresh-install.ts16.6 kB
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; /** * Workflow: Fresh Install - Clean slate app installation * * Orchestrates a complete clean installation cycle: * 1. simctl-device shutdown → Ensure simulator is stopped * 2. (optional) simctl-device erase → Wipe simulator data * 3. simctl-device boot → Start fresh simulator * 4. xcodebuild-build → Build the project * 5. simctl-app install → Install the app * 6. simctl-app launch → Start the app * * This workflow keeps intermediate results internal, returning only the final outcome. * Reduces agent context usage by ~70% compared to calling each tool manually. * * Part of the Programmatic Tool Calling pattern from Anthropic: * https://www.anthropic.com/engineering/advanced-tool-use */ export interface FreshInstallArgs { projectPath: string; // Path to .xcodeproj or .xcworkspace scheme: string; // Build scheme name simulatorUdid?: string; // Target simulator (auto-detected if omitted) eraseSimulator?: boolean; // Wipe simulator before install (default: false) configuration?: 'Debug' | 'Release'; // Build configuration (default: Debug) launchArguments?: string[]; // App launch arguments environmentVariables?: Record<string, string>; // App environment variables } interface WorkflowStep { name: string; success: boolean; result?: any; duration?: number; skipped?: boolean; skipReason?: string; } export async function workflowFreshInstallTool(args: FreshInstallArgs) { const { projectPath, scheme, simulatorUdid, eraseSimulator = false, configuration = 'Debug', launchArguments, environmentVariables, } = args; if (!projectPath || !scheme) { throw new McpError(ErrorCode.InvalidRequest, 'projectPath and scheme are required'); } const workflow: { steps: WorkflowStep[]; success: boolean; totalDuration: number; errors: string[]; } = { steps: [], success: false, totalDuration: 0, errors: [], }; const startTime = Date.now(); let targetUdid = simulatorUdid; let targetName = 'Unknown'; let bundleId = ''; try { // Dynamic imports to avoid circular dependencies const { simctlDeviceTool } = await import('../simctl/device/index.js'); const { simctlSuggestTool } = await import('../simctl/suggest.js'); const { xcodebuildBuildTool } = await import('../xcodebuild/build.js'); const { simctlInstallTool } = await import('../simctl/install.js'); const { simctlLaunchTool } = await import('../simctl/launch.js'); // ===== STEP 1: Select Simulator ===== console.error(`[workflow-fresh-install] Step 1/6: Selecting simulator...`); if (!targetUdid) { try { const suggestResult = await simctlSuggestTool({ projectPath, maxSuggestions: 1, autoBootTopSuggestion: false, }); const suggestText = suggestResult.content?.[0]?.text || JSON.stringify(suggestResult); const suggestData = typeof suggestText === 'string' ? JSON.parse(suggestText) : suggestText; if (suggestData.suggestions && suggestData.suggestions.length > 0) { targetUdid = suggestData.suggestions[0].simulator.udid; targetName = suggestData.suggestions[0].simulator.name; workflow.steps.push({ name: 'select-simulator', success: true, result: { udid: targetUdid, name: targetName, source: 'auto-suggested' }, }); console.error(`[workflow-fresh-install] ✅ Selected: ${targetName}`); } else { throw new Error('No suitable simulator found'); } } catch (suggestError) { workflow.errors.push(`Simulator selection failed: ${suggestError}`); throw suggestError; } } else { targetName = `${simulatorUdid} (specified)`; workflow.steps.push({ name: 'select-simulator', success: true, result: { udid: targetUdid, name: targetName, source: 'user-specified' }, }); } // ===== STEP 2: Shutdown Simulator ===== console.error(`[workflow-fresh-install] Step 2/6: Shutting down ${targetName}...`); try { const shutdownResult = await simctlDeviceTool({ operation: 'shutdown', deviceId: targetUdid, }); const shutdownText = shutdownResult.content?.[0]?.text || JSON.stringify(shutdownResult); const shutdownData = typeof shutdownText === 'string' ? JSON.parse(shutdownText) : shutdownText; workflow.steps.push({ name: 'shutdown', success: true, result: { wasRunning: !shutdownData.alreadyShutdown }, }); console.error(`[workflow-fresh-install] ✅ Simulator shut down`); } catch (shutdownError) { // Non-fatal - simulator may already be shutdown console.error(`[workflow-fresh-install] ⚠️ Shutdown failed (may already be off)`); workflow.steps.push({ name: 'shutdown', success: true, // Treat as success - we just need it to be shut down result: { note: 'Already shutdown or failed', error: String(shutdownError) }, }); } // ===== STEP 3: Erase Simulator (Optional) ===== if (eraseSimulator) { console.error(`[workflow-fresh-install] Step 3/6: Erasing simulator data...`); try { const eraseResult = await simctlDeviceTool({ operation: 'erase', deviceId: targetUdid, }); const eraseText = eraseResult.content?.[0]?.text || JSON.stringify(eraseResult); const eraseData = typeof eraseText === 'string' ? JSON.parse(eraseText) : eraseText; workflow.steps.push({ name: 'erase', success: eraseData.success !== false, result: { erased: true }, }); console.error(`[workflow-fresh-install] ✅ Simulator erased`); } catch (eraseError) { workflow.errors.push(`Erase failed: ${eraseError}`); throw eraseError; } } else { workflow.steps.push({ name: 'erase', success: true, skipped: true, skipReason: 'eraseSimulator not requested', }); } // ===== STEP 4: Boot Simulator ===== console.error(`[workflow-fresh-install] Step 4/6: Booting ${targetName}...`); try { const bootResult = await simctlDeviceTool({ operation: 'boot', deviceId: targetUdid, waitForBoot: true, openGui: true, }); const bootText = bootResult.content?.[0]?.text || JSON.stringify(bootResult); const bootData = typeof bootText === 'string' ? JSON.parse(bootText) : bootText; workflow.steps.push({ name: 'boot', success: bootData.success !== false, result: { bootTime: bootData.bootTime, state: bootData.state || 'Booted', }, }); console.error(`[workflow-fresh-install] ✅ Simulator booted`); } catch (bootError) { workflow.errors.push(`Boot failed: ${bootError}`); throw bootError; } // ===== STEP 5: Build Project ===== console.error(`[workflow-fresh-install] Step 5/6: Building ${scheme}...`); let appPath = ''; try { const buildResult = await xcodebuildBuildTool({ projectPath, scheme, configuration, destination: `platform=iOS Simulator,id=${targetUdid}`, autoInstall: false, // We handle install manually }); const buildText = buildResult.content?.[0]?.text || JSON.stringify(buildResult); const buildData = typeof buildText === 'string' ? JSON.parse(buildText) : buildText; if (!buildData.success) { throw new Error(buildData.summary?.firstError || 'Build failed'); } // Get app path from build artifacts const { findBuildArtifacts } = await import('../../utils/build-artifacts.js'); const artifacts = await findBuildArtifacts(projectPath, scheme, configuration); if (!artifacts.appPath) { throw new Error('Could not find .app bundle after build'); } appPath = artifacts.appPath; bundleId = artifacts.bundleIdentifier || ''; workflow.steps.push({ name: 'build', success: true, result: { duration: buildData.summary?.duration, appPath: appPath, bundleId: bundleId || 'unknown', }, }); console.error(`[workflow-fresh-install] ✅ Build succeeded`); } catch (buildError) { workflow.errors.push(`Build failed: ${buildError}`); throw buildError; } // ===== STEP 6: Install App ===== console.error(`[workflow-fresh-install] Step 6/6: Installing app...`); try { const installResult = await simctlInstallTool({ udid: targetUdid, appPath: appPath, }); const installText = installResult.content?.[0]?.text || JSON.stringify(installResult); const installData = typeof installText === 'string' ? JSON.parse(installText) : installText; // Update bundle ID from install result if available if (installData.bundleId) { bundleId = installData.bundleId; } workflow.steps.push({ name: 'install', success: installData.success !== false, result: { bundleId: bundleId, appPath: appPath, }, }); console.error(`[workflow-fresh-install] ✅ App installed`); } catch (installError) { workflow.errors.push(`Install failed: ${installError}`); throw installError; } // ===== STEP 7: Launch App ===== console.error(`[workflow-fresh-install] Launching app...`); try { if (!bundleId) { throw new Error('Could not determine bundle ID - cannot launch'); } const launchResult = await simctlLaunchTool({ udid: targetUdid, bundleId, arguments: launchArguments, environment: environmentVariables, }); const launchText = launchResult.content?.[0]?.text || JSON.stringify(launchResult); const launchData = typeof launchText === 'string' ? JSON.parse(launchText) : launchText; workflow.steps.push({ name: 'launch', success: launchData.success !== false, result: { bundleId: bundleId, pid: launchData.pid, }, }); console.error(`[workflow-fresh-install] ✅ App launched`); } catch (launchError) { workflow.errors.push(`Launch failed: ${launchError}`); throw launchError; } // ===== SUCCESS ===== workflow.success = true; workflow.totalDuration = Date.now() - startTime; const responseData = { success: true, project: { path: projectPath, scheme, configuration, }, simulator: { udid: targetUdid, name: targetName, erased: eraseSimulator, }, app: { bundleId, appPath, launched: true, }, totalDuration: workflow.totalDuration, stepsCompleted: workflow.steps.filter(s => s.success && !s.skipped).length, guidance: [ `✅ Fresh install completed successfully in ${workflow.totalDuration}ms`, ``, `App is now running on ${targetName}`, `Bundle ID: ${bundleId}`, eraseSimulator ? `Simulator was erased before install` : undefined, ``, `Next steps:`, `• Interact with UI: workflow-tap-element --elementQuery "button name"`, `• Take screenshot: screenshot`, `• Check accessibility: idb-ui-describe --operation all`, `• Find elements: idb-ui-find-element --query "search term"`, ].filter(Boolean), }; return { content: [{ type: 'text' as const, text: JSON.stringify(responseData, null, 2) }], isError: false, }; } catch (error) { workflow.success = false; workflow.totalDuration = Date.now() - startTime; const errorMessage = error instanceof Error ? error.message : String(error); const responseData = { success: false, project: { path: projectPath, scheme, configuration, }, simulator: targetUdid ? { udid: targetUdid, name: targetName, } : undefined, workflow, error: errorMessage, guidance: [ `❌ Fresh install failed: ${errorMessage}`, ``, `Steps completed:`, ...workflow.steps.map( step => ` ${step.success ? '✅' : step.skipped ? '⏭️' : '❌'} ${step.name}` ), ``, `To debug:`, `• Check build errors: xcodebuild-build --projectPath "${projectPath}" --scheme "${scheme}"`, `• Check available simulators: simctl-list`, `• Check simulator health: simctl-health-check`, targetUdid ? `• Check simulator state: simctl-device --operation boot --deviceId ${targetUdid}` : undefined, ].filter(Boolean), }; return { content: [{ type: 'text' as const, text: JSON.stringify(responseData, null, 2) }], isError: true, }; } } /** * Workflow Fresh Install documentation for RTFM */ export const WORKFLOW_FRESH_INSTALL_DOCS = ` # workflow-fresh-install Clean slate app installation - build, install, and launch with fresh simulator state. ## Overview Orchestrates a complete clean installation cycle in a single call: 1. **Select Simulator** - Auto-detect or use specified device 2. **Shutdown** - Ensure simulator is stopped 3. **Erase** (optional) - Wipe all simulator data 4. **Boot** - Start fresh simulator 5. **Build** - Compile the Xcode project 6. **Install** - Install the built app 7. **Launch** - Start the app This workflow keeps intermediate results internal, reducing agent context usage by ~70% compared to calling each tool manually. ## Parameters ### Required - **projectPath** (string): Path to .xcodeproj or .xcworkspace - **scheme** (string): Build scheme name ### Optional - **simulatorUdid** (string): Target simulator - auto-detected if omitted - **eraseSimulator** (boolean): Wipe simulator data before install (default: false) - **configuration** ("Debug" | "Release"): Build configuration (default: Debug) - **launchArguments** (string[]): App launch arguments - **environmentVariables** (Record<string, string>): App environment variables ## Returns Consolidated result with: - **success**: Overall workflow success - **project**: Build configuration details - **simulator**: Target simulator info - **app**: Installed app details (bundleId, path, launched) - **totalDuration**: Total workflow time - **guidance**: Next steps ## Examples ### Basic Fresh Install \`\`\`json { "projectPath": "/path/to/MyApp.xcodeproj", "scheme": "MyApp" } \`\`\` Auto-selects simulator, builds, installs, and launches. ### Clean Install with Erased Simulator \`\`\`json { "projectPath": "/path/to/MyApp.xcworkspace", "scheme": "MyApp", "eraseSimulator": true, "configuration": "Debug" } \`\`\` Erases all simulator data for truly fresh state. ### Specific Simulator with Launch Arguments \`\`\`json { "projectPath": "/path/to/MyApp.xcodeproj", "scheme": "MyApp", "simulatorUdid": "ABC123-DEF456", "launchArguments": ["-UITesting", "-ResetState"], "environmentVariables": {"DEBUG_MODE": "1"} } \`\`\` Targets specific simulator with custom launch configuration. ## Why Use This Workflow? ### Token Efficiency - **Manual approach**: 6-7 tool calls × ~100 tokens each = ~600+ tokens in responses - **Workflow approach**: 1 call with consolidated response = ~150 tokens ### Reduced Context Pollution - Build logs not exposed (only success/failure) - Intermediate states summarized - Only actionable outcome returned ### Consistent State - Shutdown ensures clean starting point - Optional erase for truly fresh state - Proper boot sequencing ## Related Tools - **workflow-tap-element**: UI interaction after install - **xcodebuild-build**: Direct build (used internally) - **simctl-device**: Direct simulator control (used internally) - **simctl-app**: Direct app management (used internally) ## Notes - Shutdown failures are non-fatal (simulator may already be off) - Auto-suggests best simulator based on project requirements - Build artifacts are located automatically - Bundle ID is discovered from build settings `; export const WORKFLOW_FRESH_INSTALL_DOCS_MINI = 'Build, install and launch app on fresh simulator. Use rtfm({ toolName: "workflow-fresh-install" }) for docs.';

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/conorluddy/xc-mcp'

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