record_video
Records video from an iOS Simulator using simctl. Specify codec, display, mask, and output path to capture screen activity.
Instructions
Records a video of the iOS Simulator using simctl directly
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| udid | No | Udid of target, can also be set with the IDB_UDID env var | |
| output_path | No | Optional output path. If not provided, a default name will be used. The file will be saved in the directory specified by `IOS_SIMULATOR_MCP_DEFAULT_OUTPUT_DIR` or in `~/Downloads` if the environment variable is not set. | |
| codec | No | Specifies the codec type: "h264" or "hevc". Default is "hevc". | |
| display | No | Display to capture: "internal" or "external". Default depends on device type. | |
| mask | No | For non-rectangular displays, handle the mask by policy: "ignored", "alpha", or "black". | |
| force | No | Force the output file to be written to, even if the file already exists. |
Implementation Reference
- src/index.ts:977-1058 (handler)The handler function that executes the 'record_video' tool logic. It spawns an xcrun simctl io recordVideo process, waits for the 'Recording started' message, and returns the output path. Uses helper functions getBootedDeviceId, ensureAbsolutePath, and errorWithTroubleshooting.
async ({ udid, output_path, codec, display, mask, force }) => { try { const actualUdid = await getBootedDeviceId(udid); const defaultFileName = `simulator_recording_${Date.now()}.mp4`; const outputFile = ensureAbsolutePath(output_path ?? defaultFileName); // Start the recording process const recordingProcess = spawn("xcrun", [ "simctl", "io", actualUdid, "recordVideo", ...(codec ? [`--codec=${codec}`] : []), ...(display ? [`--display=${display}`] : []), ...(mask ? [`--mask=${mask}`] : []), ...(force ? ["--force"] : []), // When passing user-provided values to a command, it's crucial to use `--` // to separate the command's options from positional arguments. // This prevents the shell from misinterpreting the arguments as options. "--", outputFile, ]); // Wait for recording to start or fail within 5 seconds await new Promise((resolve, reject) => { let errorOutput = ""; let resolved = false; recordingProcess.stderr.on("data", (data) => { const message = data.toString(); if (message.includes("Recording started")) { resolved = true; resolve(true); } else { errorOutput += message; } }); recordingProcess.on("exit", (code) => { if (!resolved) { reject(new Error( errorOutput.trim() || `Recording process exited early with code ${code}` )); } }); setTimeout(() => { if (!resolved) { if (recordingProcess.killed || recordingProcess.exitCode !== null) { reject(new Error(errorOutput.trim() || "Recording process terminated unexpectedly")); } else { // Process still running but no "Recording started" message — assume it started resolve(true); } } }, 5000); }); return { isError: false, content: [ { type: "text", text: `Recording started. The video will be saved to: ${outputFile}\nTo stop recording, use the stop_recording command.`, }, ], }; } catch (error) { return { isError: true, content: [ { type: "text", text: errorWithTroubleshooting( `Error starting recording: ${toError(error).message}` ), }, ], }; } } ); - src/index.ts:938-975 (schema)Input schema definition for the 'record_video' tool. Defines parameters: udid (optional UUID string), output_path (optional string), codec (h264/hevc), display (internal/external), mask (ignored/alpha/black), force (boolean).
{ udid: z .string() .regex(UDID_REGEX) .optional() .describe("Udid of target, can also be set with the IDB_UDID env var"), output_path: z .string() .max(1024) .optional() .describe( `Optional output path. If not provided, a default name will be used. The file will be saved in the directory specified by \`IOS_SIMULATOR_MCP_DEFAULT_OUTPUT_DIR\` or in \`~/Downloads\` if the environment variable is not set.` ), codec: z .enum(["h264", "hevc"]) .optional() .describe( 'Specifies the codec type: "h264" or "hevc". Default is "hevc".' ), display: z .enum(["internal", "external"]) .optional() .describe( 'Display to capture: "internal" or "external". Default depends on device type.' ), mask: z .enum(["ignored", "alpha", "black"]) .optional() .describe( 'For non-rectangular displays, handle the mask by policy: "ignored", "alpha", or "black".' ), force: z .boolean() .optional() .describe( "Force the output file to be written to, even if the file already exists." ), }, - src/index.ts:934-1059 (registration)Registration of the 'record_video' tool on the MCP server, conditionally gated by isToolFiltered check. Uses server.tool() with the tool name, description, input schema, and handler function.
if (!isToolFiltered("record_video")) { server.tool( "record_video", "Records a video of the iOS Simulator using simctl directly", { udid: z .string() .regex(UDID_REGEX) .optional() .describe("Udid of target, can also be set with the IDB_UDID env var"), output_path: z .string() .max(1024) .optional() .describe( `Optional output path. If not provided, a default name will be used. The file will be saved in the directory specified by \`IOS_SIMULATOR_MCP_DEFAULT_OUTPUT_DIR\` or in \`~/Downloads\` if the environment variable is not set.` ), codec: z .enum(["h264", "hevc"]) .optional() .describe( 'Specifies the codec type: "h264" or "hevc". Default is "hevc".' ), display: z .enum(["internal", "external"]) .optional() .describe( 'Display to capture: "internal" or "external". Default depends on device type.' ), mask: z .enum(["ignored", "alpha", "black"]) .optional() .describe( 'For non-rectangular displays, handle the mask by policy: "ignored", "alpha", or "black".' ), force: z .boolean() .optional() .describe( "Force the output file to be written to, even if the file already exists." ), }, { title: "Record Video", readOnlyHint: false, openWorldHint: true }, async ({ udid, output_path, codec, display, mask, force }) => { try { const actualUdid = await getBootedDeviceId(udid); const defaultFileName = `simulator_recording_${Date.now()}.mp4`; const outputFile = ensureAbsolutePath(output_path ?? defaultFileName); // Start the recording process const recordingProcess = spawn("xcrun", [ "simctl", "io", actualUdid, "recordVideo", ...(codec ? [`--codec=${codec}`] : []), ...(display ? [`--display=${display}`] : []), ...(mask ? [`--mask=${mask}`] : []), ...(force ? ["--force"] : []), // When passing user-provided values to a command, it's crucial to use `--` // to separate the command's options from positional arguments. // This prevents the shell from misinterpreting the arguments as options. "--", outputFile, ]); // Wait for recording to start or fail within 5 seconds await new Promise((resolve, reject) => { let errorOutput = ""; let resolved = false; recordingProcess.stderr.on("data", (data) => { const message = data.toString(); if (message.includes("Recording started")) { resolved = true; resolve(true); } else { errorOutput += message; } }); recordingProcess.on("exit", (code) => { if (!resolved) { reject(new Error( errorOutput.trim() || `Recording process exited early with code ${code}` )); } }); setTimeout(() => { if (!resolved) { if (recordingProcess.killed || recordingProcess.exitCode !== null) { reject(new Error(errorOutput.trim() || "Recording process terminated unexpectedly")); } else { // Process still running but no "Recording started" message — assume it started resolve(true); } } }, 5000); }); return { isError: false, content: [ { type: "text", text: `Recording started. The video will be saved to: ${outputFile}\nTo stop recording, use the stop_recording command.`, }, ], }; } catch (error) { return { isError: true, content: [ { type: "text", text: errorWithTroubleshooting( `Error starting recording: ${toError(error).message}` ), }, ], }; } } ); } - src/index.ts:195-208 (helper)Helper function getBootedDeviceId used by record_video handler to resolve the simulator UDID, falling back to the currently booted device if not provided.
async function getBootedDeviceId( deviceId: string | undefined ): Promise<string> { // If deviceId not provided, get the currently booted simulator let actualDeviceId = deviceId; if (!actualDeviceId) { const { id } = await getBootedDevice(); actualDeviceId = id; } if (!actualDeviceId) { throw new Error("No booted simulator found and no deviceId provided"); } return actualDeviceId; } - src/index.ts:819-844 (helper)Helper function ensureAbsolutePath used by record_video handler to resolve the output file path, expanding tilde and using the IOS_SIMULATOR_MCP_DEFAULT_OUTPUT_DIR env var or ~/Downloads as fallback.
function ensureAbsolutePath(filePath: string): string { if (path.isAbsolute(filePath)) { return filePath; } // Handle ~/something paths in the provided filePath if (filePath.startsWith("~/")) { return path.join(os.homedir(), filePath.slice(2)); } // Determine the default directory from env var or fallback to ~/Downloads let defaultDir = path.join(os.homedir(), "Downloads"); const customDefaultDir = process.env.IOS_SIMULATOR_MCP_DEFAULT_OUTPUT_DIR; if (customDefaultDir) { // also expand tilde for the custom directory path if (customDefaultDir.startsWith("~/")) { defaultDir = path.join(os.homedir(), customDefaultDir.slice(2)); } else { defaultDir = customDefaultDir; } } // Join the relative filePath with the resolved default directory return path.join(defaultDir, filePath); }