From e549df1788a2ab55fc5e2b543ddc223176807267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=92=8B=E8=89=BA?= Date: Thu, 14 May 2026 22:53:09 +0800 Subject: [PATCH 1/4] feat(simctl): Add clone, create, and delete simulator management commands Wraps xcrun simctl clone, create, and delete as MCP/CLI tools, registered under the simulator-management workflow. Co-Authored-By: deepseek v4 pro --- manifests/tools/clone_sims.yaml | 22 ++++ manifests/tools/create_sim.yaml | 22 ++++ manifests/tools/delete_sims.yaml | 16 +++ manifests/workflows/simulator-management.yaml | 3 + .../tools/simulator-management/clone_sims.ts | 72 +++++++++++++ .../tools/simulator-management/create_sim.ts | 77 +++++++++++++ .../tools/simulator-management/delete_sims.ts | 101 ++++++++++++++++++ 7 files changed, 313 insertions(+) create mode 100644 manifests/tools/clone_sims.yaml create mode 100644 manifests/tools/create_sim.yaml create mode 100644 manifests/tools/delete_sims.yaml create mode 100644 src/mcp/tools/simulator-management/clone_sims.ts create mode 100644 src/mcp/tools/simulator-management/create_sim.ts create mode 100644 src/mcp/tools/simulator-management/delete_sims.ts diff --git a/manifests/tools/clone_sims.yaml b/manifests/tools/clone_sims.yaml new file mode 100644 index 00000000..a20ac667 --- /dev/null +++ b/manifests/tools/clone_sims.yaml @@ -0,0 +1,22 @@ +id: clone_sims +module: mcp/tools/simulator-management/clone_sims +names: + mcp: clone_sims + cli: clone +description: Clone an existing simulator. +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 diff --git a/manifests/tools/create_sim.yaml b/manifests/tools/create_sim.yaml new file mode 100644 index 00000000..bf6f3825 --- /dev/null +++ b/manifests/tools/create_sim.yaml @@ -0,0 +1,22 @@ +id: create_sim +module: mcp/tools/simulator-management/create_sim +names: + mcp: create_sim + cli: create +description: Create a new simulator. +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 diff --git a/manifests/tools/delete_sims.yaml b/manifests/tools/delete_sims.yaml new file mode 100644 index 00000000..26b6c4a3 --- /dev/null +++ b/manifests/tools/delete_sims.yaml @@ -0,0 +1,16 @@ +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. +annotations: + title: Delete Simulators + readOnlyHint: false + destructiveHint: true + openWorldHint: false +nextSteps: + - label: List remaining simulators + toolId: list_sims + priority: 1 + when: success diff --git a/manifests/workflows/simulator-management.yaml b/manifests/workflows/simulator-management.yaml index c1ce1ddc..7afe8ef2 100644 --- a/manifests/workflows/simulator-management.yaml +++ b/manifests/workflows/simulator-management.yaml @@ -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 diff --git a/src/mcp/tools/simulator-management/clone_sims.ts b/src/mcp/tools/simulator-management/clone_sims.ts new file mode 100644 index 00000000..89e78622 --- /dev/null +++ b/src/mcp/tools/simulator-management/clone_sims.ts @@ -0,0 +1,72 @@ +import * as z from 'zod'; +import { log } from '../../../utils/logging/index.ts'; +import type { CommandExecutor } from '../../../utils/execution/index.ts'; +import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; +import { createTypedTool, getHandlerContext } from '../../../utils/typed-tool-factory.ts'; +import { withErrorHandling } from '../../../utils/tool-error-handling.ts'; +import { header, statusLine } from '../../../utils/tool-event-builders.ts'; + +const cloneSimsSchema = z.object({ + sourceSimulatorId: z.string().uuid().describe('UDID of the simulator to clone'), + newName: z + .string() + .optional() + .describe('Name for the cloned simulator. If omitted, simctl auto-generates one.'), +}); + +type CloneSimsParams = z.infer; + +export async function clone_simsLogic( + params: CloneSimsParams, + executor: CommandExecutor, +): Promise { + log( + 'info', + `Cloning simulator ${params.sourceSimulatorId}${params.newName ? ` as "${params.newName}"` : ''}`, + ); + + const headerEvent = header('Clone Simulator', [ + { label: 'Source', value: params.sourceSimulatorId }, + ...(params.newName ? [{ label: 'New Name', value: params.newName }] : []), + ]); + + const ctx = getHandlerContext(); + + return withErrorHandling( + ctx, + async () => { + const command = ['xcrun', 'simctl', 'clone', params.sourceSimulatorId]; + if (params.newName) { + command.push(params.newName); + } + + const result = await executor(command, 'Clone Simulator', false); + + if (!result.success) { + ctx.emit(headerEvent); + ctx.emit(statusLine('error', `Clone simulator failed: ${result.error}`)); + return; + } + + const newUdid = result.output.trim(); + ctx.emit(headerEvent); + ctx.emit(statusLine('success', `Simulator cloned successfully. New UDID: ${newUdid}`)); + ctx.nextStepParams = { + boot_sim: { simulatorId: newUdid }, + open_sim: {}, + install_app_sim: { simulatorId: newUdid, appPath: 'PATH_TO_YOUR_APP' }, + launch_app_sim: { simulatorId: newUdid, bundleId: 'YOUR_APP_BUNDLE_ID' }, + list_sims: {}, + }; + }, + { + header: headerEvent, + errorMessage: ({ message }) => `Clone simulator failed: ${message}`, + logMessage: ({ message }) => `Error cloning simulator: ${message}`, + }, + ); +} + +export const schema = cloneSimsSchema.shape; + +export const handler = createTypedTool(cloneSimsSchema, clone_simsLogic, getDefaultCommandExecutor); diff --git a/src/mcp/tools/simulator-management/create_sim.ts b/src/mcp/tools/simulator-management/create_sim.ts new file mode 100644 index 00000000..e7bf7f6e --- /dev/null +++ b/src/mcp/tools/simulator-management/create_sim.ts @@ -0,0 +1,77 @@ +import * as z from 'zod'; +import { log } from '../../../utils/logging/index.ts'; +import type { CommandExecutor } from '../../../utils/execution/index.ts'; +import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; +import { createTypedTool, getHandlerContext } from '../../../utils/typed-tool-factory.ts'; +import { withErrorHandling } from '../../../utils/tool-error-handling.ts'; +import { header, statusLine } from '../../../utils/tool-event-builders.ts'; + +const createSimSchema = 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.', + ), +}); + +type CreateSimParams = z.infer; + +export async function create_simLogic( + params: CreateSimParams, + executor: CommandExecutor, +): Promise { + log( + 'info', + `Creating simulator "${params.name}" (device type: ${params.deviceType}, runtime: ${params.runtime})`, + ); + + const headerEvent = header('Create Simulator', [ + { label: 'Name', value: params.name }, + { label: 'Device Type', value: params.deviceType }, + { label: 'Runtime', value: params.runtime }, + ]); + + const ctx = getHandlerContext(); + + return withErrorHandling( + ctx, + async () => { + const command = ['xcrun', 'simctl', 'create', params.name, params.deviceType, params.runtime]; + const result = await executor(command, 'Create Simulator', false); + + if (!result.success) { + ctx.emit(headerEvent); + ctx.emit(statusLine('error', `Create simulator failed: ${result.error}`)); + return; + } + + const newUdid = result.output.trim(); + ctx.emit(headerEvent); + ctx.emit(statusLine('success', `Simulator created successfully. New UDID: ${newUdid}`)); + ctx.nextStepParams = { + boot_sim: { simulatorId: newUdid }, + open_sim: {}, + install_app_sim: { simulatorId: newUdid, appPath: 'PATH_TO_YOUR_APP' }, + launch_app_sim: { simulatorId: newUdid, bundleId: 'YOUR_APP_BUNDLE_ID' }, + list_sims: {}, + }; + }, + { + header: headerEvent, + errorMessage: ({ message }) => `Create simulator failed: ${message}`, + logMessage: ({ message }) => `Error creating simulator: ${message}`, + }, + ); +} + +export const schema = createSimSchema.shape; + +export const handler = createTypedTool(createSimSchema, create_simLogic, getDefaultCommandExecutor); diff --git a/src/mcp/tools/simulator-management/delete_sims.ts b/src/mcp/tools/simulator-management/delete_sims.ts new file mode 100644 index 00000000..23efcb85 --- /dev/null +++ b/src/mcp/tools/simulator-management/delete_sims.ts @@ -0,0 +1,101 @@ +import * as z from 'zod'; +import { log } from '../../../utils/logging/index.ts'; +import type { CommandExecutor } from '../../../utils/execution/index.ts'; +import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; +import { createTypedTool, getHandlerContext } from '../../../utils/typed-tool-factory.ts'; +import { withErrorHandling } from '../../../utils/tool-error-handling.ts'; +import { header, section, statusLine } from '../../../utils/tool-event-builders.ts'; + +const deleteSimsSchema = z.object({ + target: z + .string() + .min(1) + .describe( + 'UDID of the simulator to delete, "all" to delete all simulators, or "unavailable" to delete unavailable simulators.', + ), + shutdownFirst: z + .boolean() + .optional() + .describe('Shutdown the simulator before deleting. Useful for booted simulators.'), +}); + +type DeleteSimsParams = z.infer; + +export async function delete_simsLogic( + params: DeleteSimsParams, + executor: CommandExecutor, +): Promise { + const target = params.target; + const headerEvent = header('Delete Simulator', [ + { label: 'Target', value: target }, + ...(params.shutdownFirst ? [{ label: 'Shutdown First', value: 'true' }] : []), + ]); + + const ctx = getHandlerContext(); + + return withErrorHandling( + ctx, + async () => { + log( + 'info', + `Deleting simulator(s) ${target}${params.shutdownFirst ? ' (shutdownFirst=true)' : ''}`, + ); + + if (params.shutdownFirst && target !== 'all' && target !== 'unavailable') { + try { + await executor( + ['xcrun', 'simctl', 'shutdown', target], + 'Shutdown Simulator', + true, + undefined, + ); + } catch { + // ignore shutdown errors; proceed to delete attempt + } + } + + const result = await executor( + ['xcrun', 'simctl', 'delete', target], + 'Delete Simulator', + true, + undefined, + ); + if (result.success) { + ctx.emit(headerEvent); + ctx.emit(statusLine('success', 'Simulator(s) deleted successfully')); + ctx.nextStepParams = { + list_sims: {}, + }; + return; + } + + const errText = result.error ?? 'Unknown error'; + if (/Unable to delete.*Booted/i.test(errText) && !params.shutdownFirst) { + ctx.emit(headerEvent); + ctx.emit(statusLine('error', `Failed to delete simulator: ${errText}`)); + ctx.emit( + section('Hint', [ + `The simulator appears to be Booted. Re-run delete_sims with { target: '${target}', shutdownFirst: true } to shut it down before deleting.`, + ]), + ); + return; + } + + ctx.emit(headerEvent); + ctx.emit(statusLine('error', `Failed to delete simulator: ${errText}`)); + }, + { + header: headerEvent, + errorMessage: ({ message }) => `Failed to delete simulator: ${message}`, + logMessage: ({ message }) => `Error deleting simulators: ${message}`, + }, + ); +} + +export const schema = deleteSimsSchema.shape; + +export const handler = createTypedTool( + deleteSimsSchema, + delete_simsLogic, + getDefaultCommandExecutor, +); From c63c989136055a84a39658b659f6b5e3e124b881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=92=8B=E8=89=BA?= Date: Thu, 14 May 2026 23:28:43 +0800 Subject: [PATCH 2/4] fix(simctl): Rewrite simctl tools to match upstream architecture Use NonStreamingExecutor pattern with SimulatorActionResultDomainResult, add action types to domain-results and renderer labels. Co-Authored-By: deepseek v4 pro --- .../tools/simulator-management/clone_sims.ts | 160 +++++++++++----- .../tools/simulator-management/create_sim.ts | 169 ++++++++++++----- .../tools/simulator-management/delete_sims.ts | 172 ++++++++++++------ src/types/domain-results.ts | 17 ++ src/utils/renderers/domain-result-text.ts | 6 + 5 files changed, 387 insertions(+), 137 deletions(-) diff --git a/src/mcp/tools/simulator-management/clone_sims.ts b/src/mcp/tools/simulator-management/clone_sims.ts index 89e78622..0abaeb36 100644 --- a/src/mcp/tools/simulator-management/clone_sims.ts +++ b/src/mcp/tools/simulator-management/clone_sims.ts @@ -1,12 +1,20 @@ 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 { createTypedTool, getHandlerContext } from '../../../utils/typed-tool-factory.ts'; -import { withErrorHandling } from '../../../utils/tool-error-handling.ts'; -import { header, statusLine } from '../../../utils/tool-event-builders.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 cloneSimsSchema = z.object({ +const baseSchemaObject = z.object({ sourceSimulatorId: z.string().uuid().describe('UDID of the simulator to clone'), newName: z .string() @@ -14,27 +22,60 @@ const cloneSimsSchema = z.object({ .describe('Name for the cloned simulator. If omitted, simctl auto-generates one.'), }); -type CloneSimsParams = z.infer; +const internalSchemaObject = z.object({ + sourceSimulatorId: z.string().uuid(), + newName: z.string().optional(), +}); -export async function clone_simsLogic( - params: CloneSimsParams, - executor: CommandExecutor, -): Promise { - log( - 'info', - `Cloning simulator ${params.sourceSimulatorId}${params.newName ? ` as "${params.newName}"` : ''}`, - ); +type CloneSimsParams = z.infer; +type CloneSimsResult = SimulatorActionResultDomainResult; - const headerEvent = header('Clone Simulator', [ - { label: 'Source', value: params.sourceSimulatorId }, - ...(params.newName ? [{ label: 'New Name', value: params.newName }] : []), - ]); +const publicSchemaObject = z.strictObject( + baseSchemaObject.omit({ + sourceSimulatorId: true, + newName: true, + } as const).shape, +); - const ctx = getHandlerContext(); +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, + }, + ...(params.diagnosticMessage + ? { diagnostics: createBasicDiagnostics({ errors: [params.diagnosticMessage] }) } + : {}), + artifacts: { + simulatorId: params.sourceSimulatorId, + }, + }; +} + +function setStructuredOutput(ctx: ToolHandlerContext, result: CloneSimsResult): void { + ctx.structuredOutput = { + result, + schema: 'xcodebuildmcp.output.simulator-action-result', + schemaVersion: '1', + }; +} - return withErrorHandling( - ctx, - async () => { +export function createCloneSimsExecutor( + executor: CommandExecutor, +): NonStreamingExecutor { + return async (params) => { + try { const command = ['xcrun', 'simctl', 'clone', params.sourceSimulatorId]; if (params.newName) { command.push(params.newName); @@ -43,30 +84,65 @@ export async function clone_simsLogic( const result = await executor(command, 'Clone Simulator', false); if (!result.success) { - ctx.emit(headerEvent); - ctx.emit(statusLine('error', `Clone simulator failed: ${result.error}`)); - return; + const diagnosticMessage = result.error ?? 'Unknown error'; + return createCloneSimsResult({ + sourceSimulatorId: params.sourceSimulatorId, + didError: true, + error: 'Clone simulator failed.', + diagnosticMessage, + }); } - const newUdid = result.output.trim(); - ctx.emit(headerEvent); - ctx.emit(statusLine('success', `Simulator cloned successfully. New UDID: ${newUdid}`)); - ctx.nextStepParams = { - boot_sim: { simulatorId: newUdid }, - open_sim: {}, - install_app_sim: { simulatorId: newUdid, appPath: 'PATH_TO_YOUR_APP' }, - launch_app_sim: { simulatorId: newUdid, bundleId: 'YOUR_APP_BUNDLE_ID' }, - list_sims: {}, - }; - }, - { - header: headerEvent, - errorMessage: ({ message }) => `Clone simulator failed: ${message}`, - logMessage: ({ message }) => `Error cloning simulator: ${message}`, - }, + 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 { + 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: {}, + }; } -export const schema = cloneSimsSchema.shape; +export const schema = getSessionAwareToolSchemaShape({ + sessionAware: publicSchemaObject, + legacy: baseSchemaObject, +}); -export const handler = createTypedTool(cloneSimsSchema, clone_simsLogic, getDefaultCommandExecutor); +export const handler = createSessionAwareTool({ + internalSchema: toInternalSchema(internalSchemaObject), + logicFunction: clone_simsLogic, + getExecutor: getDefaultCommandExecutor, + requirements: [], +}); diff --git a/src/mcp/tools/simulator-management/create_sim.ts b/src/mcp/tools/simulator-management/create_sim.ts index e7bf7f6e..81688fd0 100644 --- a/src/mcp/tools/simulator-management/create_sim.ts +++ b/src/mcp/tools/simulator-management/create_sim.ts @@ -1,12 +1,20 @@ 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 { createTypedTool, getHandlerContext } from '../../../utils/typed-tool-factory.ts'; -import { withErrorHandling } from '../../../utils/tool-error-handling.ts'; -import { header, statusLine } from '../../../utils/tool-event-builders.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 createSimSchema = z.object({ +const baseSchemaObject = z.object({ name: z.string().min(1).describe('Name for the new simulator (e.g., "iPhone 17 Test")'), deviceType: z .string() @@ -22,7 +30,100 @@ const createSimSchema = z.object({ ), }); -type CreateSimParams = z.infer; +const internalSchemaObject = z.object({ + name: z.string().min(1), + deviceType: z.string().min(1), + runtime: z.string().min(1), +}); + +type CreateSimParams = z.infer; +type CreateSimResult = SimulatorActionResultDomainResult; + +const publicSchemaObject = z.strictObject( + baseSchemaObject.omit({ + name: true, + deviceType: true, + runtime: true, + } as const).shape, +); + +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 { + 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, @@ -33,45 +134,31 @@ export async function create_simLogic( `Creating simulator "${params.name}" (device type: ${params.deviceType}, runtime: ${params.runtime})`, ); - const headerEvent = header('Create Simulator', [ - { label: 'Name', value: params.name }, - { label: 'Device Type', value: params.deviceType }, - { label: 'Runtime', value: params.runtime }, - ]); - const ctx = getHandlerContext(); + const executeCreateSim = createCreateSimExecutor(executor); + const result = await executeCreateSim(params); + setStructuredOutput(ctx, result); - return withErrorHandling( - ctx, - async () => { - const command = ['xcrun', 'simctl', 'create', params.name, params.deviceType, params.runtime]; - const result = await executor(command, 'Create Simulator', false); - - if (!result.success) { - ctx.emit(headerEvent); - ctx.emit(statusLine('error', `Create simulator failed: ${result.error}`)); - return; - } + if (result.didError) { + log('error', `Error creating simulator: ${result.error ?? 'Unknown error'}`); + return; + } - const newUdid = result.output.trim(); - ctx.emit(headerEvent); - ctx.emit(statusLine('success', `Simulator created successfully. New UDID: ${newUdid}`)); - ctx.nextStepParams = { - boot_sim: { simulatorId: newUdid }, - open_sim: {}, - install_app_sim: { simulatorId: newUdid, appPath: 'PATH_TO_YOUR_APP' }, - launch_app_sim: { simulatorId: newUdid, bundleId: 'YOUR_APP_BUNDLE_ID' }, - list_sims: {}, - }; - }, - { - header: headerEvent, - errorMessage: ({ message }) => `Create simulator failed: ${message}`, - logMessage: ({ message }) => `Error creating simulator: ${message}`, - }, - ); + ctx.nextStepParams = { + boot_sim: {}, + open_sim: {}, + list_sims: {}, + }; } -export const schema = createSimSchema.shape; +export const schema = getSessionAwareToolSchemaShape({ + sessionAware: publicSchemaObject, + legacy: baseSchemaObject, +}); -export const handler = createTypedTool(createSimSchema, create_simLogic, getDefaultCommandExecutor); +export const handler = createSessionAwareTool({ + internalSchema: toInternalSchema(internalSchemaObject), + logicFunction: create_simLogic, + getExecutor: getDefaultCommandExecutor, + requirements: [], +}); diff --git a/src/mcp/tools/simulator-management/delete_sims.ts b/src/mcp/tools/simulator-management/delete_sims.ts index 23efcb85..bd8512ba 100644 --- a/src/mcp/tools/simulator-management/delete_sims.ts +++ b/src/mcp/tools/simulator-management/delete_sims.ts @@ -1,12 +1,20 @@ 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 { createTypedTool, getHandlerContext } from '../../../utils/typed-tool-factory.ts'; -import { withErrorHandling } from '../../../utils/tool-error-handling.ts'; -import { header, section, statusLine } from '../../../utils/tool-event-builders.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 deleteSimsSchema = z.object({ +const baseSchemaObject = z.object({ target: z .string() .min(1) @@ -19,27 +27,58 @@ const deleteSimsSchema = z.object({ .describe('Shutdown the simulator before deleting. Useful for booted simulators.'), }); -type DeleteSimsParams = z.infer; +const internalSchemaObject = z.object({ + target: z.string().min(1), + shutdownFirst: z.boolean().optional(), +}); -export async function delete_simsLogic( - params: DeleteSimsParams, - executor: CommandExecutor, -): Promise { - const target = params.target; - const headerEvent = header('Delete Simulator', [ - { label: 'Target', value: target }, - ...(params.shutdownFirst ? [{ label: 'Shutdown First', value: 'true' }] : []), - ]); +type DeleteSimsParams = z.infer; +type DeleteSimsResult = SimulatorActionResultDomainResult; - const ctx = getHandlerContext(); +const publicSchemaObject = z.strictObject( + baseSchemaObject.omit({ + target: true, + shutdownFirst: true, + } as const).shape, +); - return withErrorHandling( - ctx, - async () => { - log( - 'info', - `Deleting simulator(s) ${target}${params.shutdownFirst ? ' (shutdownFirst=true)' : ''}`, - ); +function createDeleteSimsResult(params: { + target: string; + didError: boolean; + error?: string; + diagnosticMessage?: string; +}): DeleteSimsResult { + return { + kind: 'simulator-action-result', + didError: params.didError, + error: params.error ?? null, + summary: { + status: params.didError ? 'FAILED' : 'SUCCEEDED', + }, + action: { + type: 'delete', + target: params.target, + }, + ...(params.diagnosticMessage + ? { diagnostics: createBasicDiagnostics({ errors: [params.diagnosticMessage] }) } + : {}), + }; +} + +function setStructuredOutput(ctx: ToolHandlerContext, result: DeleteSimsResult): void { + ctx.structuredOutput = { + result, + schema: 'xcodebuildmcp.output.simulator-action-result', + schemaVersion: '1', + }; +} + +export function createDeleteSimsExecutor( + executor: CommandExecutor, +): NonStreamingExecutor { + return async (params) => { + try { + const target = params.target; if (params.shutdownFirst && target !== 'all' && target !== 'unavailable') { try { @@ -60,42 +99,67 @@ export async function delete_simsLogic( true, undefined, ); - if (result.success) { - ctx.emit(headerEvent); - ctx.emit(statusLine('success', 'Simulator(s) deleted successfully')); - ctx.nextStepParams = { - list_sims: {}, - }; - return; - } - const errText = result.error ?? 'Unknown error'; - if (/Unable to delete.*Booted/i.test(errText) && !params.shutdownFirst) { - ctx.emit(headerEvent); - ctx.emit(statusLine('error', `Failed to delete simulator: ${errText}`)); - ctx.emit( - section('Hint', [ - `The simulator appears to be Booted. Re-run delete_sims with { target: '${target}', shutdownFirst: true } to shut it down before deleting.`, - ]), - ); - return; + if (!result.success) { + const diagnosticMessage = result.error ?? 'Unknown error'; + return createDeleteSimsResult({ + target, + didError: true, + error: 'Failed to delete simulator(s).', + diagnosticMessage, + }); } - ctx.emit(headerEvent); - ctx.emit(statusLine('error', `Failed to delete simulator: ${errText}`)); - }, - { - header: headerEvent, - errorMessage: ({ message }) => `Failed to delete simulator: ${message}`, - logMessage: ({ message }) => `Error deleting simulators: ${message}`, - }, + return createDeleteSimsResult({ + target, + didError: false, + }); + } catch (error) { + const diagnosticMessage = toErrorMessage(error); + return createDeleteSimsResult({ + target: params.target, + didError: true, + error: 'Failed to delete simulator(s).', + diagnosticMessage, + }); + } + }; +} + +export async function delete_simsLogic( + params: DeleteSimsParams, + executor: CommandExecutor, +): Promise { + const target = params.target; + + log( + 'info', + `Deleting simulator(s) ${target}${params.shutdownFirst ? ' (shutdownFirst=true)' : ''}`, ); + + const ctx = getHandlerContext(); + const executeDeleteSims = createDeleteSimsExecutor(executor); + const result = await executeDeleteSims(params); + setStructuredOutput(ctx, result); + + if (result.didError) { + log('error', `Error deleting simulators: ${result.error ?? 'Unknown error'}`); + return; + } + + ctx.nextStepParams = { + list_sims: {}, + }; } -export const schema = deleteSimsSchema.shape; +export const schema = getSessionAwareToolSchemaShape({ + sessionAware: publicSchemaObject, + legacy: baseSchemaObject, +}); -export const handler = createTypedTool( - deleteSimsSchema, - delete_simsLogic, - getDefaultCommandExecutor, -); +export const handler = createSessionAwareTool({ + internalSchema: toInternalSchema(internalSchemaObject), + logicFunction: delete_simsLogic, + getExecutor: getDefaultCommandExecutor, + requirements: [], +}); diff --git a/src/types/domain-results.ts b/src/types/domain-results.ts index 236b7ca6..473596e2 100644 --- a/src/types/domain-results.ts +++ b/src/types/domain-results.ts @@ -421,8 +421,25 @@ export interface SimulatorActionToggleSoftwareKeyboard { export interface SimulatorActionToggleConnectHardwareKeyboard { type: 'toggle-connect-hardware-keyboard'; } +export interface SimulatorActionClone { + type: 'clone'; + sourceSimulatorId: string; +} +export interface SimulatorActionCreate { + type: 'create'; + name: string; + deviceType: string; + runtime: string; +} +export interface SimulatorActionDelete { + type: 'delete'; + target: string; +} export type SimulatorAction = | SimulatorActionBoot + | SimulatorActionClone + | SimulatorActionCreate + | SimulatorActionDelete | SimulatorActionErase | SimulatorActionOpen | SimulatorActionResetLocation diff --git a/src/utils/renderers/domain-result-text.ts b/src/utils/renderers/domain-result-text.ts index 40a5f618..4434a0ab 100644 --- a/src/utils/renderers/domain-result-text.ts +++ b/src/utils/renderers/domain-result-text.ts @@ -1162,6 +1162,9 @@ function createSimulatorActionItems( ): TextRenderableItem[] { const titleMap: Record = { boot: 'Boot Simulator', + clone: 'Clone Simulator', + create: 'Create Simulator', + delete: 'Delete Simulator', erase: 'Erase Simulator', open: 'Open Simulator', 'reset-location': 'Reset Location', @@ -1195,6 +1198,9 @@ function createSimulatorActionItems( } else { const successMessages: Record = { boot: 'Simulator booted successfully', + clone: 'Simulator cloned successfully', + create: 'Simulator created successfully', + delete: 'Simulator(s) deleted successfully', erase: 'Simulators were erased successfully', open: 'Simulator opened successfully', 'reset-location': 'Location successfully reset to default', From e3b5dcef960fa7093aa1353a1bb88f585c5e8485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=92=8B=E8=89=BA?= Date: Thu, 14 May 2026 23:32:13 +0800 Subject: [PATCH 3/4] fix(manifest): Add outputSchema to clone/create/delete simctl manifests Co-Authored-By: deepseek v4 pro --- manifests/tools/clone_sims.yaml | 3 +++ manifests/tools/create_sim.yaml | 3 +++ manifests/tools/delete_sims.yaml | 3 +++ 3 files changed, 9 insertions(+) diff --git a/manifests/tools/clone_sims.yaml b/manifests/tools/clone_sims.yaml index a20ac667..a883fd28 100644 --- a/manifests/tools/clone_sims.yaml +++ b/manifests/tools/clone_sims.yaml @@ -4,6 +4,9 @@ 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 diff --git a/manifests/tools/create_sim.yaml b/manifests/tools/create_sim.yaml index bf6f3825..cac18dc4 100644 --- a/manifests/tools/create_sim.yaml +++ b/manifests/tools/create_sim.yaml @@ -4,6 +4,9 @@ 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 diff --git a/manifests/tools/delete_sims.yaml b/manifests/tools/delete_sims.yaml index 26b6c4a3..966d8c99 100644 --- a/manifests/tools/delete_sims.yaml +++ b/manifests/tools/delete_sims.yaml @@ -4,6 +4,9 @@ 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 From 140c26f57cba673c93b3dab559444e59597356dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=92=8B=E8=89=BA?= Date: Fri, 15 May 2026 00:09:32 +0800 Subject: [PATCH 4/4] fix(simctl): Address PR review feedback - Fix publicSchemaObject to not omit non-session-default params - Capture new simulator UDID from simctl clone/create stdout - Allow shutdownFirst for delete_sims with target=all - Add tests for clone, create, and delete tools Co-Authored-By: deepseek v4 pro --- .../__tests__/clone_sims.test.ts | 52 ++++++++ .../__tests__/create_sim.test.ts | 52 ++++++++ .../__tests__/delete_sims.test.ts | 113 ++++++++++++++++++ .../tools/simulator-management/clone_sims.ts | 19 ++- .../tools/simulator-management/create_sim.ts | 17 ++- .../tools/simulator-management/delete_sims.ts | 11 +- 6 files changed, 235 insertions(+), 29 deletions(-) create mode 100644 src/mcp/tools/simulator-management/__tests__/clone_sims.test.ts create mode 100644 src/mcp/tools/simulator-management/__tests__/create_sim.test.ts create mode 100644 src/mcp/tools/simulator-management/__tests__/delete_sims.test.ts diff --git a/src/mcp/tools/simulator-management/__tests__/clone_sims.test.ts b/src/mcp/tools/simulator-management/__tests__/clone_sims.test.ts new file mode 100644 index 00000000..306203f6 --- /dev/null +++ b/src/mcp/tools/simulator-management/__tests__/clone_sims.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest'; +import { schema, clone_simsLogic } from '../clone_sims.ts'; +import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { allText, runLogic } from '../../../../test-utils/test-helpers.ts'; + +describe('clone_sims tool', () => { + describe('Plugin Structure', () => { + it('should expose schema', () => { + expect(schema).toBeDefined(); + }); + }); + + describe('Happy path', () => { + it('clones a simulator and captures the new UDID', async () => { + const newUdid = '00000000-0000-0000-0000-000000000001'; + const mock = createMockExecutor({ success: true, output: `${newUdid}\n` }); + const res = await runLogic(() => + clone_simsLogic({ sourceSimulatorId: '00000000-0000-0000-0000-000000000000' }, mock), + ); + expect(res.isError).toBeFalsy(); + const text = allText(res); + expect(text).toContain('Simulator cloned successfully'); + }); + + it('clones with a custom name', async () => { + const mock = createMockExecutor({ success: true, output: 'UUID1\n' }); + const res = await runLogic(() => + clone_simsLogic( + { + sourceSimulatorId: '00000000-0000-0000-0000-000000000000', + newName: 'My Clone', + }, + mock, + ), + ); + expect(res.isError).toBeFalsy(); + }); + }); + + describe('Failure path', () => { + it('returns failure when clone fails', async () => { + const mock = createMockExecutor({ success: false, error: 'No such device' }); + const res = await runLogic(() => + clone_simsLogic({ sourceSimulatorId: '00000000-0000-0000-0000-000000000000' }, mock), + ); + expect(res.isError).toBe(true); + const text = allText(res); + expect(text).toContain('Clone simulator failed'); + expect(text).toContain('No such device'); + }); + }); +}); diff --git a/src/mcp/tools/simulator-management/__tests__/create_sim.test.ts b/src/mcp/tools/simulator-management/__tests__/create_sim.test.ts new file mode 100644 index 00000000..581a4583 --- /dev/null +++ b/src/mcp/tools/simulator-management/__tests__/create_sim.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest'; +import { schema, create_simLogic } from '../create_sim.ts'; +import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { allText, runLogic } from '../../../../test-utils/test-helpers.ts'; + +describe('create_sim tool', () => { + describe('Plugin Structure', () => { + it('should expose schema', () => { + expect(schema).toBeDefined(); + }); + }); + + describe('Happy path', () => { + it('creates a simulator and captures the new UDID', async () => { + const newUdid = '00000000-0000-0000-0000-000000000001'; + const mock = createMockExecutor({ success: true, output: `${newUdid}\n` }); + const res = await runLogic(() => + create_simLogic( + { + name: 'Test Sim', + deviceType: 'iPhone 17', + runtime: 'iOS 26.4', + }, + mock, + ), + ); + expect(res.isError).toBeFalsy(); + const text = allText(res); + expect(text).toContain('Simulator created successfully'); + }); + }); + + describe('Failure path', () => { + it('returns failure when create fails', async () => { + const mock = createMockExecutor({ success: false, error: 'Invalid device type' }); + const res = await runLogic(() => + create_simLogic( + { + name: 'Bad Sim', + deviceType: 'NonExistent', + runtime: 'iOS 99', + }, + mock, + ), + ); + expect(res.isError).toBe(true); + const text = allText(res); + expect(text).toContain('Create simulator failed'); + expect(text).toContain('Invalid device type'); + }); + }); +}); diff --git a/src/mcp/tools/simulator-management/__tests__/delete_sims.test.ts b/src/mcp/tools/simulator-management/__tests__/delete_sims.test.ts new file mode 100644 index 00000000..0ba52220 --- /dev/null +++ b/src/mcp/tools/simulator-management/__tests__/delete_sims.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect } from 'vitest'; +import { schema, delete_simsLogic } from '../delete_sims.ts'; +import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { allText, runLogic } from '../../../../test-utils/test-helpers.ts'; + +describe('delete_sims tool', () => { + describe('Plugin Structure', () => { + it('should expose schema', () => { + expect(schema).toBeDefined(); + }); + }); + + describe('Happy path', () => { + it('deletes a simulator by UDID', async () => { + const mock = createMockExecutor({ success: true, output: 'OK' }); + const res = await runLogic(() => + delete_simsLogic({ target: '00000000-0000-0000-0000-000000000000' }, mock), + ); + expect(res.isError).toBeFalsy(); + const text = allText(res); + expect(text).toContain('Simulator(s) deleted successfully'); + }); + + it('deletes all simulators', async () => { + const mock = createMockExecutor({ success: true, output: 'OK' }); + const res = await runLogic(() => delete_simsLogic({ target: 'all' }, mock)); + expect(res.isError).toBeFalsy(); + }); + + it('deletes unavailable simulators', async () => { + const mock = createMockExecutor({ success: true, output: 'OK' }); + const res = await runLogic(() => delete_simsLogic({ target: 'unavailable' }, mock)); + expect(res.isError).toBeFalsy(); + }); + }); + + describe('Shutdown first', () => { + it('shuts down before deleting when shutdownFirst=true', async () => { + const calls: any[] = []; + const exec = async (cmd: string[]) => { + calls.push(cmd); + return { success: true, output: 'OK', error: '', process: { pid: 1 } as any }; + }; + const res = await runLogic(() => + delete_simsLogic( + { target: '00000000-0000-0000-0000-000000000000', shutdownFirst: true }, + exec as any, + ), + ); + expect(calls).toEqual([ + ['xcrun', 'simctl', 'shutdown', '00000000-0000-0000-0000-000000000000'], + ['xcrun', 'simctl', 'delete', '00000000-0000-0000-0000-000000000000'], + ]); + expect(res.isError).toBeFalsy(); + }); + + it('shuts down all before deleting when target=all', async () => { + const calls: any[] = []; + const exec = async (cmd: string[]) => { + calls.push(cmd); + return { success: true, output: 'OK', error: '', process: { pid: 1 } as any }; + }; + const res = await runLogic(() => + delete_simsLogic({ target: 'all', shutdownFirst: true }, exec as any), + ); + expect(calls).toEqual([ + ['xcrun', 'simctl', 'shutdown', 'all'], + ['xcrun', 'simctl', 'delete', 'all'], + ]); + expect(res.isError).toBeFalsy(); + }); + + it('skips shutdown when target=unavailable', async () => { + const calls: any[] = []; + const exec = async (cmd: string[]) => { + calls.push(cmd); + return { success: true, output: 'OK', error: '', process: { pid: 1 } as any }; + }; + const res = await runLogic(() => + delete_simsLogic({ target: 'unavailable', shutdownFirst: true }, exec as any), + ); + expect(calls).toEqual([['xcrun', 'simctl', 'delete', 'unavailable']]); + expect(res.isError).toBeFalsy(); + }); + + it('does not shut down when shutdownFirst is not set', async () => { + const calls: any[] = []; + const exec = async (cmd: string[]) => { + calls.push(cmd); + return { success: true, output: 'OK', error: '', process: { pid: 1 } as any }; + }; + await runLogic(() => + delete_simsLogic({ target: '00000000-0000-0000-0000-000000000000' }, exec as any), + ); + expect(calls).toEqual([ + ['xcrun', 'simctl', 'delete', '00000000-0000-0000-0000-000000000000'], + ]); + }); + }); + + describe('Failure path', () => { + it('returns failure when delete fails', async () => { + const mock = createMockExecutor({ success: false, error: 'Unable to delete' }); + const res = await runLogic(() => + delete_simsLogic({ target: '00000000-0000-0000-0000-000000000000' }, mock), + ); + expect(res.isError).toBe(true); + const text = allText(res); + expect(text).toContain('Failed to delete simulator'); + expect(text).toContain('Unable to delete'); + }); + }); +}); diff --git a/src/mcp/tools/simulator-management/clone_sims.ts b/src/mcp/tools/simulator-management/clone_sims.ts index 0abaeb36..d0b1f6ab 100644 --- a/src/mcp/tools/simulator-management/clone_sims.ts +++ b/src/mcp/tools/simulator-management/clone_sims.ts @@ -30,15 +30,9 @@ const internalSchemaObject = z.object({ type CloneSimsParams = z.infer; type CloneSimsResult = SimulatorActionResultDomainResult; -const publicSchemaObject = z.strictObject( - baseSchemaObject.omit({ - sourceSimulatorId: true, - newName: true, - } as const).shape, -); - function createCloneSimsResult(params: { sourceSimulatorId: string; + clonedSimulatorId?: string; didError: boolean; error?: string; diagnosticMessage?: string; @@ -58,7 +52,7 @@ function createCloneSimsResult(params: { ? { diagnostics: createBasicDiagnostics({ errors: [params.diagnosticMessage] }) } : {}), artifacts: { - simulatorId: params.sourceSimulatorId, + simulatorId: params.clonedSimulatorId ?? params.sourceSimulatorId, }, }; } @@ -93,8 +87,10 @@ export function createCloneSimsExecutor( }); } + const clonedSimulatorId = result.output.trim(); return createCloneSimsResult({ sourceSimulatorId: params.sourceSimulatorId, + clonedSimulatorId, didError: false, }); } catch (error) { @@ -128,15 +124,18 @@ export async function clone_simsLogic( return; } + const newSimulatorId = result.artifacts?.simulatorId ?? params.sourceSimulatorId; ctx.nextStepParams = { - boot_sim: { simulatorId: params.sourceSimulatorId }, + boot_sim: { simulatorId: newSimulatorId }, open_sim: {}, + install_app_sim: { simulatorId: newSimulatorId, appPath: 'PATH_TO_YOUR_APP' }, + launch_app_sim: { simulatorId: newSimulatorId, bundleId: 'YOUR_APP_BUNDLE_ID' }, list_sims: {}, }; } export const schema = getSessionAwareToolSchemaShape({ - sessionAware: publicSchemaObject, + sessionAware: baseSchemaObject, legacy: baseSchemaObject, }); diff --git a/src/mcp/tools/simulator-management/create_sim.ts b/src/mcp/tools/simulator-management/create_sim.ts index 81688fd0..d27882c9 100644 --- a/src/mcp/tools/simulator-management/create_sim.ts +++ b/src/mcp/tools/simulator-management/create_sim.ts @@ -39,18 +39,11 @@ const internalSchemaObject = z.object({ type CreateSimParams = z.infer; type CreateSimResult = SimulatorActionResultDomainResult; -const publicSchemaObject = z.strictObject( - baseSchemaObject.omit({ - name: true, - deviceType: true, - runtime: true, - } as const).shape, -); - function createCreateSimResult(params: { name: string; deviceType: string; runtime: string; + simulatorId?: string; didError: boolean; error?: string; diagnosticMessage?: string; @@ -71,6 +64,7 @@ function createCreateSimResult(params: { ...(params.diagnosticMessage ? { diagnostics: createBasicDiagnostics({ errors: [params.diagnosticMessage] }) } : {}), + ...(params.simulatorId ? { artifacts: { simulatorId: params.simulatorId } } : {}), }; } @@ -105,10 +99,12 @@ export function createCreateSimExecutor( }); } + const simulatorId = result.output.trim(); return createCreateSimResult({ name: params.name, deviceType: params.deviceType, runtime: params.runtime, + simulatorId, didError: false, }); } catch (error) { @@ -144,15 +140,16 @@ export async function create_simLogic( return; } + const newSimulatorId = result.artifacts?.simulatorId; ctx.nextStepParams = { - boot_sim: {}, + boot_sim: newSimulatorId ? { simulatorId: newSimulatorId } : {}, open_sim: {}, list_sims: {}, }; } export const schema = getSessionAwareToolSchemaShape({ - sessionAware: publicSchemaObject, + sessionAware: baseSchemaObject, legacy: baseSchemaObject, }); diff --git a/src/mcp/tools/simulator-management/delete_sims.ts b/src/mcp/tools/simulator-management/delete_sims.ts index bd8512ba..4548c1c1 100644 --- a/src/mcp/tools/simulator-management/delete_sims.ts +++ b/src/mcp/tools/simulator-management/delete_sims.ts @@ -35,13 +35,6 @@ const internalSchemaObject = z.object({ type DeleteSimsParams = z.infer; type DeleteSimsResult = SimulatorActionResultDomainResult; -const publicSchemaObject = z.strictObject( - baseSchemaObject.omit({ - target: true, - shutdownFirst: true, - } as const).shape, -); - function createDeleteSimsResult(params: { target: string; didError: boolean; @@ -80,7 +73,7 @@ export function createDeleteSimsExecutor( try { const target = params.target; - if (params.shutdownFirst && target !== 'all' && target !== 'unavailable') { + if (params.shutdownFirst && target !== 'unavailable') { try { await executor( ['xcrun', 'simctl', 'shutdown', target], @@ -153,7 +146,7 @@ export async function delete_simsLogic( } export const schema = getSessionAwareToolSchemaShape({ - sessionAware: publicSchemaObject, + sessionAware: baseSchemaObject, legacy: baseSchemaObject, });