Skip to main content
Glama

XC-MCP: XCode CLI wrapper

by conorluddy
ui-input.ts14.3 kB
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { formatToolError } from '../../utils/error-formatter.js'; import { executeCommandWithArgs } from '../../utils/command.js'; import { resolveIdbUdid, validateTargetBooted } from '../../utils/idb-device-detection.js'; import { IDBTargetCache } from '../../state/idb-target-cache.js'; import { isValidUdid } from '../../utils/shell-escape.js'; interface IdbUiInputArgs { udid?: string; operation: 'text' | 'key' | 'key-sequence'; // For 'text' operation text?: string; // For 'key' operation key?: | 'home' | 'lock' | 'siri' | 'delete' | 'return' | 'space' | 'escape' | 'tab' | 'up' | 'down' | 'left' | 'right'; // For 'key-sequence' operation keySequence?: string[]; // Array of key names // LLM optimization actionName?: string; // e.g., "Enter Email" fieldContext?: string; // e.g., "Email TextField" expectedOutcome?: string; // e.g., "Email field populated" isSensitive?: boolean; // Mark as sensitive (password, etc.) } /** * Input text and keyboard commands - automated text entry and special key presses for form automation * * **What it does:** * Sends text input and keyboard commands to focused elements on iOS targets. Types text strings into * active text fields, presses special keys (home, return, delete, arrows), and executes key sequences * for complex input workflows. Automatically redacts sensitive data (passwords) in responses and provides * semantic field context tracking for test documentation. * * **Why you'd use it:** * - Automate form filling without manual keyboard interaction - login flows, search, data entry * - Execute keyboard shortcuts and navigation (tab, return, arrows) for multi-field workflows * - Safely handle sensitive data with automatic redaction in tool responses and logs * - Track input operations with semantic metadata (actionName, fieldContext, expectedOutcome) * * **Parameters:** * - operation (required): "text" | "key" | "key-sequence" * - text (required for text operation): String to type into focused field * - key (required for key operation): Special key name (home, return, delete, tab, arrows, etc.) * - keySequence (required for key-sequence operation): Array of key names to press in order * - udid (optional): Target identifier - auto-detects if omitted * - actionName, fieldContext, expectedOutcome (optional): Semantic tracking for test documentation * - isSensitive (optional): Mark as sensitive to redact from output * * **Returns:** * Input execution status with operation details (redacted if sensitive), duration, input context * metadata for test tracking, error details if failed, and troubleshooting guidance specific to * text vs. key operations. * * **Example:** * ```typescript * // Type email into focused field * const result = await idbUiInputTool({ * operation: 'text', * text: 'user@example.com', * actionName: 'Enter Email', * fieldContext: 'Email TextField' * }); * * // Press return to submit * await idbUiInputTool({ operation: 'key', key: 'return' }); * ``` * * **Full documentation:** See idb/ui-input.md for detailed parameters and available keys * * @param args Tool arguments with operation type and input data * @returns Tool result with input status and semantic context */ export async function idbUiInputTool(args: IdbUiInputArgs) { const { udid, operation, text, key, keySequence, actionName, fieldContext, expectedOutcome, isSensitive, } = args; try { // ============================================================================ // STAGE 1: Validation & Preparation // ============================================================================ if (!operation || !['text', 'key', 'key-sequence'].includes(operation)) { throw new McpError( ErrorCode.InvalidRequest, 'operation must be "text", "key", or "key-sequence"' ); } // Validate operation-specific parameters if (operation === 'text' && !text) { throw new McpError(ErrorCode.InvalidRequest, 'text parameter required for text operation'); } if (operation === 'key' && !key) { throw new McpError(ErrorCode.InvalidRequest, 'key parameter required for key operation'); } if (operation === 'key-sequence' && (!keySequence || keySequence.length === 0)) { throw new McpError( ErrorCode.InvalidRequest, 'keySequence parameter required for key-sequence operation' ); } // Resolve UDID and validate target is booted const resolvedUdid = await resolveIdbUdid(udid); const target = await validateTargetBooted(resolvedUdid); const startTime = Date.now(); // ============================================================================ // STAGE 2: Execute Input // ============================================================================ const result = await executeInputCommand(resolvedUdid, { operation, text, key, keySequence, }); // Record successful input if (result.success) { IDBTargetCache.recordSuccess(resolvedUdid); } // ============================================================================ // STAGE 3: Response Formatting // ============================================================================ const duration = Date.now() - startTime; // Redact sensitive text in response const displayText = isSensitive && text ? `[REDACTED (${text.length} chars)]` : text; const responseData = { success: result.success, udid: resolvedUdid, targetName: target.name, operation, input: operation === 'text' ? { text: displayText, length: text?.length } : operation === 'key' ? { key } : { keySequence, count: keySequence?.length }, duration, // LLM optimization: input context inputContext: actionName || fieldContext || expectedOutcome ? { actionName, fieldContext, expectedOutcome, isSensitive, } : undefined, output: result.output, error: result.error || undefined, guidance: formatGuidance(result.success, target, { operation, displayText, key, keySequence, actionName, expectedOutcome, resolvedUdid, }), }; return { content: [ { type: 'text' as const, text: JSON.stringify(responseData, null, 2), }, ], isError: !result.success, }; } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `idb-ui-input failed: ${error instanceof Error ? error.message : String(error)}` ); } } // ============================================================================ // INPUT EXECUTION // ============================================================================ /** * Execute IDB input command * * Why: Sends text or keyboard events to focused element. * Different command formats for text vs keys. */ async function executeInputCommand( udid: string, params: { operation: string; text?: string; key?: string; keySequence?: string[]; } ): Promise<{ success: boolean; output: string; error?: string }> { const { operation, text, key, keySequence } = params; // Validate UDID to prevent command injection if (!isValidUdid(udid)) { throw new McpError(ErrorCode.InvalidRequest, `Invalid UDID format: ${udid}`); } let args: string[]; switch (operation) { case 'text': { // Format: idb ui text --udid <UDID> <text-to-type> // No escaping needed - spawn passes arguments safely if (!text) { throw new McpError( ErrorCode.InvalidRequest, 'text parameter is required for text operation' ); } args = ['ui', 'text', '--udid', udid, text]; break; } case 'key': { // Format: idb ui key --udid <UDID> <key-name> if (!key) { throw new McpError(ErrorCode.InvalidRequest, 'key parameter is required for key operation'); } args = ['ui', 'key', '--udid', udid, key]; break; } case 'key-sequence': { // Format: idb ui key-sequence --udid <UDID> <key1> <key2> ... if (!keySequence || keySequence.length === 0) { throw new McpError( ErrorCode.InvalidRequest, 'keySequence parameter is required for key-sequence operation' ); } args = ['ui', 'key-sequence', '--udid', udid, ...keySequence]; break; } default: throw new McpError(ErrorCode.InvalidRequest, `Unknown operation: ${operation}`); } console.error(`[idb-ui-input] Executing: idb ${args.join(' ')}`); const result = await executeCommandWithArgs('idb', args, { timeout: 10000 }); return { success: result.code === 0, output: result.stdout, error: result.stderr ? formatToolError(result.stderr) : undefined, }; } // ============================================================================ // GUIDANCE FORMATTING // ============================================================================ function formatGuidance( success: boolean, target: any, context: { operation: string; displayText?: string; key?: string; keySequence?: string[]; actionName?: string; expectedOutcome?: string; resolvedUdid: string; } ): string[] { const { operation, displayText, key, keySequence, actionName, expectedOutcome, resolvedUdid } = context; if (success) { let inputDescription = ''; if (operation === 'text') { inputDescription = `text "${displayText}"`; } else if (operation === 'key') { inputDescription = `key "${key}"`; } else { inputDescription = `key sequence: ${keySequence?.join(' → ')}`; } return [ `✅ Input successful: ${inputDescription}`, actionName ? `Action: ${actionName}` : undefined, expectedOutcome ? `Expected: ${expectedOutcome}` : undefined, ``, `Next steps to verify input:`, `• Take screenshot: simctl-screenshot-inline --udid ${resolvedUdid}`, expectedOutcome ? `• Verify outcome: Check if ${expectedOutcome}` : `• Check UI state: Verify text field or UI updated`, operation === 'text' ? `• Submit form: idb-ui-input --operation key --key return` : undefined, `• Continue workflow: Use idb-ui-tap to proceed`, ].filter(Boolean) as string[]; } return [ `❌ Failed to input ${operation === 'text' ? 'text' : operation === 'key' ? `key "${key}"` : 'key sequence'}`, ``, `Troubleshooting:`, operation === 'text' ? [ `• Ensure text field is focused: Tap on field first with idb-ui-tap`, `• Check keyboard is visible: Some fields show keyboard automatically`, `• Verify field accepts text: Some fields may be read-only`, ] : [ `• Verify key name is valid: ${key || keySequence?.join(', ')}`, `• Check target supports key: Not all keys work on all devices`, `• Try alternative: Use idb-ui-tap for on-screen buttons`, ], ``, `Available keys:`, `home, lock, siri, delete, return, space, escape, tab, up, down, left, right`, ``, `Verify target state:`, `• idb-targets --operation describe --udid ${resolvedUdid}`, `• Take screenshot to see current UI`, ] .flat() .filter(Boolean) as string[]; } export const IDB_UI_INPUT_DOCS = ` # idb-ui-input ⌨️ **Input text and keyboard commands** - automated text entry and special key presses for form automation ## What it does Sends text input and keyboard commands to focused elements on iOS targets. Types text strings into active text fields, presses special keys (home, return, delete, arrows), and executes key sequences for complex input workflows. Automatically redacts sensitive data (passwords) in responses and provides semantic field context tracking for test documentation. ## Why you'd use it - Automate form filling without manual keyboard interaction - login flows, search, data entry - Execute keyboard shortcuts and navigation (tab, return, arrows) for multi-field workflows - Safely handle sensitive data with automatic redaction in tool responses and logs - Track input operations with semantic metadata (actionName, fieldContext, expectedOutcome) ## Parameters ### Required - **operation** (string): "text" | "key" | "key-sequence" ### Operation-specific parameters - **text** (string, required for text operation): String to type into focused field - **key** (string, required for key operation): Special key name (home, return, delete, tab, arrows, etc.) - **keySequence** (string[], required for key-sequence operation): Array of key names to press in order ### Optional - **udid** (string): Target identifier - auto-detects if omitted - **actionName** (string): Semantic action name (e.g., "Enter Email") - **fieldContext** (string): Field name for context (e.g., "Email TextField") - **expectedOutcome** (string): Expected result (e.g., "Email field populated") - **isSensitive** (boolean): Mark as sensitive to redact from output ## Returns Input execution status with operation details (redacted if sensitive), duration, input context metadata for test tracking, error details if failed, and troubleshooting guidance specific to text vs. key operations. ## Examples ### Type email into focused field \`\`\`typescript const result = await idbUiInputTool({ operation: 'text', text: 'user@example.com', actionName: 'Enter Email', fieldContext: 'Email TextField' }); \`\`\` ### Press return to submit \`\`\`typescript await idbUiInputTool({ operation: 'key', key: 'return' }); \`\`\` ## Related Tools - idb-ui-tap: Tap to focus text fields before typing - idb-ui-describe: Find text field coordinates `; export const IDB_UI_INPUT_DOCS_MINI = 'Input text and keyboard commands. Use rtfm({ toolName: "idb-ui-input" }) for docs.';

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