start_device_log_cap
Capture logs from Apple devices by launching an app with console output. Requires device UDID and app bundle ID. Returns a session ID for log monitoring.
Instructions
Starts capturing logs from a specified Apple device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro) by launching the app with console output. Returns a session ID.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| bundleId | Yes | Bundle identifier of the app to launch and capture logs for. | |
| deviceId | Yes | UDID of the device (obtained from list_devices) |
Implementation Reference
- The main handler logic function for the tool, which invokes startDeviceLogCapture and formats the ToolResponse.export async function start_device_log_capLogic( params: StartDeviceLogCapParams, executor: CommandExecutor, fileSystemExecutor?: FileSystemExecutor, ): Promise<ToolResponse> { const { deviceId, bundleId } = params; const { sessionId, error } = await startDeviceLogCapture( { deviceUuid: deviceId, bundleId: bundleId, }, executor, fileSystemExecutor, ); if (error) { return { content: [ { type: 'text', text: `Failed to start device log capture: ${error}`, }, ], isError: true, }; } return { content: [ { type: 'text', text: `✅ Device log capture started successfully\n\nSession ID: ${sessionId}\n\nNote: The app has been launched on the device with console output capture enabled.\n\nNext Steps:\n1. Interact with your app on the device\n2. Use stop_device_log_cap({ logSessionId: '${sessionId}' }) to stop capture and retrieve logs`, }, ], }; }
- Core implementation that executes the device log capture by launching the app via xcrun devicectl, sets up logging to a temp file, manages ChildProcess, detects failures, and creates a session.export async function startDeviceLogCapture( params: { deviceUuid: string; bundleId: string; }, executor: CommandExecutor = getDefaultCommandExecutor(), fileSystemExecutor?: FileSystemExecutor, ): Promise<{ sessionId: string; error?: string }> { // Clean up old logs before starting a new session await cleanOldDeviceLogs(); const { deviceUuid, bundleId } = params; const logSessionId = uuidv4(); const logFileName = `${DEVICE_LOG_FILE_PREFIX}${logSessionId}.log`; const tempDir = fileSystemExecutor ? fileSystemExecutor.tmpdir() : os.tmpdir(); const logFilePath = path.join(tempDir, logFileName); const launchJsonPath = path.join(tempDir, `devicectl-launch-${logSessionId}.json`); let logStream: fs.WriteStream | undefined; try { // Use injected file system executor or default if (fileSystemExecutor) { await fileSystemExecutor.mkdir(tempDir, { recursive: true }); await fileSystemExecutor.writeFile(logFilePath, ''); } else { await fs.promises.mkdir(tempDir, { recursive: true }); await fs.promises.writeFile(logFilePath, ''); } logStream = fs.createWriteStream(logFilePath, { flags: 'a' }); logStream.write( `\n--- Device log capture for bundle ID: ${bundleId} on device: ${deviceUuid} ---\n`, ); // Use executor with dependency injection instead of spawn directly const result = await executor( [ 'xcrun', 'devicectl', 'device', 'process', 'launch', '--console', '--terminate-existing', '--device', deviceUuid, '--json-output', launchJsonPath, bundleId, ], 'Device Log Capture', true, undefined, true, ); if (!result.success) { log( 'error', `Device log capture process reported failure: ${result.error ?? 'unknown error'}`, ); if (logStream && !logStream.destroyed) { logStream.write( `\n--- Device log capture failed to start ---\n${result.error ?? 'Unknown error'}\n`, ); logStream.end(); } return { sessionId: '', error: result.error ?? 'Failed to start device log capture', }; } const childProcess = result.process; if (!childProcess) { throw new Error('Device log capture process handle was not returned'); } const session: DeviceLogSession = { process: childProcess, logFilePath, deviceUuid, bundleId, logStream, hasEnded: false, }; let bufferedOutput = ''; const appendBufferedOutput = (text: string): void => { bufferedOutput += text; if (bufferedOutput.length > INITIAL_OUTPUT_LIMIT) { bufferedOutput = bufferedOutput.slice(bufferedOutput.length - INITIAL_OUTPUT_LIMIT); } }; let triggerImmediateFailure: ((message: string) => void) | undefined; const handleOutput = (chunk: unknown): void => { if (!logStream || logStream.destroyed) return; const text = typeof chunk === 'string' ? chunk : chunk instanceof Buffer ? chunk.toString('utf8') : String(chunk ?? ''); if (text.length > 0) { appendBufferedOutput(text); const extracted = extractFailureMessage(bufferedOutput); if (extracted) { triggerImmediateFailure?.(extracted); } logStream.write(text); } }; childProcess.stdout?.setEncoding?.('utf8'); childProcess.stdout?.on?.('data', handleOutput); childProcess.stderr?.setEncoding?.('utf8'); childProcess.stderr?.on?.('data', handleOutput); const cleanupStreams = (): void => { childProcess.stdout?.off?.('data', handleOutput); childProcess.stderr?.off?.('data', handleOutput); }; const earlyFailure = await detectEarlyLaunchFailure( childProcess, EARLY_FAILURE_WINDOW_MS, () => bufferedOutput, (handler) => { triggerImmediateFailure = handler; }, ); if (earlyFailure) { cleanupStreams(); session.hasEnded = true; const failureMessage = earlyFailure.errorMessage && earlyFailure.errorMessage.length > 0 ? earlyFailure.errorMessage : `Device log capture process exited immediately (exit code: ${ earlyFailure.exitCode ?? 'unknown' })`; log('error', `Device log capture failed to start: ${failureMessage}`); if (logStream && !logStream.destroyed) { try { logStream.write(`\n--- Device log capture failed to start ---\n${failureMessage}\n`); } catch { // best-effort logging } logStream.end(); } await removeFileIfExists(launchJsonPath, fileSystemExecutor); childProcess.kill?.('SIGTERM'); return { sessionId: '', error: failureMessage }; } const jsonOutcome = await pollJsonOutcome( launchJsonPath, fileSystemExecutor, getJsonResultWaitMs(), ); if (jsonOutcome?.errorMessage) { cleanupStreams(); session.hasEnded = true; const failureMessage = jsonOutcome.errorMessage; log('error', `Device log capture failed to start (JSON): ${failureMessage}`); if (logStream && !logStream.destroyed) { try { logStream.write(`\n--- Device log capture failed to start ---\n${failureMessage}\n`); } catch { // ignore secondary logging failures } logStream.end(); } childProcess.kill?.('SIGTERM'); return { sessionId: '', error: failureMessage }; } if (jsonOutcome?.pid && logStream && !logStream.destroyed) { try { logStream.write(`Process ID: ${jsonOutcome.pid}\n`); } catch { // best-effort logging only } } childProcess.once?.('error', (err) => { log( 'error', `Device log capture process error (session ${logSessionId}): ${ err instanceof Error ? err.message : String(err) }`, ); }); childProcess.once?.('close', (code) => { cleanupStreams(); session.hasEnded = true; if (logStream && !logStream.destroyed && !(logStream as WriteStreamWithClosed).closed) { logStream.write(`\n--- Device log capture ended (exit code: ${code ?? 'unknown'}) ---\n`); logStream.end(); } void removeFileIfExists(launchJsonPath, fileSystemExecutor); }); // For testing purposes, we'll simulate process management // In actual usage, the process would be managed by the executor result activeDeviceLogSessions.set(logSessionId, session); log('info', `Device log capture started with session ID: ${logSessionId}`); return { sessionId: logSessionId }; } catch (error) { const message = error instanceof Error ? error.message : String(error); log('error', `Failed to start device log capture: ${message}`); if (logStream && !logStream.destroyed && !(logStream as WriteStreamWithClosed).closed) { try { logStream.write(`\n--- Device log capture failed: ${message} ---\n`); } catch { // ignore secondary stream write failures } logStream.end(); } await removeFileIfExists(launchJsonPath, fileSystemExecutor); return { sessionId: '', error: message }; } }
- Zod schema defining the input parameters: deviceId (UDID) and bundleId.const startDeviceLogCapSchema = z.object({ deviceId: z.string().describe('UDID of the device (obtained from list_devices)'), bundleId: z.string().describe('Bundle identifier of the app to launch and capture logs for.'), });
- src/mcp/tools/logging/start_device_log_cap.ts:679-689 (registration)Tool registration exporting the default object with name, description, schema, and handler created via createSessionAwareTool.export default { name: 'start_device_log_cap', description: 'Starts log capture on a connected device.', schema: startDeviceLogCapSchema.omit({ deviceId: true } as const).shape, handler: createSessionAwareTool<StartDeviceLogCapParams>({ internalSchema: startDeviceLogCapSchema as unknown as z.ZodType<StartDeviceLogCapParams>, logicFunction: start_device_log_capLogic, getExecutor: getDefaultCommandExecutor, requirements: [{ allOf: ['deviceId'], message: 'deviceId is required' }], }), };
- Interface and Map for managing active device log capture sessions, shared with stop_device_log_cap.export interface DeviceLogSession { process: ChildProcess; logFilePath: string; deviceUuid: string; bundleId: string; logStream?: fs.WriteStream; hasEnded: boolean; } export const activeDeviceLogSessions = new Map<string, DeviceLogSession>();