Skip to content
25 changes: 25 additions & 0 deletions manifests/tools/clone_sims.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
id: clone_sims
module: mcp/tools/simulator-management/clone_sims
names:
mcp: clone_sims
cli: clone
description: Clone an existing simulator.
outputSchema:
schema: xcodebuildmcp.output.simulator-action-result
version: "1"
annotations:
title: Clone Simulator
readOnlyHint: false
destructiveHint: false
openWorldHint: false
nextSteps:
- label: List simulators to see the clone
toolId: list_sims
priority: 1
when: success
- label: Boot the cloned simulator
toolId: boot_sim
params:
simulatorId: NEW_UDID
priority: 2
when: success
25 changes: 25 additions & 0 deletions manifests/tools/create_sim.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
id: create_sim
module: mcp/tools/simulator-management/create_sim
names:
mcp: create_sim
cli: create
description: Create a new simulator.
outputSchema:
schema: xcodebuildmcp.output.simulator-action-result
version: "1"
annotations:
title: Create Simulator
readOnlyHint: false
destructiveHint: false
openWorldHint: false
nextSteps:
- label: List simulators to see the new device
toolId: list_sims
priority: 1
when: success
- label: Boot the new simulator
toolId: boot_sim
params:
simulatorId: NEW_UDID
priority: 2
when: success
19 changes: 19 additions & 0 deletions manifests/tools/delete_sims.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
id: delete_sims
module: mcp/tools/simulator-management/delete_sims
names:
mcp: delete_sims
cli: delete
description: Delete simulators by UDID, all simulators, or unavailable simulators.
outputSchema:
schema: xcodebuildmcp.output.simulator-action-result
version: "1"
annotations:
title: Delete Simulators
readOnlyHint: false
destructiveHint: true
openWorldHint: false
nextSteps:
- label: List remaining simulators
toolId: list_sims
priority: 1
when: success
3 changes: 3 additions & 0 deletions manifests/workflows/simulator-management.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ tools:
- set_sim_location
- reset_sim_location
- set_sim_appearance
- clone_sims
- create_sim
- delete_sims
- sim_statusbar
- toggle_software_keyboard
- toggle_connect_hardware_keyboard
148 changes: 148 additions & 0 deletions src/mcp/tools/simulator-management/clone_sims.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import * as z from 'zod';
import type { ToolHandlerContext } from '../../../rendering/types.ts';
import type { SimulatorActionResultDomainResult } from '../../../types/domain-results.ts';
import type { NonStreamingExecutor } from '../../../types/tool-execution.ts';
import { log } from '../../../utils/logging/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
getHandlerContext,
toInternalSchema,
} from '../../../utils/typed-tool-factory.ts';
import { toErrorMessage } from '../../../utils/errors.ts';
import { createBasicDiagnostics } from '../../../utils/diagnostics.ts';

const baseSchemaObject = z.object({
sourceSimulatorId: z.string().uuid().describe('UDID of the simulator to clone'),
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
newName: z
.string()
.optional()
.describe('Name for the cloned simulator. If omitted, simctl auto-generates one.'),
});

const internalSchemaObject = z.object({
sourceSimulatorId: z.string().uuid(),
newName: z.string().optional(),
});

type CloneSimsParams = z.infer<typeof internalSchemaObject>;
type CloneSimsResult = SimulatorActionResultDomainResult;

const publicSchemaObject = z.strictObject(
baseSchemaObject.omit({
sourceSimulatorId: true,
newName: true,
} as const).shape,
);

function createCloneSimsResult(params: {
sourceSimulatorId: string;
didError: boolean;
error?: string;
diagnosticMessage?: string;
}): CloneSimsResult {
return {
kind: 'simulator-action-result',
didError: params.didError,
error: params.error ?? null,
summary: {
status: params.didError ? 'FAILED' : 'SUCCEEDED',
},
action: {
type: 'clone',
sourceSimulatorId: params.sourceSimulatorId,
},
Comment thread
cursor[bot] marked this conversation as resolved.
...(params.diagnosticMessage
? { diagnostics: createBasicDiagnostics({ errors: [params.diagnosticMessage] }) }
: {}),
artifacts: {
simulatorId: params.sourceSimulatorId,
},
};
Comment thread
sentry[bot] marked this conversation as resolved.
Outdated
}

function setStructuredOutput(ctx: ToolHandlerContext, result: CloneSimsResult): void {
ctx.structuredOutput = {
result,
schema: 'xcodebuildmcp.output.simulator-action-result',
schemaVersion: '1',
};
}

export function createCloneSimsExecutor(
executor: CommandExecutor,
): NonStreamingExecutor<CloneSimsParams, CloneSimsResult> {
return async (params) => {
try {
const command = ['xcrun', 'simctl', 'clone', params.sourceSimulatorId];
if (params.newName) {
command.push(params.newName);
}
Comment thread
cameroncooke marked this conversation as resolved.
Outdated

const result = await executor(command, 'Clone Simulator', false);

if (!result.success) {
const diagnosticMessage = result.error ?? 'Unknown error';
return createCloneSimsResult({
sourceSimulatorId: params.sourceSimulatorId,
didError: true,
error: 'Clone simulator failed.',
diagnosticMessage,
});
}

return createCloneSimsResult({
sourceSimulatorId: params.sourceSimulatorId,
didError: false,
});
} catch (error) {
const diagnosticMessage = toErrorMessage(error);
return createCloneSimsResult({
sourceSimulatorId: params.sourceSimulatorId,
didError: true,
error: 'Clone simulator failed.',
diagnosticMessage,
});
}
};
}

