Skip to main content
Glama
devyhan
by devyhan

run-on-device

Build and run Xcode projects on iOS devices or simulators by specifying project paths, schemes, and target devices. Use this tool to deploy apps for testing and debugging with configurable build settings and environment variables.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
projectPathYesXcode 프로젝트 또는 워크스페이스 경로
schemeYes빌드 및 실행할 스킴
deviceYes기기 식별자 또는 이름 (한글 이름 지원)
configurationNo빌드 구성 (Debug/Release)
streamLogsNo앱 실행 후 로그 스트리밍 여부
startStoppedNo디버거 연결을 위한 일시 중지 상태로 시작
environmentVarsNo앱에 전달할 환경 변수 (key1=value1,key2=value2 형식)
xcodePathNoXcode 애플리케이션 경로
listDevicesNo실행 전 감지된 모든 디바이스 목록 표시
skipBuildNo이미 설치된 앱을 재실행할 때 빌드 및 설치 건너뛰기
extraLaunchArgsNodevicectl launch 명령어에 전달할 추가 인자
directBundleIdNo직접 지정할 번들 ID (프로젝트에서 추출하지 않음)

Implementation Reference

  • Complete inline handler implementation for the 'run-on-device' MCP tool. Handles parameter validation implicitly via Zod, device discovery and identification (Xcode UDID and devicectl UUID), bundle ID extraction, optional app build and install via xcodebuild, app launch using devicectl process launch with environment variables and debug options, automatic reinstall if app not found, and optional real-time log streaming.
    server.tool( "run-on-device", { projectPath: z.string().describe("Xcode 프로젝트 또는 워크스페이스 경로"), scheme: z.string().describe("빌드 및 실행할 스킴"), device: z.string().describe("기기 식별자 또는 이름 (한글 이름 지원)"), configuration: z.string().optional().describe("빌드 구성 (Debug/Release)"), streamLogs: z.boolean().optional().describe("앱 실행 후 로그 스트리밍 여부"), startStopped: z.boolean().optional().describe("디버거 연결을 위한 일시 중지 상태로 시작"), environmentVars: z.string().optional().describe("앱에 전달할 환경 변수 (key1=value1,key2=value2 형식)"), xcodePath: z.string().optional().describe("Xcode 애플리케이션 경로"), listDevices: z.boolean().optional().describe("실행 전 감지된 모든 디바이스 목록 표시"), skipBuild: z.boolean().optional().describe("이미 설치된 앱을 재실행할 때 빌드 및 설치 건너뛰기"), extraLaunchArgs: z.array(z.string()).optional().describe("devicectl launch 명령어에 전달할 추가 인자"), directBundleId: z.string().optional().describe("직접 지정할 번들 ID (프로젝트에서 추출하지 않음)") }, async ({ projectPath, scheme, device, configuration = "Debug", streamLogs = false, startStopped = false, environmentVars = "", xcodePath, listDevices = false, skipBuild = false, extraLaunchArgs = [], directBundleId }) => { try { console.error(`실제 기기에서 앱 실행 준비: ${projectPath}, 스킴: ${scheme}, 기기: ${device}`); // 0. 디바이스 목록 표시 (요청된 경우) if (listDevices) { // 캩0싱 무시하고 강제로 최신 디바이스 목록 가져오기 const allDevices = await getAllDevices(true); let deviceListText = "감지된 디바이스 목록:\n"; allDevices.forEach(d => { deviceListText += `- ${d.name}\n`; if (d.xcodeId) deviceListText += ` Xcode ID: ${d.xcodeId}\n`; if (d.deviceCtlId) deviceListText += ` DeviceCtl ID: ${d.deviceCtlId}\n`; deviceListText += ` 상태: ${d.isAvailable ? '사용 가능' : '사용 불가'}\n`; if (d.model) deviceListText += ` 모델: ${d.model}\n`; if (d.osVersion) deviceListText += ` OS 버전: ${d.osVersion}\n`; deviceListText += "\n"; }); return { content: [{ type: "text", text: deviceListText }] }; } // 1. 자동으로 Xcode 경로 찾기 (xcodePathw가 지정되지 않은 경우) if (!xcodePath) { const installations = await findXcodeInstallations(); if (installations.length > 0) { xcodePath = installations[0].path; console.error(`자동 감지된 Xcode 경로 사용: ${xcodePath}`); } } // 2. 디바이스 정보 찾기 const deviceInfo = await findDeviceInfo(device); console.error(`디바이스 정보: ${JSON.stringify(deviceInfo)}`); if (!deviceInfo.xcodeId) { return { content: [{ type: "text", text: `디바이스 '${deviceInfo.name}'의 Xcode 식별자(UDID)를 찾을 수 없습니다. 디바이스가 올바르게 연결되어 있는지 확인하세요.` }], isError: true }; } if (!deviceInfo.deviceCtlId) { return { content: [{ type: "text", text: `디바이스 '${deviceInfo.name}'의 CoreDevice 식별자(UUID)를 찾을 수 없습니다. 디바이스가 올바르게 연결되어 있는지 확인하세요.` }], isError: true }; } const xcodeDeviceId = deviceInfo.xcodeId; const deviceCtlId = deviceInfo.deviceCtlId; console.error(`Xcode 식별자: ${xcodeDeviceId}`); console.error(`DeviceCtl 식별자: ${deviceCtlId}`); // 기기 추가 정보 표시 if (deviceInfo.model) { console.error(`기기 모델: ${deviceInfo.model}`); } if (deviceInfo.osVersion) { console.error(`기기 OS 버전: ${deviceInfo.osVersion}`); } // 3. 번들 ID 가져오기 let bundleId: string; if (directBundleId) { console.error(`사용자 지정 번들 ID 사용: ${directBundleId}`); bundleId = directBundleId; } else { console.error(`번들 ID 조회 중...`); bundleId = await getBundleIdentifier(projectPath, scheme); console.error(`번들 ID: ${bundleId}`); } // 4. devicectl 경로 찾기 const deviceCtlPath = await getDeviceCtlPath(xcodePath); console.error(`devicectl 경로: ${deviceCtlPath}`); // 5. 앱 출력 경로 추정 (skipBuild = false만 사용) let appPath: string | undefined; if (!skipBuild && !directBundleId) { try { // 빌드 서적으로부터 앱 경로 추정 const derivedDataCmd = `xcodebuild -project "${projectPath}" -scheme "${scheme}" -showBuildSettings | grep CONFIGURATION_BUILD_DIR`; const { stdout: derivedDataOutput } = await executeCommand(derivedDataCmd); const match = derivedDataOutput.match(/CONFIGURATION_BUILD_DIR = (.*)/); if (match && match[1]) { const buildDir = match[1].trim(); appPath = `${buildDir}/${scheme}.app`; console.error(`추정된 앱 경로: ${appPath}`); } } catch (pathError) { console.error(`앱 경로 추정 실패, 자동 재설치 기능을 사용할 수 없습니다: ${pathError}`); } } // 6. 빌드 및 설치 (선택적) if (!skipBuild && !directBundleId) { console.error(`앱 빌드 및 설치 시작...`); await buildAndInstallApp(projectPath, scheme, xcodeDeviceId, configuration); console.error(`앱 빌드 및 설치 완료`); } else { console.error(`빌드 및 설치 과정 건너뛰기`); } // 7. 환경 변수 파싱 let envVars: Record<string, string> = {}; if (environmentVars) { environmentVars.split(',').forEach(pair => { const [key, value] = pair.split('='); if (key && value) { envVars[key] = value; } }); } // 8. 앱 실행 - 개선된 launchAppOnDevice 함수 사용 console.error(`앱 실행 시작...`); try { const launchResult = await launchAppOnDevice( deviceCtlId, bundleId, xcodePath, envVars, startStopped, extraLaunchArgs, appPath // 자동 재설치를 위한 앱 경로 (필요한 경우) ); let resultText = `앱 실행 결과:\n${launchResult}\n`; // 9. 로그 스트리밍 (옵션이 활성화된 경우) if (streamLogs) { resultText += "\n로그 스트리밍이 시작되었습니다. 로그는 터미널에서 확인할 수 있습니다.\n"; // 비동기적으로 로그 스트리밍 시작 startDeviceLogStream(deviceCtlId, bundleId, xcodePath); } return { content: [{ type: "text", text: resultText }] }; } catch (launchError: any) { // 실행 오류인 경우, 메시지 체크 if (launchError.message.includes('is not installed') && appPath) { try { // 앱이 설치되지 않은 경우 수동 설치 시도 console.error(`앱이 설치되지 않았습니다. 수동 설치 시도 중...`); // devicectl을 사용해 수동 설치 const installCmd = `"${deviceCtlPath}" device install app --device ${deviceCtlId} "${appPath}"`; console.error(`실행할 설치 명령어: ${installCmd}`); const { stdout: installOutput } = await executeCommand(installCmd); console.error(`설치 결과: ${installOutput}`); // 설치 후 다시 실행 시도 let retryLaunchCmd = `"${deviceCtlPath}" device process launch --device ${deviceCtlId}`; // 환경 변수 추가 if (Object.keys(envVars).length > 0) { const envString = Object.entries(envVars) .map(([key, value]) => `${key}=${value}`) .join(","); retryLaunchCmd += ` --environment-variables "${envString}"`; } // 중단 모드로 시작 (디버깅용) if (startStopped) { retryLaunchCmd += " --start-stopped"; } // 추가 인자 적용 if (extraLaunchArgs.length > 0) { retryLaunchCmd += " " + extraLaunchArgs.join(" "); } // 번들 ID 추가 retryLaunchCmd += ` ${bundleId}`; console.error(`재시도 명령어: ${retryLaunchCmd}`); const { stdout: retryOutput } = await executeCommand(retryLaunchCmd); let resultText = `앱 설치 및 실행 결과:\n설치: ${installOutput}\n실행: ${retryOutput}\n`; // 로그 스트리밍 (옵션이 활성화된 경우) if (streamLogs) { resultText += "\n로그 스트리밍이 시작되었습니다. 로그는 터미널에서 확인할 수 있습니다.\n"; startDeviceLogStream(deviceCtlId, bundleId, xcodePath); } return { content: [{ type: "text", text: resultText }] }; } catch (installError: any) { console.error(`수동 설치 실패: ${installError.message}`); return { content: [{ type: "text", text: `앱이 설치되지 않았고, 수동 설치도 실패했습니다:\n${installError.message}\n${installError.stderr || ''}` }], isError: true }; } } else { // 다른 오류인 경우 return { content: [{ type: "text", text: `실제 기기에서 앱 실행 중 오류가 발생했습니다:\n${launchError.message}\n${launchError.stderr || ''}` }], isError: true }; } } } catch (error: any) { console.error(`실제 기기에서 앱 실행 오류: ${error.message}`); return { content: [{ type: "text", text: `실제 기기에서 앱 실행 중 오류가 발생했습니다:\n${error.message}\n${error.stderr || ''}` }], isError: true }; } } );
  • Zod schema defining the input parameters for the 'run-on-device' tool, including projectPath, scheme, device (name or ID), configuration, streamLogs, startStopped, environmentVars, etc.
    { projectPath: z.string().describe("Xcode 프로젝트 또는 워크스페이스 경로"), scheme: z.string().describe("빌드 및 실행할 스킴"), device: z.string().describe("기기 식별자 또는 이름 (한글 이름 지원)"), configuration: z.string().optional().describe("빌드 구성 (Debug/Release)"), streamLogs: z.boolean().optional().describe("앱 실행 후 로그 스트리밍 여부"), startStopped: z.boolean().optional().describe("디버거 연결을 위한 일시 중지 상태로 시작"), environmentVars: z.string().optional().describe("앱에 전달할 환경 변수 (key1=value1,key2=value2 형식)"), xcodePath: z.string().optional().describe("Xcode 애플리케이션 경로"), listDevices: z.boolean().optional().describe("실행 전 감지된 모든 디바이스 목록 표시"), skipBuild: z.boolean().optional().describe("이미 설치된 앱을 재실행할 때 빌드 및 설치 건너뛰기"), extraLaunchArgs: z.array(z.string()).optional().describe("devicectl launch 명령어에 전달할 추가 인자"), directBundleId: z.string().optional().describe("직접 지정할 번들 ID (프로젝트에서 추출하지 않음)")
  • launchAppOnDevice: Key helper function that executes devicectl device process launch to run the app on physical device, supports env vars, start-stopped for debugging, additional args, and auto-installs .app if not present.
    export async function launchAppOnDevice( deviceId: string, bundleId: string, xcodePath?: string, environmentVars: Record<string, string> = {}, startStopped: boolean = false, additionalArgs: string[] = [], appPath?: string ): Promise<string> { try { console.error(`실제 기기에서 앱 실행: 기기 ${deviceId}, 번들 ID: ${bundleId}`); // devicectl 경로 찾기 const deviceCtlPath = await getDeviceCtlPath(xcodePath); console.error(`devicectl 경로: ${deviceCtlPath}`); // devicectl 명령 구성 let command = `"${deviceCtlPath}" device process launch --device ${deviceId}`; // 환경 변수 추가 if (Object.keys(environmentVars).length > 0) { const envString = Object.entries(environmentVars) .map(([key, value]) => `${key}=${value}`) .join(","); command += ` --environment-variables "${envString}"`; } // 중단 모드로 시작 (디버깅용) if (startStopped) { command += " --start-stopped"; } // 추가 인자 적용 if (additionalArgs.length > 0) { command += " " + additionalArgs.join(" "); } // 번들 ID 추가 command += ` ${bundleId}`; console.error(`실행할 명령어: ${command}`); try { const { stdout, stderr } = await executeCommand(command); if (stderr && stderr.trim() !== "") { console.error(`앱 실행 경고/오류: ${stderr}`); } console.error("앱 실행 명령 완료", stdout); return stdout; } catch (launchError: any) { // 앱이 설치되지 않은 경우 자동 설치 시도 if (launchError.message.includes('is not installed') && appPath) { console.error(`앱이 설치되지 않았습니다. 자동 설치 시도...`); // 앱 설치 시도 await installAppOnDevice(deviceId, appPath, xcodePath); // 설치 후 다시 실행 시도 console.error(`설치 완료. 앱 다시 실행 시도...`); const { stdout, stderr } = await executeCommand(command); if (stderr && stderr.trim() !== "") { console.error(`앱 실행 경고/오류 (재시도): ${stderr}`); } console.error("앱 실행 명령 완료 (재시도)", stdout); return stdout; } else { // 다른 오류일 경우 그대로 전파 throw launchError; } } } catch (error: any) { console.error(`앱 실행 오류: ${error.message}`); throw error; } }
  • buildAndInstallApp: Builds the Xcode project for the given scheme/configuration and installs directly to the physical device using xcodebuild build install.
    export async function buildAndInstallApp(projectPath: string, scheme: string, deviceId: string, configuration: string = "Debug"): Promise<void> { try { console.error(`앱 빌드 및 설치: ${projectPath}, 스킴: ${scheme}, 기기: ${deviceId}`); let command = `xcodebuild`; // 워크스페이스인지 프로젝트인지 확인 if (projectPath.endsWith(".xcworkspace")) { command += ` -workspace "${projectPath}"`; } else { command += ` -project "${projectPath}"`; } command += ` -scheme "${scheme}" -configuration "${configuration}" -destination "platform=iOS,id=${deviceId}" build install`; await executeCommand(command); console.error("앱 빌드 및 설치 완료"); } catch (error: any) { console.error(`앱 빌드 및 설치 오류: ${error.message}`); throw error; } }
  • findDeviceInfo: Resolves device name/ID to full info including xcodeId (UDID) and deviceCtlId (UUID) by querying xctrace list devices and devicectl list devices.
    /** * 디바이스 이름 또는 ID로 디바이스 정보를 찾습니다. * 먼저 정확한 ID(xcodeId 또는 deviceCtlId)로 검색하고, * 찾지 못한 경우 이름으로 검색합니다. * * @param nameOrId 디바이스 이름 또는 ID * @returns DeviceInfo 디바이스 정보 */ export async function findDeviceInfo(nameOrId: string): Promise<DeviceInfo> { const devices = await getAllDevices(); // 먼저 정확한 ID로 검색 let device = devices.find(d => d.xcodeId === nameOrId || d.deviceCtlId === nameOrId ); // ID로 찾지 못한 경우 이름으로 검색 if (!device) { device = devices.find(d => d.name === nameOrId || d.name.includes(nameOrId) || nameOrId.includes(d.name) ); } if (!device) { throw new Error(`기기를 찾을 수 없습니다: ${nameOrId}`); } return device; }

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/devyhan/xcode-mcp'

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