Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 192 additions & 38 deletions src/tools/simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ToolResponse> {
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',
Expand All @@ -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<ToolResponse> => {
const simulatorUuidValidation = validateRequiredParam('simulatorUuid', params.simulatorUuid);
if (!simulatorUuidValidation.isValid) {
return simulatorUuidValidation.errorResponse!;
}

async (params: { simulatorUuid: string; mode: 'dark' | 'light' }): Promise<ToolResponse> => {
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<ToolResponse> => {
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<ToolResponse> => {
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<ToolResponse> => {
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<ToolResponse> => {
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',
);
},
);
}
24 changes: 24 additions & 0 deletions src/utils/register-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ import {
registerLaunchAppInSimulatorTool,
registerLaunchAppWithLogsInSimulatorTool,
registerSetSimulatorAppearanceTool,
registerSetSimulatorLocationTool,
registerResetSimulatorLocationTool,
registerSetNetworkConditionTool,
registerResetNetworkConditionTool,
} from '../tools/simulator.js';

// Import bundle ID tools
Expand Down Expand Up @@ -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
{
Expand Down