Skip to main content
Glama

xcresult_get_ui_hierarchy

Extract UI hierarchy data from Xcode test results to analyze accessibility trees and UI snapshots for debugging and AI processing.

Instructions

Get UI hierarchy attachment from test. Returns raw accessibility tree (best for AI), slim AI-readable JSON (default), or full JSON.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
xcresult_pathYesAbsolute path to the .xcresult file
test_idYesTest ID or index number to get UI hierarchy for
timestampNoOptional timestamp in seconds to find the closest UI snapshot. If not provided, uses the first available UI snapshot.
full_hierarchyNoSet to true to get the full hierarchy (several MB). Default is false for AI-readable slim version.
raw_formatNoSet to true to get the raw accessibility tree text (most AI-friendly). Default is false for JSON format.

Implementation Reference

  • Main handler function that implements the xcresult_get_ui_hierarchy tool logic: validates inputs, parses XCResult, finds UI hierarchy attachments, exports and converts to AI-friendly JSON or raw text, saves to temp files.
    public static async xcresultGetUIHierarchy( xcresultPath: string, testId: string, timestamp?: number, fullHierarchy: boolean = false, rawFormat: boolean = false ): 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 UI snapshots.` ); } Logger.info(`Found ${attachments.length} attachments for test ${testNode.name}`); // Log all attachment details for debugging Logger.info('All attachments:'); attachments.forEach((att, index) => { Logger.info(` ${index + 1}. Name: ${att.name || att.filename || 'unnamed'}, Type: ${att.uniform_type_identifier || att.uniformTypeIdentifier || 'unknown'}`); }); // Look for App UI hierarchy attachments (text-based) const uiHierarchyAttachments = this.findAppUIHierarchyAttachments(attachments); if (uiHierarchyAttachments.length === 0) { const attachmentNames = attachments.map(a => a.name || a.filename || 'unnamed').join(', '); throw new McpError( ErrorCode.InvalidParams, `No App UI hierarchy attachments found for test '${testNode.name}'. Available attachments: ${attachmentNames}` ); } // If timestamp is provided, find the closest UI hierarchy attachment let selectedAttachment = uiHierarchyAttachments[0]; if (timestamp !== undefined && uiHierarchyAttachments.length > 1) { Logger.info(`Looking for UI hierarchy closest to timestamp ${timestamp}s`); const closestAttachment = this.findClosestUISnapshot(uiHierarchyAttachments, timestamp); if (closestAttachment) { selectedAttachment = closestAttachment; } } else if (uiHierarchyAttachments.length > 1) { Logger.info(`Multiple UI hierarchy attachments found (${uiHierarchyAttachments.length}). Using the first one. Specify a timestamp to select a specific one.`); } if (!selectedAttachment) { throw new McpError( ErrorCode.InternalError, `No valid UI hierarchy found for test '${testNode.name}'` ); } // If raw format is requested, return the original accessibility tree text if (rawFormat) { const attachmentId = selectedAttachment.payloadId || selectedAttachment.payload_uuid || selectedAttachment.payloadUUID; if (!attachmentId) { throw new McpError( ErrorCode.InternalError, 'UI hierarchy attachment does not have a valid ID for export' ); } const filename = selectedAttachment.filename || selectedAttachment.name || 'ui-hierarchy'; const exportedPath = await parser.exportAttachment(attachmentId, filename); const { readFile } = await import('fs/promises'); const hierarchyContent = await readFile(exportedPath, 'utf-8'); const timestampInfo = timestamp !== undefined && selectedAttachment.timestamp !== undefined ? ` (closest to ${timestamp}s, actual: ${selectedAttachment.timestamp}s)` : ''; return { content: [{ type: 'text', text: `🌲 Raw UI Hierarchy for test '${testNode.name}'${timestampInfo}:\n\n${hierarchyContent}` }] }; } // Export and convert text-based UI hierarchy to JSON const hierarchyData = await this.exportTextUIHierarchyAsJSON(parser, selectedAttachment, testNode.name); if (fullHierarchy) { // Save full JSON to file with warning const jsonFilename = `ui_hierarchy_full_${testNode.name.replace(/[^a-zA-Z0-9]/g, '_')}_${Date.now()}.json`; const jsonPath = await this.saveUIHierarchyJSON(hierarchyData, jsonFilename); const fileSizeKB = Math.round(JSON.stringify(hierarchyData).length / 1024); return { content: [{ type: 'text', text: `⚠️ LARGE FILE WARNING: Full UI hierarchy exported (${fileSizeKB} KB)\n\n` + `πŸ“„ Full hierarchy: ${jsonPath}\n\n` + `πŸ’‘ For AI analysis, consider using the slim version instead:\n` + ` xcresult_get_ui_hierarchy "${xcresultPath}" "${testId}" ${timestamp || ''} false` }] }; } else { // Default: Create and save slim AI-readable version const slimData = this.createSlimUIHierarchy(hierarchyData); const slimFilename = `ui_hierarchy_${testNode.name.replace(/[^a-zA-Z0-9]/g, '_')}_${Date.now()}.json`; const slimPath = await this.saveUIHierarchyJSON(slimData, slimFilename); // Also save full data for element lookup const fullFilename = `ui_hierarchy_full_${testNode.name.replace(/[^a-zA-Z0-9]/g, '_')}_${Date.now()}.json`; const fullPath = await this.saveUIHierarchyJSON(hierarchyData, fullFilename); // Try to find a screenshot at the specified timestamp let screenshotInfo = ''; const screenshotTimestamp = timestamp || selectedAttachment.timestamp; if (screenshotTimestamp !== undefined) { try { const screenshotResult = await this.xcresultGetScreenshot( xcresultPath, testId, screenshotTimestamp ); if (screenshotResult && screenshotResult.content?.[0] && 'text' in screenshotResult.content[0]) { const textContent = screenshotResult.content[0]; if (textContent.type === 'text' && typeof textContent.text === 'string') { // Extract the screenshot path from the result text const pathMatch = textContent.text.match(/Screenshot extracted .+: (.+)/); if (pathMatch && pathMatch[1]) { screenshotInfo = `\nπŸ“Έ Screenshot at timestamp ${screenshotTimestamp}s: ${pathMatch[1]}`; } } } } catch (error) { // Screenshot extraction failed, continue without it Logger.info(`Could not extract screenshot at timestamp ${screenshotTimestamp}s: ${error}`); } } return { content: [{ type: 'text', text: `πŸ€– AI-readable UI hierarchy: ${slimPath}\n\n` + `πŸ’‘ Slim version properties:\n` + ` β€’ t = type (element type like Button, StaticText, etc.)\n` + ` β€’ l = label (visible text/accessibility label)\n` + ` β€’ c = children (array of child elements)\n` + ` β€’ j = index (reference to full element in original JSON)\n\n` + `πŸ” Use xcresult_get_ui_element "${fullPath}" <index> to get full details of any element.\n` + `⚠️ To get the full hierarchy (several MB), use: full_hierarchy=true${screenshotInfo}` }] }; } } 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 UI hierarchy: ${errorMessage}` ); } }
  • Tool registration and dispatch in XcodeServer: handles CallToolRequest for 'xcresult_get_ui_hierarchy' and delegates to XCResultTools.xcresultGetUIHierarchy
    case 'xcresult_get_ui_hierarchy': 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`); } return await XCResultTools.xcresultGetUIHierarchy( args.xcresult_path as string, args.test_id as string, args.timestamp as number | undefined, args.full_hierarchy as boolean | undefined, args.raw_format as boolean | undefined );
  • Input schema definition for the xcresult_get_ui_hierarchy tool, used for MCP listTools and validation
    name: 'xcresult_get_ui_hierarchy', description: 'Get UI hierarchy attachment from test. Returns raw accessibility tree (best for AI), slim AI-readable JSON (default), or full JSON.', 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 UI hierarchy for', }, timestamp: { type: 'number', description: 'Optional timestamp in seconds to find the closest UI snapshot. If not provided, uses the first available UI snapshot.', }, full_hierarchy: { type: 'boolean', description: 'Set to true to get the full hierarchy (several MB). Default is false for AI-readable slim version.', }, raw_format: { type: 'boolean', description: 'Set to true to get the raw accessibility tree text (most AI-friendly). Default is false for JSON format.', }, }, required: ['xcresult_path', 'test_id'], },
  • Secondary registration in direct callToolDirect method (CLI compatibility) dispatching to XCResultTools.xcresultGetUIHierarchy
    case 'xcresult_get_ui_hierarchy': 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`); } return await XCResultTools.xcresultGetUIHierarchy( args.xcresult_path as string, args.test_id as string, args.timestamp as number | undefined, args.full_hierarchy as boolean | undefined, args.raw_format as boolean | undefined );
  • Helper method to find App UI hierarchy attachments from test attachments list, used in xcresultGetUIHierarchy.
    private static findAppUIHierarchyAttachments(attachments: TestAttachment[]): TestAttachment[] { return attachments.filter(attachment => { const name = attachment.name || attachment.filename || ''; return name.includes('App UI hierarchy'); }); }

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/lapfelix/XcodeMCP'

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