Skip to main content
Glama

Xcode MCP Server

by r-huijts
index.tsโ€ข33.8 kB
import { z } from "zod"; import { promisify } from "util"; import { exec } from "child_process"; import * as path from "path"; import * as fs from "fs/promises"; import { XcodeServer } from "../../server.js"; import { CommandExecutionError, PathAccessError, FileOperationError } from "../../utils/errors.js"; const execAsync = promisify(exec); /** * Get available Xcode versions on the system */ async function getXcodeVersions(): Promise<{path: string, version: string, build: string, isDefault: boolean}[]> { try { const { stdout } = await execAsync('mdfind "kMDItemCFBundleIdentifier == com.apple.dt.Xcode"'); const paths = stdout.trim().split('\n').filter(Boolean); const versions = []; const defaultPath = await getDefaultXcodePath(); for (const path of paths) { try { const { stdout: versionOutput } = await execAsync(`/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "${path}/Contents/Info.plist"`); const { stdout: buildOutput } = await execAsync(`/usr/libexec/PlistBuddy -c "Print :DTXcode" "${path}/Contents/Info.plist"`); versions.push({ path, version: versionOutput.trim(), build: buildOutput.trim(), isDefault: path === defaultPath }); } catch { // Skip if we can't get version info } } return versions; } catch (error) { console.error("Error getting Xcode versions:", error); return []; } } /** * Get the path to the default Xcode installation */ async function getDefaultXcodePath(): Promise<string> { try { const { stdout } = await execAsync('xcode-select -p'); // xcode-select returns the Developer directory, we need to go up two levels return path.dirname(path.dirname(stdout.trim())); } catch (error) { console.error("Error getting default Xcode path:", error); return ""; } } /** * Register Xcode-related tools */ export function registerXcodeTools(server: XcodeServer) { // Register "run_xcrun" server.server.tool( "run_xcrun", "Executes a specified Xcode tool via xcrun", { tool: z.string().describe("The name of the Xcode tool to run"), args: z.string().optional().describe("Arguments to pass to the tool"), workingDir: z.string().optional().describe("Working directory to execute the command in") }, async ({ tool, args = "", workingDir }) => { try { let options = {}; if (workingDir) { // Validate and resolve the working directory path const resolvedWorkingDir = server.pathManager.normalizePath(workingDir); server.pathManager.validatePathForReading(resolvedWorkingDir); options = { cwd: resolvedWorkingDir }; } const { stdout, stderr } = await execAsync(`xcrun ${tool} ${args}`, options); return { content: [{ type: "text", text: `${tool} output:\n${stdout}\n${stderr ? 'Error output:\n' + stderr : ''}` }] }; } catch (error) { let stderr = ''; if (error instanceof Error && 'stderr' in error) { stderr = (error as any).stderr; } if (error instanceof PathAccessError) { throw new Error(`Access denied: ${error.message}`); } throw new CommandExecutionError( `xcrun ${tool}`, stderr || (error instanceof Error ? error.message : String(error)) ); } } ); // Register "compile_asset_catalog" server.server.tool( "compile_asset_catalog", "Compiles an asset catalog (.xcassets) using actool", { catalogPath: z.string().describe("Path to the asset catalog (.xcassets directory)"), outputDir: z.string().describe("Directory where compiled assets should be placed"), platform: z.enum(["iphoneos", "iphonesimulator", "macosx", "watchos"]).optional().describe("Target platform (default: iphoneos)"), minDeploymentTarget: z.string().optional().describe("Minimum deployment target version (default: 14.0)"), appIcon: z.string().optional().describe("Name of the app icon set to include"), targetDevices: z.array(z.string()).optional().describe("Target devices (default: ['iphone', 'ipad'])") }, async ({ catalogPath, outputDir, platform = "iphoneos", minDeploymentTarget = "14.0", appIcon, targetDevices = ["iphone", "ipad"] }) => { try { // Validate paths for security const resolvedCatalogPath = server.pathManager.normalizePath(catalogPath); server.pathManager.validatePathForReading(resolvedCatalogPath); const resolvedOutputDir = server.pathManager.normalizePath(outputDir); server.pathManager.validatePathForWriting(resolvedOutputDir); // Ensure the catalog path ends with .xcassets if (!resolvedCatalogPath.endsWith('.xcassets')) { throw new Error(`Asset catalog path must end with .xcassets: ${catalogPath}`); } // Ensure the catalog exists try { const stats = await fs.stat(resolvedCatalogPath); if (!stats.isDirectory()) { throw new Error(`Asset catalog path is not a directory: ${catalogPath}`); } } catch (error) { throw new Error(`Asset catalog not found: ${catalogPath}`); } // Ensure the output directory exists try { await fs.mkdir(resolvedOutputDir, { recursive: true }); } catch (error) { throw new FileOperationError(`Failed to create output directory: ${outputDir}`, String(error)); } // Build the command with validated paths let cmd = `xcrun actool "${resolvedCatalogPath}" --output-format human-readable-text --notices --warnings`; // Add platform cmd += ` --platform ${platform}`; // Add deployment target cmd += ` --minimum-deployment-target ${minDeploymentTarget}`; // Add target devices for (const device of targetDevices) { cmd += ` --target-device ${device}`; } // Add app icon if specified if (appIcon) { cmd += ` --app-icon ${appIcon}`; } // Add output paths const plistPath = path.join(resolvedOutputDir, 'assetcatalog_generated_info.plist'); cmd += ` --output-partial-info-plist "${plistPath}" --compress-pngs --compile "${resolvedOutputDir}"`; const { stdout, stderr } = await execAsync(cmd); return { content: [{ type: "text", text: `Asset catalog compilation:\n${stdout}\n${stderr ? 'Error output:\n' + stderr : ''}\n\nAssets compiled to: ${resolvedOutputDir}` }] }; } catch (error) { if (error instanceof PathAccessError) { throw new Error(`Access denied: ${error.message}`); } else if (error instanceof FileOperationError) { throw new Error(`File operation error: ${error.message}`); } else { let stderr = ''; if (error instanceof Error && 'stderr' in error) { stderr = (error as any).stderr; } throw new CommandExecutionError( 'xcrun actool', stderr || (error instanceof Error ? error.message : String(error)) ); } } } ); // Register "run_lldb" server.server.tool( "run_lldb", "Launches the LLDB debugger with optional arguments", { args: z.string().optional().describe("Arguments to pass to lldb"), command: z.string().optional().describe("Single LLDB command to execute") }, async ({ args = "", command }) => { try { let cmd = `xcrun lldb ${args}`; // If a single command is provided, execute it and exit if (command) { cmd += ` -o "${command}" -b`; // -b for batch mode (exit after running commands) } const { stdout, stderr } = await execAsync(cmd); return { content: [{ type: "text", text: `LLDB output:\n${stdout}\n${stderr ? 'Error output:\n' + stderr : ''}` }] }; } catch (error) { let stderr = ''; if (error instanceof Error && 'stderr' in error) { stderr = (error as any).stderr; } throw new CommandExecutionError( 'xcrun lldb', stderr || (error instanceof Error ? error.message : String(error)) ); } } ); // Register "trace_app" server.server.tool( "trace_app", "Captures a performance trace of an application using xctrace", { appPath: z.string().describe("Path to the application to trace"), duration: z.number().optional().describe("Duration of the trace in seconds (default: 10)"), template: z.string().optional().describe("Trace template to use (default: 'Time Profiler')"), outputPath: z.string().optional().describe("Path where to save the trace file (default: app_trace.trace in active directory)"), startSuspended: z.boolean().optional().describe("Start the application in a suspended state") }, async ({ appPath, duration = 10, template = "Time Profiler", outputPath, startSuspended = false }) => { try { // Validate app path const resolvedAppPath = server.pathManager.normalizePath(appPath); server.pathManager.validatePathForReading(resolvedAppPath); // Determine output path const activeDirectory = server.directoryState.getActiveDirectory(); const resolvedOutputPath = outputPath ? server.pathManager.normalizePath(outputPath) : path.join(activeDirectory, "app_trace.trace"); // Make sure we can write to the output path server.pathManager.validatePathForWriting(resolvedOutputPath); // Ensure output directory exists const outputDir = path.dirname(resolvedOutputPath); await fs.mkdir(outputDir, { recursive: true }); // Build the command let cmd = `xcrun xctrace record --template "${template}" --time-limit ${duration}s --output "${resolvedOutputPath}"`; if (startSuspended) { cmd += ' --launch-suspended'; } cmd += ` --launch -- "${resolvedAppPath}"`; const { stdout, stderr } = await execAsync(cmd); return { content: [{ type: "text", text: `XCTrace output:\n${stdout}\n${stderr ? 'Error output:\n' + stderr : ''}\nTrace saved to: ${resolvedOutputPath}` }] }; } catch (error) { if (error instanceof PathAccessError) { throw new Error(`Access denied: ${error.message}`); } else { let stderr = ''; if (error instanceof Error && 'stderr' in error) { stderr = (error as any).stderr; } throw new CommandExecutionError( 'xcrun xctrace', stderr || (error instanceof Error ? error.message : String(error)) ); } } } ); // Register "get_xcode_info" server.server.tool( "get_xcode_info", "Get information about Xcode installations on the system", {}, async () => { try { const xcodeVersions = await getXcodeVersions(); const defaultPath = await getDefaultXcodePath(); let formattedOutput = "Xcode Installations:\n\n"; if (xcodeVersions.length === 0) { formattedOutput += "No Xcode installations found.\n"; } else { xcodeVersions.forEach(xcode => { formattedOutput += `${xcode.isDefault ? '* ' : ' '}Xcode ${xcode.version} (${xcode.build})\n`; formattedOutput += ` Path: ${xcode.path}\n\n`; }); formattedOutput += "* = Active Xcode installation\n\n"; formattedOutput += `Active Xcode Developer Path: ${defaultPath}/Contents/Developer\n`; } // Add SDK information try { // Get iOS SDK information const { stdout: iOSSDKOutput } = await execAsync('xcrun --show-sdk-path --sdk iphoneos'); formattedOutput += `\niOS SDK Path: ${iOSSDKOutput.trim()}\n`; const { stdout: iOSSDKVersionOutput } = await execAsync('xcrun --show-sdk-version --sdk iphoneos'); formattedOutput += `iOS SDK Version: ${iOSSDKVersionOutput.trim()}\n`; // Get macOS SDK information try { const { stdout: macOSSDKOutput } = await execAsync('xcrun --show-sdk-path --sdk macosx'); formattedOutput += `\nmacOS SDK Path: ${macOSSDKOutput.trim()}\n`; const { stdout: macOSSDKVersionOutput } = await execAsync('xcrun --show-sdk-version --sdk macosx'); formattedOutput += `macOS SDK Version: ${macOSSDKVersionOutput.trim()}\n`; } catch { // Ignore if macOS SDK info can't be retrieved } // Get watchOS SDK information try { const { stdout: watchOSSDKOutput } = await execAsync('xcrun --show-sdk-path --sdk watchos'); formattedOutput += `\nwatchOS SDK Path: ${watchOSSDKOutput.trim()}\n`; const { stdout: watchOSSDKVersionOutput } = await execAsync('xcrun --show-sdk-version --sdk watchos'); formattedOutput += `watchOS SDK Version: ${watchOSSDKVersionOutput.trim()}\n`; } catch { // Ignore if watchOS SDK info can't be retrieved } // Get tvOS SDK information try { const { stdout: tvOSSDKOutput } = await execAsync('xcrun --show-sdk-path --sdk appletvos'); formattedOutput += `\ntvOS SDK Path: ${tvOSSDKOutput.trim()}\n`; const { stdout: tvOSSDKVersionOutput } = await execAsync('xcrun --show-sdk-version --sdk appletvos'); formattedOutput += `tvOS SDK Version: ${tvOSSDKVersionOutput.trim()}\n`; } catch { // Ignore if tvOS SDK info can't be retrieved } } catch { formattedOutput += "\nCould not determine SDK information\n"; } return { content: [{ type: "text", text: formattedOutput }] }; } catch (error) { let errorMessage = "Failed to retrieve Xcode information"; if (error instanceof Error) { errorMessage += `: ${error.message}`; } throw new Error(errorMessage); } } ); // Register "switch_xcode" server.server.tool( "switch_xcode", "Switch the active Xcode version", { xcodePath: z.string().optional().describe("Path to the Xcode.app to use. If not provided, available Xcode installations will be listed."), version: z.string().optional().describe("Version of Xcode to use (e.g., '14.3'). Will use the first matching version found.") }, async ({ xcodePath, version }) => { try { // Get available Xcode versions const xcodeVersions = await getXcodeVersions(); if (xcodeVersions.length === 0) { throw new Error("No Xcode installations found on this system."); } // If neither xcodePath nor version is provided, just list available versions if (!xcodePath && !version) { let formattedOutput = "Available Xcode installations:\n\n"; xcodeVersions.forEach((xcode, index) => { formattedOutput += `${index + 1}. Xcode ${xcode.version} (${xcode.build})${xcode.isDefault ? ' (active)' : ''}\n`; formattedOutput += ` Path: ${xcode.path}\n\n`; }); formattedOutput += "To switch Xcode version, use this tool with either 'path' or 'version' parameter."; return { content: [{ type: "text", text: formattedOutput }] }; } // Find the Xcode to switch to let targetXcode; if (xcodePath) { // Normalize and validate the path const resolvedPath = server.pathManager.normalizePath(xcodePath); // Find by path targetXcode = xcodeVersions.find(xcode => xcode.path === resolvedPath); if (!targetXcode) { throw new Error(`No Xcode installation found at path: ${resolvedPath}`); } } else if (version) { // Find by version targetXcode = xcodeVersions.find(xcode => xcode.version === version); if (!targetXcode) { throw new Error(`No Xcode installation found with version: ${version}`); } } // Check if already active if (targetXcode && targetXcode.isDefault) { return { content: [{ type: "text", text: `Xcode ${targetXcode.version} (${targetXcode.build}) is already the active Xcode version.` }] }; } // Switch Xcode version using xcode-select if (!targetXcode) { throw new Error('No target Xcode version found'); } // Use the path module // (already imported at the top of the file) const developerDir = path.join(targetXcode.path, 'Contents', 'Developer'); const { stdout, stderr } = await execAsync(`sudo xcode-select --switch "${developerDir}"`); return { content: [{ type: "text", text: `Successfully switched to Xcode ${targetXcode.version} (${targetXcode.build})\n${stdout}${stderr ? '\nConsole output:\n' + stderr : ''}` }] }; } catch (error) { let errorMessage = "Failed to switch Xcode version"; if (error instanceof Error) { if (error.message.includes("sudo") || error.message.includes("password")) { errorMessage = "Switching Xcode versions requires sudo privileges. Please run the command manually in your terminal:\n\n" + `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer`; } else { errorMessage += `: ${error.message}`; } } throw new Error(errorMessage); } } ); // Register "export_archive" server.server.tool( "export_archive", "Export an Xcode archive for distribution (App Store, Ad Hoc, Enterprise, Development)", { archivePath: z.string().describe("Path to the .xcarchive file to export"), exportPath: z.string().describe("Directory where exported IPA and other files will be placed"), method: z.enum(["app-store", "ad-hoc", "enterprise", "development"]).describe("Distribution method"), teamId: z.string().optional().describe("Team ID for code signing. If not provided, will try to use the default team."), signingCertificate: z.string().optional().describe("Signing certificate to use. If not provided, will try to use the default certificate for the selected method."), provisioningProfiles: z.record(z.string()).optional().describe("Dictionary mapping bundle identifiers to provisioning profile names."), compileBitcode: z.boolean().optional().describe("Whether to compile Bitcode. Default is true for App Store, false otherwise."), stripSwiftSymbols: z.boolean().optional().describe("Whether to strip Swift symbols. Default is true.") }, async ({ archivePath, exportPath, method, teamId, signingCertificate, provisioningProfiles, compileBitcode, stripSwiftSymbols }) => { try { // Validate paths const resolvedArchivePath = server.pathManager.normalizePath(archivePath); server.pathManager.validatePathForReading(resolvedArchivePath); const resolvedExportPath = server.pathManager.normalizePath(exportPath); server.pathManager.validatePathForWriting(resolvedExportPath); // Ensure the archive exists try { const stats = await fs.stat(resolvedArchivePath); if (!stats.isDirectory()) { throw new Error(`Archive path is not a directory: ${archivePath}`); } // Check if it's a valid .xcarchive if (!resolvedArchivePath.endsWith('.xcarchive')) { throw new Error(`Archive path must end with .xcarchive: ${archivePath}`); } } catch (error) { throw new Error(`Archive not found: ${archivePath}`); } // Create the export directory if it doesn't exist await fs.mkdir(resolvedExportPath, { recursive: true }); // Create a temporary exportOptions.plist file const tempDir = path.join(resolvedExportPath, 'temp_export_options'); await fs.mkdir(tempDir, { recursive: true }); const exportOptionsPath = path.join(tempDir, 'exportOptions.plist'); // Build the export options plist content let exportOptionsContent = `<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> \t<key>method</key> \t<string>${method}</string> `; // Add team ID if provided if (teamId) { exportOptionsContent += `\t<key>teamID</key> \t<string>${teamId}</string> `; } // Add signing certificate if provided if (signingCertificate) { exportOptionsContent += `\t<key>signingCertificate</key> \t<string>${signingCertificate}</string> `; } // Add compileBitcode option if provided if (compileBitcode !== undefined) { exportOptionsContent += `\t<key>compileBitcode</key> \t<${compileBitcode ? 'true' : 'false'}/> `; } // Add stripSwiftSymbols option if provided if (stripSwiftSymbols !== undefined) { exportOptionsContent += `\t<key>stripSwiftSymbols</key> \t<${stripSwiftSymbols ? 'true' : 'false'}/> `; } // Add provisioning profiles if provided if (provisioningProfiles && Object.keys(provisioningProfiles).length > 0) { exportOptionsContent += `\t<key>provisioningProfiles</key> \t<dict> `; for (const [bundleId, profileName] of Object.entries(provisioningProfiles)) { exportOptionsContent += `\t\t<key>${bundleId}</key> \t\t<string>${profileName}</string> `; } exportOptionsContent += `\t</dict> `; } // Close the plist exportOptionsContent += `</dict> </plist> `; // Write the export options plist await fs.writeFile(exportOptionsPath, exportOptionsContent, 'utf-8'); // Run the export command const cmd = `xcrun xcodebuild -exportArchive -archivePath "${resolvedArchivePath}" -exportOptionsPlist "${exportOptionsPath}" -exportPath "${resolvedExportPath}"`; const { stdout, stderr } = await execAsync(cmd); // Clean up the temporary directory try { await fs.rm(tempDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } // Try to find the exported IPA file let ipaPath = ""; try { const files = await fs.readdir(resolvedExportPath); const ipaFile = files.find(file => file.endsWith('.ipa')); if (ipaFile) { ipaPath = path.join(resolvedExportPath, ipaFile); } } catch { // Ignore errors finding IPA } return { content: [{ type: "text", text: `Archive exported successfully to ${resolvedExportPath}\n` + (ipaPath ? `IPA file: ${ipaPath}\n\n` : "\n") + `Export method: ${method}\n\n` + `Export log:\n${stdout}\n${stderr ? 'Error output:\n' + stderr : ''}` }] }; } catch (error) { if (error instanceof PathAccessError) { throw new Error(`Access denied: ${error.message}`); } else { let stderr = ''; if (error instanceof Error && 'stderr' in error) { stderr = (error as any).stderr; } // Check for common export errors if (stderr) { if (stderr.includes("No signing certificate")) { throw new Error("Export failed: No signing certificate found. Make sure you have a valid signing certificate installed."); } if (stderr.includes("requires a provisioning profile")) { throw new Error("Export failed: Missing provisioning profile. Make sure you have a valid provisioning profile for your app."); } if (stderr.includes("doesn't match the identity")) { throw new Error("Export failed: Provisioning profile doesn't match the signing identity. Check your team ID and certificate."); } } throw new Error( `Failed to export archive: ${stderr || (error instanceof Error ? error.message : String(error))}` ); } } } ); // Register "validate_app" server.server.tool( "validate_app", "Validate an app for App Store submission using altool", { ipaPath: z.string().describe("Path to the .ipa file to validate"), username: z.string().describe("App Store Connect username (usually an email)"), password: z.string().describe("App-specific password for the App Store Connect account"), apiKey: z.string().optional().describe("API Key ID (alternative to username/password)"), apiIssuer: z.string().optional().describe("API Key Issuer ID (required if using apiKey)"), apiKeyPath: z.string().optional().describe("Path to the API Key .p8 file (required if using apiKey)") }, async ({ ipaPath, username, password, apiKey, apiIssuer, apiKeyPath }) => { try { // Validate paths const resolvedIpaPath = server.pathManager.normalizePath(ipaPath); server.pathManager.validatePathForReading(resolvedIpaPath); // Ensure the IPA exists try { const stats = await fs.stat(resolvedIpaPath); if (!stats.isFile()) { throw new Error(`IPA path is not a file: ${ipaPath}`); } // Check if it's a valid .ipa if (!resolvedIpaPath.endsWith('.ipa')) { throw new Error(`IPA path must end with .ipa: ${ipaPath}`); } } catch (error) { throw new Error(`IPA file not found: ${ipaPath}`); } // Validate API Key path if provided if (apiKey && apiKeyPath) { const resolvedApiKeyPath = server.pathManager.normalizePath(apiKeyPath); server.pathManager.validatePathForReading(resolvedApiKeyPath); try { const stats = await fs.stat(resolvedApiKeyPath); if (!stats.isFile()) { throw new Error(`API Key path is not a file: ${apiKeyPath}`); } } catch (error) { throw new Error(`API Key file not found: ${apiKeyPath}`); } } // Build the validation command let cmd; if (apiKey && apiIssuer && apiKeyPath) { // Use API Key authentication const resolvedApiKeyPath = server.pathManager.normalizePath(apiKeyPath); cmd = `xcrun altool --validate-app -f "${resolvedIpaPath}" --apiKey "${apiKey}" --apiIssuer "${apiIssuer}" --api-key-path "${resolvedApiKeyPath}" --type ios`; } else { // Use username/password authentication cmd = `xcrun altool --validate-app -f "${resolvedIpaPath}" -u "${username}" -p "${password}" --type ios`; } const { stdout, stderr } = await execAsync(cmd); // Check for validation success if (stdout.includes("No errors validating")) { return { content: [{ type: "text", text: `App validation successful! The app is ready for submission to the App Store.\n\nValidation output:\n${stdout}\n${stderr ? 'Error output:\n' + stderr : ''}` }] }; } else { // There were validation issues return { content: [{ type: "text", text: `App validation completed with issues. Please review the validation output:\n\n${stdout}\n${stderr ? 'Error output:\n' + stderr : ''}` }] }; } } catch (error) { let stderr = ''; if (error instanceof Error && 'stderr' in error) { stderr = (error as any).stderr; } // Check for common validation errors if (stderr) { if (stderr.includes("Invalid username and password")) { throw new Error("Validation failed: Invalid username and password. Make sure your App Store Connect credentials are correct."); } if (stderr.includes("Unable to validate archive")) { throw new Error("Validation failed: Unable to validate archive. The IPA file might be corrupted or not properly signed."); } if (stderr.includes("API Key not found")) { throw new Error("Validation failed: API Key not found. Make sure the API Key ID and path are correct."); } } throw new Error( `Failed to validate app: ${stderr || (error instanceof Error ? error.message : String(error))}` ); } } ); // Register "generate_icon_set" server.server.tool( "generate_icon_set", "Generate an app icon set from a source image", { sourceImage: z.string().describe("Path to the source image (should be at least 1024x1024)"), outputPath: z.string().describe("Path where to create the AppIcon.appiconset directory"), platform: z.enum(["ios", "macos", "watchos"]).optional().describe("Target platform (default: ios)") }, async ({ sourceImage, outputPath, platform = "ios" }) => { try { // Validate paths const resolvedSourceImage = server.pathManager.normalizePath(sourceImage); server.pathManager.validatePathForReading(resolvedSourceImage); const resolvedOutputPath = server.pathManager.normalizePath(outputPath); server.pathManager.validatePathForWriting(resolvedOutputPath); // Ensure the source image exists try { const stats = await fs.stat(resolvedSourceImage); if (!stats.isFile()) { throw new Error(`Source image is not a file: ${sourceImage}`); } } catch (error) { throw new Error(`Source image not found: ${sourceImage}`); } // Create the AppIcon.appiconset directory const iconsetPath = path.join(resolvedOutputPath, "AppIcon.appiconset"); await fs.mkdir(iconsetPath, { recursive: true }); // Define icon sizes based on platform const iconSizes = { ios: [ { size: 20, scales: [1, 2, 3] }, { size: 29, scales: [1, 2, 3] }, { size: 40, scales: [1, 2, 3] }, { size: 60, scales: [2, 3] }, { size: 76, scales: [1, 2] }, { size: 83.5, scales: [2] }, { size: 1024, scales: [1] } // App Store ], macos: [ { size: 16, scales: [1, 2] }, { size: 32, scales: [1, 2] }, { size: 64, scales: [1, 2] }, { size: 128, scales: [1, 2] }, { size: 256, scales: [1, 2] }, { size: 512, scales: [1, 2] }, { size: 1024, scales: [1] } ], watchos: [ { size: 24, scales: [2] }, { size: 27.5, scales: [2] }, { size: 29, scales: [2, 3] }, { size: 40, scales: [2] }, { size: 44, scales: [2] }, { size: 50, scales: [2] }, { size: 86, scales: [2] }, { size: 98, scales: [2] }, { size: 108, scales: [2] }, { size: 1024, scales: [1] } ] }; // Create Contents.json const contentsJson = { images: [], info: { version: 1, author: "xcode-mcp-server" } }; // Generate the resize commands and add to Contents.json for (const { size, scales } of iconSizes[platform]) { for (const scale of scales) { const pixelSize = Math.round(size * scale); const filename = `icon_${size}x${size}@${scale}x.png`; const outputFilePath = path.join(iconsetPath, filename); // Generate the resized image const resizeCmd = `sips -Z ${pixelSize} "${resolvedSourceImage}" --out "${outputFilePath}"`; await execAsync(resizeCmd); // Add to Contents.json (contentsJson.images as any[]).push({ size: `${size}x${size}`, idiom: platform === "macos" ? "mac" : platform === "watchos" ? "watch" : "iphone", filename, scale: `${scale}x` }); } } // Write Contents.json await fs.writeFile( path.join(iconsetPath, "Contents.json"), JSON.stringify(contentsJson, null, 2), "utf-8" ); return { content: [{ type: "text", text: `App icon set generated successfully at ${iconsetPath}\n\nThe icon set includes ${(contentsJson.images as any[]).length} icon sizes for ${platform} platform.` }] }; } catch (error) { if (error instanceof PathAccessError) { throw new Error(`Access denied: ${error.message}`); } else { let stderr = ''; if (error instanceof Error && 'stderr' in error) { stderr = (error as any).stderr; } throw new Error( `Failed to generate icon set: ${stderr || (error instanceof Error ? error.message : String(error))}` ); } } } ); }

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/r-huijts/xcode-mcp-server'

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