Skip to main content
Glama

XC-MCP: XCode CLI wrapper

by conorluddy
idb-target-cache.ts10.2 kB
import { executeCommand } from '../utils/command.js'; import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; /** * IDB Target Cache * * <architecture> * Caches IDB target information to avoid repeated `idb list-targets` calls. * Similar to SimulatorCache, tracks: * - Available targets (devices + simulators) * - Screen dimensions for coordinate validation * - Connection status (USB/WiFi for devices) * - Last used target for auto-detection * - Usage tracking for intelligent defaults * </architecture> */ export interface IDBTarget { udid: string; name: string; type: 'device' | 'simulator'; state: 'Booted' | 'Shutdown'; osVersion: string; architecture: string; screenDimensions: { width: number; height: number }; // Device-specific fields connectionType?: 'usb' | 'wifi'; companionPort?: number; // Usage tracking for intelligent defaults lastUsed?: number; successfulOperations: number; } interface IDBTargetCacheEntry { targets: Map<string, IDBTarget>; lastFetched: number; ttl: number; // Default: 5000ms (5 seconds) - simulator state changes frequently } /** * IDB Target Cache Manager * * Why: Avoid expensive `idb list-targets` calls on every operation. * Cache targets for 5 seconds (configurable) to balance performance with state accuracy. * * Note: Short TTL is critical because simulator boot state changes frequently during * development. A 60s TTL caused stale "Shutdown" data when simulators were actually "Booted", * blocking all IDB operations. 5 seconds provides fast cache while ensuring state accuracy. */ class IDBTargetCacheManager { private cache: IDBTargetCacheEntry = { targets: new Map(), lastFetched: 0, ttl: 5000, // 5 seconds default - balance performance with state accuracy }; /** * Get target by UDID, refreshing cache if stale * * Why: Primary method for accessing target info. * Auto-refreshes cache if TTL expired. * * @param udid - Target UDID * @returns IDBTarget with full details * @throws McpError if target not found */ async getTarget(udid: string): Promise<IDBTarget> { await this.refreshIfStale(); const target = this.cache.targets.get(udid); if (!target) { throw new McpError( ErrorCode.InvalidRequest, `Target "${udid}" not found. Use idb-targets to list available targets.` ); } console.error(`[IDBTargetCache] getTarget(${udid}): ${target.name} - state: ${target.state}`); // If screen dimensions are missing (0x0), fetch them with idb describe if (target.screenDimensions.width === 0 || target.screenDimensions.height === 0) { await this.fetchScreenDimensions(udid, target); } return target; } /** * Get last used target for auto-detection * * Why: Enable UDID auto-detection for better UX. * Prefers booted targets, then most recently used. * * @returns Last used booted target, or undefined if none */ async getLastUsedTarget(): Promise<IDBTarget | undefined> { await this.refreshIfStale(); const targets = Array.from(this.cache.targets.values()); const bootedTargets = targets.filter(t => t.state === 'Booted'); if (bootedTargets.length === 0) { return undefined; } // Sort by last used timestamp (most recent first) return bootedTargets.sort((a, b) => (b.lastUsed || 0) - (a.lastUsed || 0))[0]; } /** * Find target by UDID (no error if not found) * * Why: Non-throwing version for existence checks. * * @param udid - Target UDID * @returns IDBTarget if found, undefined otherwise */ async findTargetByUdid(udid: string): Promise<IDBTarget | undefined> { await this.refreshIfStale(); return this.cache.targets.get(udid); } /** * List all cached targets * * Why: Support idb-targets tool for listing available targets. * * @param filters - Optional filters for state, type * @returns Array of IDBTargets */ async listTargets(filters?: { state?: 'Booted' | 'Shutdown'; type?: 'device' | 'simulator'; }): Promise<IDBTarget[]> { await this.refreshIfStale(); let targets = Array.from(this.cache.targets.values()); if (filters?.state) { targets = targets.filter(t => t.state === filters.state); } if (filters?.type) { targets = targets.filter(t => t.type === filters.type); } return targets; } /** * Record successful operation for usage tracking * * Why: Track which targets are actively used for intelligent defaults. * * @param udid - Target UDID */ recordSuccess(udid: string): void { const target = this.cache.targets.get(udid); if (target) { target.lastUsed = Date.now(); target.successfulOperations++; } } /** * Clear cache to force refresh * * Why: Support manual cache invalidation for troubleshooting. */ clearCache(): void { this.cache.targets.clear(); this.cache.lastFetched = 0; } /** * Set cache TTL * * Why: Allow runtime configuration of cache duration. * * @param ttlMs - Time-to-live in milliseconds */ setCacheTTL(ttlMs: number): void { this.cache.ttl = ttlMs; } /** * Get cache statistics * * Why: Support cache-get-stats tool for monitoring. * * @returns Cache stats including target count, TTL, last fetch time */ getCacheStats(): { targetCount: number; ttl: number; lastFetched: number; cacheAge: number; } { return { targetCount: this.cache.targets.size, ttl: this.cache.ttl, lastFetched: this.cache.lastFetched, cacheAge: Date.now() - this.cache.lastFetched, }; } /** * Fetch screen dimensions for a target using idb describe * * Why: idb list-targets doesn't include screen_dimensions. * We need to call idb describe to get this information. * * @param udid - Target UDID * @param target - Target object to update */ private async fetchScreenDimensions(udid: string, target: IDBTarget): Promise<void> { try { const result = await executeCommand(`idb describe --udid "${udid}" --json`, { timeout: 10000, }); if (result.code !== 0) { console.warn( `[IDBTargetCache] Failed to fetch screen dimensions for ${udid}: ${result.stderr}` ); return; } // Parse describe output - it's regular JSON (not NDJSON) const description = JSON.parse(result.stdout); // Update screen dimensions if available if (description.screen_dimensions) { target.screenDimensions = { width: description.screen_dimensions.width || 0, height: description.screen_dimensions.height || 0, }; } } catch (error) { console.warn( `[IDBTargetCache] Failed to parse screen dimensions for ${udid}: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Refresh cache from IDB if stale * * Why: Internal method to maintain cache freshness. * Called automatically by public methods. */ private async refreshIfStale(): Promise<void> { const now = Date.now(); if (now - this.cache.lastFetched < this.cache.ttl) { return; // Cache still valid } await this.refreshCache(); } /** * Force cache refresh from IDB * * Why: Fetch latest target list from IDB. * Parses JSON output and populates cache. */ private async refreshCache(): Promise<void> { try { console.error('[IDBTargetCache] Refreshing cache from idb list-targets...'); const result = await executeCommand('idb list-targets --json', { timeout: 10000, }); if (result.code !== 0) { throw new Error(`idb list-targets failed: ${result.stderr}`); } // Parse IDB NDJSON output (newline-delimited JSON) // IDB returns one JSON object per line, not a JSON array interface IDBTarget { udid: string; name: string; state?: string; type?: string; os_version?: string; architecture?: string; screen_dimensions?: { width: number; height: number }; connection_type?: string; companion_info?: { port: number }; } const targets: IDBTarget[] = result.stdout .trim() .split('\n') .filter(line => line.trim()) .map(line => JSON.parse(line) as IDBTarget); console.error(`[IDBTargetCache] idb list-targets returned ${targets.length} targets`); this.cache.targets.clear(); for (const target of targets) { // Preserve existing usage tracking if target already cached const existing = this.cache.targets.get(target.udid); console.error( `[IDBTargetCache] Parsed target: ${target.name} (${target.udid}) - state: ${target.state}` ); this.cache.targets.set(target.udid, { udid: target.udid, name: target.name, type: target.type === 'simulator' ? 'simulator' : 'device', state: target.state === 'Booted' ? 'Booted' : 'Shutdown', osVersion: target.os_version || 'Unknown', architecture: target.architecture || 'Unknown', screenDimensions: { width: target.screen_dimensions?.width || 0, height: target.screen_dimensions?.height || 0, }, connectionType: target.connection_type === 'usb' || target.connection_type === 'wifi' ? target.connection_type : undefined, companionPort: target.companion_info?.port, // Preserve usage tracking lastUsed: existing?.lastUsed, successfulOperations: existing?.successfulOperations || 0, }); } this.cache.lastFetched = Date.now(); console.error('[IDBTargetCache] Cache refresh completed'); } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to refresh IDB target cache: ${error instanceof Error ? error.message : String(error)}` ); } } } // Export singleton instance export const IDBTargetCache = new IDBTargetCacheManager();

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/conorluddy/xc-mcp'

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