Skip to main content
Glama
devyhan
by devyhan

run-on-device

Build and run Xcode projects directly on specified iOS devices or simulators. Stream logs, set environment variables, and manage configurations for efficient app development and debugging.

Input Schema

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

Implementation Reference

  • Main handler function for 'run-on-device' tool. Orchestrates the entire process: device discovery (findDeviceInfo, getAllDevices), bundle ID extraction, optional build/install (buildAndInstallApp), app launch (launchAppOnDevice), optional log streaming (startDeviceLogStream), and comprehensive error handling including auto-reinstall.
    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 input parameters for the 'run-on-device' tool.
    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 (프로젝트에서 추출하지 않음)") },
  • src/index.ts:547-548 (registration)
    Registration of the 'run-on-device' tool using McpServer.tool() with name, schema, and handler.
    server.tool( "run-on-device",
  • Critical helper function called by the handler to launch the app process on the physical device using devicectl. Supports environment variables, debugging mode, extra args, and automatic app installation fallback if the app is not yet installed on the device.
    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; } }
  • Helper function called by the handler (if !skipBuild) to build the Xcode project and install the app directly onto the target physical device using xcodebuild.
    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; } }

Other Tools

Related Tools

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