xcresult_get_screenshot
Extract screenshots from failed Xcode test videos to diagnose issues by specifying a timestamp before the failure occurs.
Instructions
Get screenshot from a failed test at specific timestamp - extracts frame from video attachment using ffmpeg
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| xcresult_path | Yes | Absolute path to the .xcresult file | |
| test_id | Yes | Test ID or index number to get screenshot for | |
| timestamp | Yes | Timestamp in seconds when to extract the screenshot. WARNING: Use a timestamp BEFORE the failure (e.g., if failure is at 30.71s, use 30.69s) as failure timestamps often show the home screen after the app has crashed or reset. |
Implementation Reference
- src/tools/XCResultTools.ts:784-907 (handler)Core handler function implementing the xcresult_get_screenshot tool. Parses XCResult file, locates test attachments, prefers video attachments (extracts frame using ffmpeg at given timestamp) or falls back to closest direct image attachment.public static async xcresultGetScreenshot( xcresultPath: string, testId: string, timestamp: number ): Promise<McpResult> { // Validate xcresult path if (!existsSync(xcresultPath)) { throw new McpError( ErrorCode.InvalidParams, `XCResult file not found: ${xcresultPath}` ); } if (!xcresultPath.endsWith('.xcresult')) { throw new McpError( ErrorCode.InvalidParams, `Path must be an .xcresult file: ${xcresultPath}` ); } // Check if xcresult is readable if (!XCResultParser.isXCResultReadable(xcresultPath)) { throw new McpError( ErrorCode.InternalError, `XCResult file is not readable or incomplete: ${xcresultPath}` ); } if (!testId || testId.trim() === '') { throw new McpError( ErrorCode.InvalidParams, 'Test ID or index is required' ); } try { const parser = new XCResultParser(xcresultPath); // First find the test node to get the actual test identifier const testNode = await parser.findTestNode(testId); if (!testNode) { throw new McpError( ErrorCode.InvalidParams, `Test '${testId}' not found. Run xcresult_browse "${xcresultPath}" to see all available tests` ); } if (!testNode.nodeIdentifier) { throw new McpError( ErrorCode.InvalidParams, `Test '${testId}' does not have a valid identifier for attachment retrieval` ); } // Get test attachments const attachments = await parser.getTestAttachments(testNode.nodeIdentifier); if (attachments.length === 0) { throw new McpError( ErrorCode.InvalidParams, `No attachments found for test '${testNode.name}'. This test may not have failed or may not have generated screenshots/videos.` ); } Logger.info(`Found ${attachments.length} attachments for test ${testNode.name}`); // Look for video attachment first (gives us actual PNG images) const videoAttachment = this.findVideoAttachment(attachments); if (videoAttachment) { const screenshotPath = await this.extractScreenshotFromVideo(parser, videoAttachment, testNode.name, timestamp); return { content: [{ type: 'text', text: `Screenshot extracted from video for test '${testNode.name}' at ${timestamp}s: ${screenshotPath}` }] }; } // Look for direct image attachment (PNG or JPEG) as fallback const closestImageResult = this.findClosestImageAttachment(attachments, timestamp); if (closestImageResult) { const screenshotPath = await this.exportScreenshotAttachment(parser, closestImageResult.attachment); const timeDiff = closestImageResult.timeDifference; const timeDiffText = timeDiff === 0 ? 'at exact timestamp' : timeDiff > 0 ? `${timeDiff.toFixed(2)}s after requested time` : `${Math.abs(timeDiff).toFixed(2)}s before requested time`; return { content: [{ type: 'text', text: `Screenshot exported for test '${testNode.name}' (${timeDiffText}): ${screenshotPath}` }] }; } // No suitable attachments found const attachmentTypes = attachments.map(a => a.uniform_type_identifier || a.uniformTypeIdentifier || 'unknown').join(', '); throw new McpError( ErrorCode.InvalidParams, `No screenshot or video attachments found for test '${testNode.name}'. Available attachment types: ${attachmentTypes}` ); } catch (error) { if (error instanceof McpError) { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('xcresulttool')) { throw new McpError( ErrorCode.InternalError, `XCResult parsing failed. Make sure Xcode Command Line Tools are installed: ${errorMessage}` ); } throw new McpError( ErrorCode.InternalError, `Failed to get screenshot: ${errorMessage}` ); } }
- Primary input schema definition for the xcresult_get_screenshot tool, used by the MCP server for validation.name: 'xcresult_get_screenshot', description: 'Get screenshot from a failed test at specific timestamp - extracts frame from video attachment using ffmpeg', inputSchema: { type: 'object', properties: { xcresult_path: { type: 'string', description: 'Absolute path to the .xcresult file', }, test_id: { type: 'string', description: 'Test ID or index number to get screenshot for', }, timestamp: { type: 'number', description: 'Timestamp in seconds when to extract the screenshot. WARNING: Use a timestamp BEFORE the failure (e.g., if failure is at 30.71s, use 30.69s) as failure timestamps often show the home screen after the app has crashed or reset.', }, }, required: ['xcresult_path', 'test_id', 'timestamp'], },
- src/XcodeServer.ts:551-565 (registration)Tool registration in MCP server request handler: switch case dispatching 'xcresult_get_screenshot' calls to XCResultTools handler.case 'xcresult_get_screenshot': if (!args.xcresult_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcresult_path`); } if (!args.test_id) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: test_id`); } if (args.timestamp === undefined) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: timestamp`); } return await XCResultTools.xcresultGetScreenshot( args.xcresult_path as string, args.test_id as string, args.timestamp as number );
- src/tools/XCResultTools.ts:1615-1678 (helper)Key helper function that runs ffmpeg to extract a single PNG frame from video attachments at the specified timestamp.private static async runFFmpeg(videoPath: string, outputPath: string, timestamp: number): Promise<void> { return new Promise((resolve, reject) => { // Try common ffmpeg paths const ffmpegPaths = [ '/opt/homebrew/bin/ffmpeg', // Homebrew on Apple Silicon '/usr/local/bin/ffmpeg', // Homebrew on Intel 'ffmpeg' // System PATH ]; let ffmpegPath = 'ffmpeg'; for (const path of ffmpegPaths) { if (existsSync(path)) { ffmpegPath = path; break; } } Logger.info(`Using ffmpeg at: ${ffmpegPath}`); Logger.info(`Extracting frame from: ${videoPath} at ${timestamp}s`); Logger.info(`Output path: ${outputPath}`); // Extract a frame at the specific timestamp as PNG const process = spawn(ffmpegPath, [ '-i', videoPath, // Input video '-ss', timestamp.toString(), // Seek to specific timestamp '-frames:v', '1', // Extract only 1 frame '-q:v', '2', // High quality '-y', // Overwrite output file outputPath // Output PNG file ], { stdio: ['pipe', 'pipe', 'pipe'] }); let stderr = ''; process.stderr.on('data', (data) => { stderr += data.toString(); }); process.on('close', (code) => { if (code === 0) { Logger.info(`ffmpeg completed successfully`); // Add a small delay to ensure file is written setTimeout(() => { if (existsSync(outputPath)) { resolve(); } else { reject(new Error(`Screenshot file not found after ffmpeg completion: ${outputPath}`)); } }, 100); } else { Logger.error(`ffmpeg failed with code ${code}`); Logger.error(`ffmpeg stderr: ${stderr}`); reject(new Error(`ffmpeg failed with code ${code}: ${stderr}`)); } }); process.on('error', (error) => { Logger.error(`ffmpeg execution error: ${error.message}`); reject(new Error(`Failed to run ffmpeg: ${error.message}. Make sure ffmpeg is installed (brew install ffmpeg)`)); }); }); }
- src/XcodeServer.ts:988-1002 (registration)Additional registration in direct callTool method for CLI compatibility: identical switch case dispatching to the same handler.case 'xcresult_get_screenshot': if (!args.xcresult_path) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: xcresult_path`); } if (!args.test_id) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: test_id`); } if (args.timestamp === undefined) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter: timestamp`); } return await XCResultTools.xcresultGetScreenshot( args.xcresult_path as string, args.test_id as string, args.timestamp as number );