export async function clone_simsLogic(
params: CloneSimsParams,
executor: CommandExecutor,
): Promise<void> {
log(
'info',
`Cloning simulator ${params.sourceSimulatorId}${params.newName ? ` as "${params.newName}"` : ''}`,
);

const ctx = getHandlerContext();
const executeCloneSims = createCloneSimsExecutor(executor);
const result = await executeCloneSims(params);
setStructuredOutput(ctx, result);

if (result.didError) {
log('error', `Error cloning simulator: ${result.error ?? 'Unknown error'}`);
return;
}

ctx.nextStepParams = {
boot_sim: { simulatorId: params.sourceSimulatorId },
open_sim: {},
list_sims: {},
};
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
sentry[bot] marked this conversation as resolved.
}

export const schema = getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: baseSchemaObject,
});

export const handler = createSessionAwareTool<CloneSimsParams>({
internalSchema: toInternalSchema<CloneSimsParams>(internalSchemaObject),
logicFunction: clone_simsLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [],
});
164 changes: 164 additions & 0 deletions src/mcp/tools/simulator-management/create_sim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import * as z from 'zod';
import type { ToolHandlerContext } from '../../../rendering/types.ts';
import type { SimulatorActionResultDomainResult } from '../../../types/domain-results.ts';
import type { NonStreamingExecutor } from '../../../types/tool-execution.ts';
import { log } from '../../../utils/logging/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
getHandlerContext,
toInternalSchema,
} from '../../../utils/typed-tool-factory.ts';
import { toErrorMessage } from '../../../utils/errors.ts';
import { createBasicDiagnostics } from '../../../utils/diagnostics.ts';

const baseSchemaObject = z.object({
name: z.string().min(1).describe('Name for the new simulator (e.g., "iPhone 17 Test")'),
deviceType: z
.string()
.min(1)
.describe(
'Device type identifier (e.g., "iPhone 17" or "com.apple.CoreSimulator.SimDeviceType.iPhone-17"). Use list_sims to see available device types.',
),
runtime: z
.string()
.min(1)
.describe(
'Runtime identifier (e.g., "iOS 26" or "com.apple.CoreSimulator.SimRuntime.iOS-26"). Use list_sims to see available runtimes.',
),
});

const internalSchemaObject = z.object({
name: z.string().min(1),
deviceType: z.string().min(1),
runtime: z.string().min(1),
});

type CreateSimParams = z.infer<typeof internalSchemaObject>;
type CreateSimResult = SimulatorActionResultDomainResult;

const publicSchemaObject = z.strictObject(
baseSchemaObject.omit({
name: true,
deviceType: true,
runtime: true,
} as const).shape,
);
Comment thread
sentry[bot] marked this conversation as resolved.
Outdated
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

function createCreateSimResult(params: {
name: string;
deviceType: string;
runtime: string;
didError: boolean;
error?: string;
diagnosticMessage?: string;
}): CreateSimResult {
return {
kind: 'simulator-action-result',
didError: params.didError,
error: params.error ?? null,
summary: {
status: params.didError ? 'FAILED' : 'SUCCEEDED',
},
action: {
type: 'create',
name: params.name,
deviceType: params.deviceType,
runtime: params.runtime,
},
...(params.diagnosticMessage
? { diagnostics: createBasicDiagnostics({ errors: [params.diagnosticMessage] }) }
: {}),
};
}

function setStructuredOutput(ctx: ToolHandlerContext, result: CreateSimResult): void {
ctx.structuredOutput = {
result,
schema: 'xcodebuildmcp.output.simulator-action-result',
schemaVersion: '1',
};
}

export function createCreateSimExecutor(
executor: CommandExecutor,
): NonStreamingExecutor<CreateSimParams, CreateSimResult> {
return async (params) => {
try {
const result = await executor(
['xcrun', 'simctl', 'create', params.name, params.deviceType, params.runtime],
'Create Simulator',
false,
);

if (!result.success) {
const diagnosticMessage = result.error ?? 'Unknown error';
return createCreateSimResult({
name: params.name,
deviceType: params.deviceType,
runtime: params.runtime,
didError: true,
error: 'Create simulator failed.',
diagnosticMessage,
});
}

return createCreateSimResult({
name: params.name,
deviceType: params.deviceType,
runtime: params.runtime,
didError: false,
});
} catch (error) {
const diagnosticMessage = toErrorMessage(error);
return createCreateSimResult({
name: params.name,
deviceType: params.deviceType,
runtime: params.runtime,
didError: true,
error: 'Create simulator failed.',
diagnosticMessage,
});
}
};
}

export async function create_simLogic(
params: CreateSimParams,
executor: CommandExecutor,
): Promise<void> {
log(
'info',
`Creating simulator "${params.name}" (device type: ${params.deviceType}, runtime: ${params.runtime})`,
);

const ctx = getHandlerContext();
const executeCreateSim = createCreateSimExecutor(executor);
const result = await executeCreateSim(params);
setStructuredOutput(ctx, result);

if (result.didError) {
log('error', `Error creating simulator: ${result.error ?? 'Unknown error'}`);
return;
}

ctx.nextStepParams = {
boot_sim: {},
open_sim: {},
list_sims: {},
};
}

export const schema = getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: baseSchemaObject,
});

export const handler = createSessionAwareTool<CreateSimParams>({
internalSchema: toInternalSchema<CreateSimParams>(internalSchemaObject),
logicFunction: create_simLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [],
});
Loading
Loading