video_record
Record screen video on mobile devices for testing. Use start action to begin and stop action to end and retrieve the file.
Instructions
Enregistre une vidéo de l'écran du device. Utilise action='start' pour commencer et action='stop' pour arrêter et récupérer le fichier.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| action | Yes | 'start' pour commencer l'enregistrement, 'stop' pour arrêter |
Implementation Reference
- src/tools/video-record.ts:27-91 (handler)Main handler for video_record tool. Registers the MCP tool with 'start'/'stop' actions, manages recording state, dispatches to iOS (simctl) or Android (adb) recording helpers.
export function registerVideoRecord(server: McpServer): void { server.tool( "video_record", "Enregistre une vidéo de l'écran du device. Utilise action='start' pour commencer et action='stop' pour arrêter et récupérer le fichier.", { action: z.enum(["start", "stop"]).describe("'start' pour commencer l'enregistrement, 'stop' pour arrêter"), }, async ({ action }) => { const result = await resolveDevice(); if ("error" in result) return { content: [{ type: "text", text: result.error }], isError: true }; const dev = result.device; // Recover from stale state if process died recoverStaleState(); try { if (action === "start") { if (isRecording) { return { content: [{ type: "text", text: `Enregistrement déjà en cours (${isRecording}). Arrête-le d'abord avec action='stop'.` }], isError: true }; } if (dev.platform === "ios") { iosRecordPid = await iosStartVideoRecord(dev.id, "/tmp/phantom-ios-record.mp4"); isRecording = "ios"; } else { await androidStartScreenRecord(); isRecording = "android"; } return { content: [{ type: "text", text: `Enregistrement vidéo démarré sur ${dev.name}. Utilise video_record(action='stop') pour arrêter.` }] }; } // Stop if (!isRecording) { return { content: [{ type: "text", text: "Aucun enregistrement en cours. Lance d'abord video_record(action='start')." }], isError: true }; } let filePath: string; if (isRecording === "ios") { if (iosRecordPid) { try { process.kill(iosRecordPid, "SIGINT"); } catch (err) { console.error(`[phantom] Failed to stop iOS recording (PID ${iosRecordPid}): ${err instanceof Error ? err.message : err}`); } iosRecordPid = null; } await new Promise((r) => setTimeout(r, 1500)); filePath = "/tmp/phantom-ios-record.mp4"; } else { filePath = await androidStopScreenRecord(); } isRecording = null; return { content: [{ type: "text", text: `Vidéo enregistrée : ${filePath}\nTu peux la lire avec : open "${filePath}"` }], }; } catch (err) { isRecording = null; const msg = err instanceof Error ? err.message : String(err); return { content: [{ type: "text", text: `Erreur video_record: ${msg}` }], isError: true }; } } ); } - src/tools/video-record.ts:31-33 (schema)Zod schema for video_record: 'action' enum of 'start' or 'stop'.
{ action: z.enum(["start", "stop"]).describe("'start' pour commencer l'enregistrement, 'stop' pour arrêter"), }, - src/index.ts:60-60 (registration)Registration call in the main server setup, which registers the video_record tool on the MCP server.
registerVideoRecord(server); - src/platforms/ios/simctl.ts:171-181 (helper)iOS helper: starts video recording via xcrun simctl io recordVideo, returns PID for later termination.
export async function iosStartVideoRecord(deviceUdid: string, outputPath: string): Promise<number | null> { validateUdid(deviceUdid); if (!outputPath.startsWith("/tmp/phantom-")) throw new Error("outputPath doit commencer par /tmp/phantom-"); const proc = spawn("xcrun", ["simctl", "io", deviceUdid, "recordVideo", outputPath], { detached: true, stdio: "ignore", }); proc.unref(); console.error(`[phantom] iOS video recording started (PID: ${proc.pid})`); return proc.pid ?? null; } - src/platforms/android/adb.ts:382-416 (helper)Android helpers: starts screenrecord via adb shell screenrecord (detached), stops by killing PID/pulling file to /tmp.
export async function androidStartScreenRecord(outputPath: string = "/sdcard/phantom-record.mp4"): Promise<void> { if (!outputPath.startsWith("/sdcard/phantom-")) throw new Error("outputPath doit commencer par /sdcard/phantom-"); const path = await findAdb(); const proc = spawn(path, buildArgs(["shell", "screenrecord", outputPath]), { detached: true, stdio: "ignore", }); proc.unref(); // Store PID for later kill screenRecordPid = proc.pid ?? null; console.error(`[phantom] Screen recording started on Android (PID: ${screenRecordPid})`); } let screenRecordPid: number | null = null; export async function androidStopScreenRecord(): Promise<string> { // Kill the screenrecord process if (screenRecordPid) { try { process.kill(screenRecordPid); } catch (err) { console.error(`[phantom] Failed to kill screenrecord (PID ${screenRecordPid}): ${err instanceof Error ? err.message : err}`); } screenRecordPid = null; } else { await adb(["shell", "pkill", "-f", "screenrecord"]).catch(() => {}); } await new Promise((r) => setTimeout(r, 1000)); // Pull the file to local disk const localPath = "/tmp/phantom-android-record.mp4"; const adbPath = await findAdb(); await execFileAsync(adbPath, buildArgs(["pull", "/sdcard/phantom-record.mp4", localPath])); await adb(["shell", "rm", "/sdcard/phantom-record.mp4"]).catch(() => {}); return localPath; }