Skip to main content
Glama

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
NameRequiredDescriptionDefault
bundleIdYesBundle identifier of the app to launch and capture logs for.
deviceIdYesUDID 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.'),
    });
  • 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>();
Install Server

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/cameroncooke/XcodeBuildMCP'

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