diff --git a/src/tools/simulator.ts b/src/tools/simulator.ts index f6322933..8751be81 100644 --- a/src/tools/simulator.ts +++ b/src/tools/simulator.ts @@ -496,6 +496,66 @@ export function registerOpenSimulatorTool(server: McpServer): void { ); } +// Helper function to execute simctl commands and handle responses +async function executeSimctlCommandAndRespond( + params: { simulatorUuid: string; [key: string]: unknown }, + simctlSubCommand: string[], + operationDescriptionForXcodeCommand: string, + successMessage: string, + failureMessagePrefix: string, + operationLogContext: string, + extraValidation?: () => ToolResponse | null, +): Promise { + const simulatorUuidValidation = validateRequiredParam( + 'simulatorUuid', + params.simulatorUuid as string, + ); + if (!simulatorUuidValidation.isValid) { + return simulatorUuidValidation.errorResponse!; + } + + if (extraValidation) { + const validationResult = extraValidation(); + if (validationResult) { + return validationResult; + } + } + + try { + const command = ['xcrun', 'simctl', ...simctlSubCommand]; + const result = await executeXcodeCommand(command, operationDescriptionForXcodeCommand); + + if (!result.success) { + const fullFailureMessage = `${failureMessagePrefix}: ${result.error}`; + log( + 'error', + `${fullFailureMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorUuid})`, + ); + return { + content: [{ type: 'text', text: fullFailureMessage }], + }; + } + + log( + 'info', + `${successMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorUuid})`, + ); + return { + content: [{ type: 'text', text: successMessage }], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const fullFailureMessage = `${failureMessagePrefix}: ${errorMessage}`; + log( + 'error', + `Error during ${operationLogContext} for simulator ${params.simulatorUuid}: ${errorMessage}`, + ); + return { + content: [{ type: 'text', text: fullFailureMessage }], + }; + } +} + export function registerSetSimulatorAppearanceTool(server: McpServer): void { server.tool( 'set_sim_appearance', @@ -508,49 +568,143 @@ export function registerSetSimulatorAppearanceTool(server: McpServer): void { .enum(['dark', 'light']) .describe('The appearance mode to set (either "dark" or "light")'), }, - async (params): Promise => { - const simulatorUuidValidation = validateRequiredParam('simulatorUuid', params.simulatorUuid); - if (!simulatorUuidValidation.isValid) { - return simulatorUuidValidation.errorResponse!; - } - + async (params: { simulatorUuid: string; mode: 'dark' | 'light' }): Promise => { log('info', `Setting simulator ${params.simulatorUuid} appearance to ${params.mode} mode`); - try { - const command = ['xcrun', 'simctl', 'ui', params.simulatorUuid, 'appearance', params.mode]; - const result = await executeXcodeCommand(command, 'Set Simulator Appearance'); + return executeSimctlCommandAndRespond( + params, + ['ui', params.simulatorUuid, 'appearance', params.mode], + 'Set Simulator Appearance', + `Successfully set simulator ${params.simulatorUuid} appearance to ${params.mode} mode`, + 'Failed to set simulator appearance', + 'set simulator appearance', + ); + }, + ); +} - if (!result.success) { - return { - content: [ - { - type: 'text', - text: `Failed to set simulator appearance: ${result.error}`, - }, - ], - }; +export function registerSetSimulatorLocationTool(server: McpServer): void { + server.tool( + 'set_simulator_location', + 'Sets a custom GPS location for the simulator.', + { + simulatorUuid: z + .string() + .describe('UUID of the simulator to use (obtained from list_simulators)'), + latitude: z.number().describe('The latitude for the custom location.'), + longitude: z.number().describe('The longitude for the custom location.'), + }, + async (params: { + simulatorUuid: string; + latitude: number; + longitude: number; + }): Promise => { + const extraValidation = (): ToolResponse | null => { + const latitudeValidation = validateRequiredParam('latitude', params.latitude); + if (!latitudeValidation.isValid) { + return latitudeValidation.errorResponse!; + } + const longitudeValidation = validateRequiredParam('longitude', params.longitude); + if (!longitudeValidation.isValid) { + return longitudeValidation.errorResponse!; } + return null; + }; - return { - content: [ - { - type: 'text', - text: `Successfully set simulator ${params.simulatorUuid} to ${params.mode} mode`, - }, - ], - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - log('error', `Error setting simulator appearance: ${errorMessage}`); - return { - content: [ - { - type: 'text', - text: `Failed to set simulator appearance: ${errorMessage}`, - }, - ], - }; - } + log( + 'info', + `Setting simulator ${params.simulatorUuid} location to ${params.latitude},${params.longitude}`, + ); + + return executeSimctlCommandAndRespond( + params, + ['location', params.simulatorUuid, 'set', `${params.latitude},${params.longitude}`], + 'Set Simulator Location', + `Successfully set simulator ${params.simulatorUuid} location to ${params.latitude},${params.longitude}`, + 'Failed to set simulator location', + 'set simulator location', + extraValidation, + ); + }, + ); +} + +export function registerResetSimulatorLocationTool(server: McpServer): void { + server.tool( + 'reset_simulator_location', + "Resets the simulator's location to default.", + { + simulatorUuid: z + .string() + .describe('UUID of the simulator to use (obtained from list_simulators)'), + }, + async (params: { simulatorUuid: string }): Promise => { + log('info', `Resetting simulator ${params.simulatorUuid} location`); + + return executeSimctlCommandAndRespond( + params, + ['location', params.simulatorUuid, 'clear'], + 'Reset Simulator Location', + `Successfully reset simulator ${params.simulatorUuid} location.`, + 'Failed to reset simulator location', + 'reset simulator location', + ); + }, + ); +} + +export function registerSetNetworkConditionTool(server: McpServer): void { + server.tool( + 'set_network_condition', + 'Simulates different network conditions (e.g., wifi, 3g, edge, high-latency, dsl, 100%loss, 3g-lossy, very-lossy) in the simulator.', + { + simulatorUuid: z + .string() + .describe('UUID of the simulator to use (obtained from list_simulators)'), + profile: z + .enum(['wifi', '3g', 'edge', 'high-latency', 'dsl', '100%loss', '3g-lossy', 'very-lossy']) + .describe( + 'The network profile to simulate. Must be one of: wifi, 3g, edge, high-latency, dsl, 100%loss, 3g-lossy, very-lossy.', + ), + }, + async (params: { simulatorUuid: string; profile: string }): Promise => { + log( + 'info', + `Setting simulator ${params.simulatorUuid} network condition to ${params.profile}`, + ); + + return executeSimctlCommandAndRespond( + params, + ['status_bar', params.simulatorUuid, 'override', '--dataNetwork', params.profile], + 'Set Network Condition', + `Successfully set simulator ${params.simulatorUuid} network condition to ${params.profile} profile`, + 'Failed to set network condition', + 'set network condition', + ); + }, + ); +} + +export function registerResetNetworkConditionTool(server: McpServer): void { + server.tool( + 'reset_network_condition', + 'Resets network conditions to default in the simulator.', + { + simulatorUuid: z + .string() + .describe('UUID of the simulator to use (obtained from list_simulators)'), + }, + async (params: { simulatorUuid: string }): Promise => { + log('info', `Resetting simulator ${params.simulatorUuid} network condition`); + + return executeSimctlCommandAndRespond( + params, + ['status_bar', params.simulatorUuid, 'clear'], + 'Reset Network Condition', + `Successfully reset simulator ${params.simulatorUuid} network conditions.`, + 'Failed to reset network condition', + 'reset network condition', + ); }, ); } diff --git a/src/utils/register-tools.ts b/src/utils/register-tools.ts index 0c9d0e55..4c1a8e8e 100644 --- a/src/utils/register-tools.ts +++ b/src/utils/register-tools.ts @@ -50,6 +50,10 @@ import { registerLaunchAppInSimulatorTool, registerLaunchAppWithLogsInSimulatorTool, registerSetSimulatorAppearanceTool, + registerSetSimulatorLocationTool, + registerResetSimulatorLocationTool, + registerSetNetworkConditionTool, + registerResetNetworkConditionTool, } from '../tools/simulator.js'; // Import bundle ID tools @@ -284,6 +288,26 @@ const toolRegistrations = [ groups: [ToolGroup.SIMULATOR_MANAGEMENT], envVar: 'XCODEBUILDMCP_TOOL_SET_SIMULATOR_APPEARANCE', }, + { + register: registerSetSimulatorLocationTool, + groups: [ToolGroup.SIMULATOR_MANAGEMENT], + envVar: 'XCODEBUILDMCP_TOOL_SET_SIMULATOR_LOCATION', + }, + { + register: registerResetSimulatorLocationTool, + groups: [ToolGroup.SIMULATOR_MANAGEMENT], + envVar: 'XCODEBUILDMCP_TOOL_RESET_SIMULATOR_LOCATION', + }, + { + register: registerSetNetworkConditionTool, + groups: [ToolGroup.SIMULATOR_MANAGEMENT], + envVar: 'XCODEBUILDMCP_TOOL_SET_NETWORK_CONDITION', + }, + { + register: registerResetNetworkConditionTool, + groups: [ToolGroup.SIMULATOR_MANAGEMENT], + envVar: 'XCODEBUILDMCP_TOOL_RESET_NETWORK_CONDITION', + }, // App installation and launch tools {