From e0d82e7efb1b9aeb208cd250717ae44d65c7510f Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Sun, 21 Sep 2025 19:49:34 +0100 Subject: [PATCH 1/5] feat(simulator-management): add consolidated erase_sims tool Implements Simulator "Erase Content and Settings" using native simctl: - UDID: xcrun simctl erase - All: xcrun simctl erase all Adds tests and updates workflow metadata to include 'erase'. Closes #110 --- .../__tests__/erase_sims.test.ts | 64 ++++++++++++++ .../__tests__/index.test.ts | 3 +- .../tools/simulator-management/erase_sims.ts | 86 +++++++++++++++++++ src/mcp/tools/simulator-management/index.ts | 7 +- 4 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts create mode 100644 src/mcp/tools/simulator-management/erase_sims.ts diff --git a/src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts b/src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts new file mode 100644 index 00000000..4f4c4c94 --- /dev/null +++ b/src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest'; +import { z } from 'zod'; +import eraseSims, { erase_simsLogic } from '../erase_sims.ts'; +import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; + +describe('erase_sims tool (UDID or ALL only)', () => { + describe('Export Field Validation (Literal)', () => { + it('should have correct name', () => { + expect(eraseSims.name).toBe('erase_sims'); + }); + + it('should have correct description', () => { + expect(eraseSims.description).toContain('Provide exactly one of: simulatorUuid or all=true'); + }); + + it('should have handler function', () => { + expect(typeof eraseSims.handler).toBe('function'); + }); + + it('should validate schema fields (shape only)', () => { + const schema = z.object(eraseSims.schema); + // Valid + expect(schema.safeParse({ simulatorUuid: 'UDID-1' }).success).toBe(true); + expect(schema.safeParse({ all: true }).success).toBe(true); + // Shape-level schema does not enforce selection rules; handler validation covers that. + }); + }); + + describe('Single mode', () => { + it('erases a simulator successfully', async () => { + const mock = createMockExecutor({ success: true, output: 'OK' }); + const res = await erase_simsLogic({ simulatorUuid: 'UD1' }, mock); + expect(res).toEqual({ + content: [{ type: 'text', text: 'Successfully erased simulator UD1' }], + }); + }); + + it('returns failure when erase fails', async () => { + const mock = createMockExecutor({ success: false, error: 'Booted device' }); + const res = await erase_simsLogic({ simulatorUuid: 'UD1' }, mock); + expect(res).toEqual({ + content: [{ type: 'text', text: 'Failed to erase simulator: Booted device' }], + }); + }); + }); + + describe('All mode', () => { + it('erases all simulators successfully', async () => { + const exec = createMockExecutor({ success: true, output: 'OK' }); + const res = await erase_simsLogic({ all: true }, exec); + expect(res).toEqual({ + content: [{ type: 'text', text: 'Successfully erased all simulators' }], + }); + }); + + it('returns failure when erase all fails', async () => { + const exec = createMockExecutor({ success: false, error: 'Denied' }); + const res = await erase_simsLogic({ all: true }, exec); + expect(res).toEqual({ + content: [{ type: 'text', text: 'Failed to erase all simulators: Denied' }], + }); + }); + }); +}); diff --git a/src/mcp/tools/simulator-management/__tests__/index.test.ts b/src/mcp/tools/simulator-management/__tests__/index.test.ts index 3cdf064c..f7224127 100644 --- a/src/mcp/tools/simulator-management/__tests__/index.test.ts +++ b/src/mcp/tools/simulator-management/__tests__/index.test.ts @@ -21,7 +21,7 @@ describe('simulator-management workflow metadata', () => { it('should have correct description', () => { expect(workflow.description).toBe( - 'Tools for managing simulators from booting, opening simulators, listing simulators, stopping simulators and setting simulator environment options like location, network, statusbar and appearance.', + 'Tools for managing simulators from booting, opening simulators, listing simulators, stopping simulators, erasing simulator content and settings, and setting simulator environment options like location, network, statusbar and appearance.', ); }); @@ -46,6 +46,7 @@ describe('simulator-management workflow metadata', () => { 'location', 'network', 'statusbar', + 'erase', ]); }); }); diff --git a/src/mcp/tools/simulator-management/erase_sims.ts b/src/mcp/tools/simulator-management/erase_sims.ts new file mode 100644 index 00000000..87ec91b0 --- /dev/null +++ b/src/mcp/tools/simulator-management/erase_sims.ts @@ -0,0 +1,86 @@ +import { z } from 'zod'; +import { ToolResponse } from '../../../types/common.ts'; +import { log } from '../../../utils/logging/index.ts'; +import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; +import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; + +const eraseSimsBaseSchema = z.object({ + simulatorUuid: z.string().optional().describe('UUID of the simulator to erase.'), + all: z.boolean().optional().describe('When true, erases all simulators.'), +}); + +const eraseSimsSchema = eraseSimsBaseSchema.refine( + (v) => { + const selectors = (v.simulatorUuid ? 1 : 0) + (v.all === true ? 1 : 0); + return selectors === 1; + }, + { message: 'Provide exactly one of: simulatorUuid OR all=true.' }, +); + +type EraseSimsParams = z.infer; + +async function eraseSingle(udid: string, executor: CommandExecutor): Promise { + const result = await executor( + ['xcrun', 'simctl', 'erase', udid], + 'Erase Simulator', + true, + undefined, + ); + if (result.success) { + return { content: [{ type: 'text', text: `Successfully erased simulator ${udid}` }] }; + } + return { + content: [ + { type: 'text', text: `Failed to erase simulator: ${result.error ?? 'Unknown error'}` }, + ], + }; +} + +export async function erase_simsLogic( + params: EraseSimsParams, + executor: CommandExecutor, +): Promise { + try { + if (params.simulatorUuid) { + log('info', `Erasing simulator ${params.simulatorUuid}`); + return await eraseSingle(params.simulatorUuid, executor); + } + + if (params.all === true) { + log('info', 'Erasing ALL simulators'); + const result = await executor( + ['xcrun', 'simctl', 'erase', 'all'], + 'Erase All Simulators', + true, + undefined, + ); + if (!result.success) { + return { + content: [ + { + type: 'text', + text: `Failed to erase all simulators: ${result.error ?? 'Unknown error'}`, + }, + ], + }; + } + return { content: [{ type: 'text', text: 'Successfully erased all simulators' }] }; + } + + return { + content: [{ type: 'text', text: 'Invalid parameters: provide simulatorUuid or all=true.' }], + }; + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + log('error', `Error erasing simulators: ${message}`); + return { content: [{ type: 'text', text: `Failed to erase simulators: ${message}` }] }; + } +} + +export default { + name: 'erase_sims', + description: + 'Erases simulator content and settings. Provide exactly one of: simulatorUuid or all=true.', + schema: eraseSimsBaseSchema.shape, + handler: createTypedTool(eraseSimsSchema, erase_simsLogic, getDefaultCommandExecutor), +}; diff --git a/src/mcp/tools/simulator-management/index.ts b/src/mcp/tools/simulator-management/index.ts index 6c538e4a..c073c6cf 100644 --- a/src/mcp/tools/simulator-management/index.ts +++ b/src/mcp/tools/simulator-management/index.ts @@ -2,15 +2,16 @@ * Simulator Management workflow * * Provides tools for working with simulators like booting and opening simulators, launching apps, - * listing sims, stopping apps and setting sim environment options like location, network, statusbar and appearance. + * listing sims, stopping apps, erasing simulator content and settings, and setting sim environment + * options like location, network, statusbar and appearance. */ export const workflow = { name: 'Simulator Management', description: - 'Tools for managing simulators from booting, opening simulators, listing simulators, stopping simulators and setting simulator environment options like location, network, statusbar and appearance.', + 'Tools for managing simulators from booting, opening simulators, listing simulators, stopping simulators, erasing simulator content and settings, and setting simulator environment options like location, network, statusbar and appearance.', platforms: ['iOS'], targets: ['simulator'], projectTypes: ['project', 'workspace'], - capabilities: ['boot', 'open', 'list', 'appearance', 'location', 'network', 'statusbar'], + capabilities: ['boot', 'open', 'list', 'appearance', 'location', 'network', 'statusbar', 'erase'], }; From e93cf4cc1fbf662b7b2398d78a599b8f4556e4e2 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Sun, 21 Sep 2025 19:52:22 +0100 Subject: [PATCH 2/5] docs: update TOOLS.md and README workflow counts; document erase_sims in Simulator Management --- README.md | 3 +-- docs/TOOLS.md | 11 ++++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d908b8b0..4020edc6 100644 --- a/README.md +++ b/README.md @@ -274,7 +274,7 @@ For clients that don't support MCP Sampling but still want to reduce context win **Available Workflows:** - `device` (14 tools) - iOS Device Development - `simulator` (18 tools) - iOS Simulator Development -- `simulator-management` (7 tools) - Simulator Management +- `simulator-management` (8 tools) - Simulator Management - `swift-package` (6 tools) - Swift Package Manager - `project-discovery` (5 tools) - Project Discovery - `macos` (11 tools) - macOS Development @@ -371,4 +371,3 @@ See our documentation for development: ## Licence This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - diff --git a/docs/TOOLS.md b/docs/TOOLS.md index 62021e66..c3aa99fd 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -1,6 +1,6 @@ # XcodeBuildMCP Tools Reference -XcodeBuildMCP provides 59 tools organized into 12 workflow groups for comprehensive Apple development workflows. +XcodeBuildMCP provides 60 tools organized into 12 workflow groups for comprehensive Apple development workflows. ## Workflow Groups @@ -64,10 +64,11 @@ XcodeBuildMCP provides 59 tools organized into 12 workflow groups for comprehens ### Project Utilities (`utilities`) **Purpose**: Essential project maintenance utilities for cleaning and managing existing projects. Provides clean operations for both .xcodeproj and .xcworkspace files. (1 tools) -- `clean` - Cleans build products for either a project or a workspace using xcodebuild. Provide exactly one of projectPath or workspacePath. Example: clean({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme' }) +- `clean` - Cleans build products for either a project or a workspace using xcodebuild. Provide exactly one of projectPath or workspacePath. Platform defaults to iOS if not specified. Example: clean({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme', platform: 'iOS' }) ### Simulator Management (`simulator-management`) -**Purpose**: Tools for managing simulators from booting, opening simulators, listing simulators, stopping simulators and setting simulator environment options like location, network, statusbar and appearance. (4 tools) +**Purpose**: Tools for managing simulators from booting, opening simulators, listing simulators, stopping simulators, erasing simulator content and settings, and setting simulator environment options like location, network, statusbar and appearance. (5 tools) +- `erase_sims` - Erases simulator content and settings. Provide exactly one of: simulatorUuid or all=true. - `reset_sim_location` - Resets the simulator's location to default. - `set_sim_appearance` - Sets the appearance mode (dark/light) of an iOS simulator. - `set_sim_location` - Sets a custom GPS location for the simulator. @@ -102,9 +103,9 @@ XcodeBuildMCP provides 59 tools organized into 12 workflow groups for comprehens ## Summary Statistics -- **Total Tools**: 59 canonical tools + 22 re-exports = 81 total +- **Total Tools**: 60 canonical tools + 22 re-exports = 82 total - **Workflow Groups**: 12 --- -*This documentation is automatically generated by `scripts/update-tools-docs.ts` using static analysis. Last updated: 2025-08-16* +*This documentation is automatically generated by `scripts/update-tools-docs.ts` using static analysis. Last updated: 2025-09-21* From 4a5ebd35f5bf0134062353cb9079c97235d69236 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Sun, 21 Sep 2025 20:03:05 +0100 Subject: [PATCH 3/5] feat(simulator-management): add shutdownFirst option and tool hints for booted erase errors; update tests and docs generation --- .../__tests__/erase_sims.test.ts | 50 +++++++++ .../tools/simulator-management/erase_sims.ts | 102 +++++++++++++----- 2 files changed, 125 insertions(+), 27 deletions(-) diff --git a/src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts b/src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts index 4f4c4c94..165e201d 100644 --- a/src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts +++ b/src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts @@ -11,6 +11,7 @@ describe('erase_sims tool (UDID or ALL only)', () => { it('should have correct description', () => { expect(eraseSims.description).toContain('Provide exactly one of: simulatorUuid or all=true'); + expect(eraseSims.description).toContain('shutdownFirst'); }); it('should have handler function', () => { @@ -42,6 +43,31 @@ describe('erase_sims tool (UDID or ALL only)', () => { content: [{ type: 'text', text: 'Failed to erase simulator: Booted device' }], }); }); + + it('adds tool hint when booted error occurs without shutdownFirst', async () => { + const bootedError = + 'An error was encountered processing the command (domain=com.apple.CoreSimulator.SimError, code=405):\nUnable to erase contents and settings in current state: Booted\n'; + const mock = createMockExecutor({ success: false, error: bootedError }); + const res = await erase_simsLogic({ simulatorUuid: 'UD1' }, mock); + expect((res.content?.[1] as any).text).toContain('Tool hint'); + expect((res.content?.[1] as any).text).toContain('shutdownFirst: true'); + }); + + it('performs shutdown first 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 erase_simsLogic({ simulatorUuid: 'UD1', shutdownFirst: true }, exec as any); + expect(calls).toEqual([ + ['xcrun', 'simctl', 'shutdown', 'UD1'], + ['xcrun', 'simctl', 'erase', 'UD1'], + ]); + expect(res).toEqual({ + content: [{ type: 'text', text: 'Successfully erased simulator UD1' }], + }); + }); }); describe('All mode', () => { @@ -60,5 +86,29 @@ describe('erase_sims tool (UDID or ALL only)', () => { content: [{ type: 'text', text: 'Failed to erase all simulators: Denied' }], }); }); + + it('performs shutdown all 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 erase_simsLogic({ all: true, shutdownFirst: true }, exec as any); + expect(calls).toEqual([ + ['xcrun', 'simctl', 'shutdown', 'all'], + ['xcrun', 'simctl', 'erase', 'all'], + ]); + expect(res).toEqual({ + content: [{ type: 'text', text: 'Successfully erased all simulators' }], + }); + }); + + it('adds tool hint on booted error without shutdownFirst (all mode)', async () => { + const bootedError = 'Unable to erase contents and settings in current state: Booted'; + const exec = createMockExecutor({ success: false, error: bootedError }); + const res = await erase_simsLogic({ all: true }, exec); + expect((res.content?.[1] as any).text).toContain('Tool hint'); + expect((res.content?.[1] as any).text).toContain('shutdownFirst: true'); + }); }); }); diff --git a/src/mcp/tools/simulator-management/erase_sims.ts b/src/mcp/tools/simulator-management/erase_sims.ts index 87ec91b0..99e2e275 100644 --- a/src/mcp/tools/simulator-management/erase_sims.ts +++ b/src/mcp/tools/simulator-management/erase_sims.ts @@ -7,6 +7,10 @@ import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; const eraseSimsBaseSchema = z.object({ simulatorUuid: z.string().optional().describe('UUID of the simulator to erase.'), all: z.boolean().optional().describe('When true, erases all simulators.'), + shutdownFirst: z + .boolean() + .optional() + .describe('If true, shuts down the target (UDID or all) before erasing.'), }); const eraseSimsSchema = eraseSimsBaseSchema.refine( @@ -19,51 +23,95 @@ const eraseSimsSchema = eraseSimsBaseSchema.refine( type EraseSimsParams = z.infer; -async function eraseSingle(udid: string, executor: CommandExecutor): Promise { - const result = await executor( - ['xcrun', 'simctl', 'erase', udid], - 'Erase Simulator', - true, - undefined, - ); - if (result.success) { - return { content: [{ type: 'text', text: `Successfully erased simulator ${udid}` }] }; - } - return { - content: [ - { type: 'text', text: `Failed to erase simulator: ${result.error ?? 'Unknown error'}` }, - ], - }; -} - export async function erase_simsLogic( params: EraseSimsParams, executor: CommandExecutor, ): Promise { try { if (params.simulatorUuid) { - log('info', `Erasing simulator ${params.simulatorUuid}`); - return await eraseSingle(params.simulatorUuid, executor); - } + const udid = params.simulatorUuid; + log( + 'info', + `Erasing simulator ${udid}${params.shutdownFirst ? ' (shutdownFirst=true)' : ''}`, + ); + + if (params.shutdownFirst) { + try { + await executor( + ['xcrun', 'simctl', 'shutdown', udid], + 'Shutdown Simulator', + true, + undefined, + ); + } catch { + // ignore shutdown errors; proceed to erase attempt + } + } - if (params.all === true) { - log('info', 'Erasing ALL simulators'); const result = await executor( - ['xcrun', 'simctl', 'erase', 'all'], - 'Erase All Simulators', + ['xcrun', 'simctl', 'erase', udid], + 'Erase Simulator', true, undefined, ); - if (!result.success) { + if (result.success) { + return { content: [{ type: 'text', text: `Successfully erased simulator ${udid}` }] }; + } + + // Add tool hint if simulator is booted and shutdownFirst was not requested + const errText = result.error ?? 'Unknown error'; + if (/Unable to erase contents and settings.*Booted/i.test(errText) && !params.shutdownFirst) { return { content: [ + { type: 'text', text: `Failed to erase simulator: ${errText}` }, { type: 'text', - text: `Failed to erase all simulators: ${result.error ?? 'Unknown error'}`, + text: `Tool hint: The simulator appears to be Booted. Re-run erase_sims with { simulatorUuid: '${udid}', shutdownFirst: true } to shut it down before erasing.`, }, ], }; } + + return { + content: [{ type: 'text', text: `Failed to erase simulator: ${errText}` }], + }; + } + + if (params.all === true) { + log('info', `Erasing ALL simulators${params.shutdownFirst ? ' (shutdownFirst=true)' : ''}`); + if (params.shutdownFirst) { + try { + await executor( + ['xcrun', 'simctl', 'shutdown', 'all'], + 'Shutdown All Simulators', + true, + undefined, + ); + } catch { + // ignore and continue to erase + } + } + + const result = await executor( + ['xcrun', 'simctl', 'erase', 'all'], + 'Erase All Simulators', + true, + undefined, + ); + if (!result.success) { + const errText = result.error ?? 'Unknown error'; + const content = [{ type: 'text', text: `Failed to erase all simulators: ${errText}` }]; + if ( + /Unable to erase contents and settings.*Booted/i.test(errText) && + !params.shutdownFirst + ) { + content.push({ + type: 'text', + text: 'Tool hint: One or more simulators appear to be Booted. Re-run erase_sims with { all: true, shutdownFirst: true } to shut them down before erasing.', + }); + } + return { content }; + } return { content: [{ type: 'text', text: 'Successfully erased all simulators' }] }; } @@ -80,7 +128,7 @@ export async function erase_simsLogic( export default { name: 'erase_sims', description: - 'Erases simulator content and settings. Provide exactly one of: simulatorUuid or all=true.', + 'Erases simulator content and settings. Provide exactly one of: simulatorUuid or all=true. Optional: shutdownFirst to shut down before erasing.', schema: eraseSimsBaseSchema.shape, handler: createTypedTool(eraseSimsSchema, erase_simsLogic, getDefaultCommandExecutor), }; From dc695f5727b5894de85596a1941eaaa46d2aafa8 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Sun, 21 Sep 2025 20:06:22 +0100 Subject: [PATCH 4/5] docs(TOOLS): reflect erase_sims shutdownFirst option (no default) --- docs/TOOLS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TOOLS.md b/docs/TOOLS.md index c3aa99fd..98ed6d5c 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -68,7 +68,7 @@ XcodeBuildMCP provides 60 tools organized into 12 workflow groups for comprehens ### Simulator Management (`simulator-management`) **Purpose**: Tools for managing simulators from booting, opening simulators, listing simulators, stopping simulators, erasing simulator content and settings, and setting simulator environment options like location, network, statusbar and appearance. (5 tools) -- `erase_sims` - Erases simulator content and settings. Provide exactly one of: simulatorUuid or all=true. +- `erase_sims` - Erases simulator content and settings. Provide exactly one of: simulatorUuid or all=true. Optional: shutdownFirst to shut down before erasing. - `reset_sim_location` - Resets the simulator's location to default. - `set_sim_appearance` - Sets the appearance mode (dark/light) of an iOS simulator. - `set_sim_location` - Sets a custom GPS location for the simulator. From 90dc951877bb28689b0ca398de0942530c8a635f Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Sun, 21 Sep 2025 20:14:52 +0100 Subject: [PATCH 5/5] refactor(simulator-management): adopt UDID terminology (simulatorUdid + z.string().uuid), add typed content, update tests; address PR review --- .../__tests__/erase_sims.test.ts | 14 +++++++----- .../tools/simulator-management/erase_sims.ts | 22 ++++++++++--------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts b/src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts index 165e201d..5861287e 100644 --- a/src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts +++ b/src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts @@ -10,7 +10,7 @@ describe('erase_sims tool (UDID or ALL only)', () => { }); it('should have correct description', () => { - expect(eraseSims.description).toContain('Provide exactly one of: simulatorUuid or all=true'); + expect(eraseSims.description).toContain('Provide exactly one of: simulatorUdid or all=true'); expect(eraseSims.description).toContain('shutdownFirst'); }); @@ -21,7 +21,9 @@ describe('erase_sims tool (UDID or ALL only)', () => { it('should validate schema fields (shape only)', () => { const schema = z.object(eraseSims.schema); // Valid - expect(schema.safeParse({ simulatorUuid: 'UDID-1' }).success).toBe(true); + expect( + schema.safeParse({ simulatorUdid: '123e4567-e89b-12d3-a456-426614174000' }).success, + ).toBe(true); expect(schema.safeParse({ all: true }).success).toBe(true); // Shape-level schema does not enforce selection rules; handler validation covers that. }); @@ -30,7 +32,7 @@ describe('erase_sims tool (UDID or ALL only)', () => { describe('Single mode', () => { it('erases a simulator successfully', async () => { const mock = createMockExecutor({ success: true, output: 'OK' }); - const res = await erase_simsLogic({ simulatorUuid: 'UD1' }, mock); + const res = await erase_simsLogic({ simulatorUdid: 'UD1' }, mock); expect(res).toEqual({ content: [{ type: 'text', text: 'Successfully erased simulator UD1' }], }); @@ -38,7 +40,7 @@ describe('erase_sims tool (UDID or ALL only)', () => { it('returns failure when erase fails', async () => { const mock = createMockExecutor({ success: false, error: 'Booted device' }); - const res = await erase_simsLogic({ simulatorUuid: 'UD1' }, mock); + const res = await erase_simsLogic({ simulatorUdid: 'UD1' }, mock); expect(res).toEqual({ content: [{ type: 'text', text: 'Failed to erase simulator: Booted device' }], }); @@ -48,7 +50,7 @@ describe('erase_sims tool (UDID or ALL only)', () => { const bootedError = 'An error was encountered processing the command (domain=com.apple.CoreSimulator.SimError, code=405):\nUnable to erase contents and settings in current state: Booted\n'; const mock = createMockExecutor({ success: false, error: bootedError }); - const res = await erase_simsLogic({ simulatorUuid: 'UD1' }, mock); + const res = await erase_simsLogic({ simulatorUdid: 'UD1' }, mock); expect((res.content?.[1] as any).text).toContain('Tool hint'); expect((res.content?.[1] as any).text).toContain('shutdownFirst: true'); }); @@ -59,7 +61,7 @@ describe('erase_sims tool (UDID or ALL only)', () => { calls.push(cmd); return { success: true, output: 'OK', error: '', process: { pid: 1 } as any }; }; - const res = await erase_simsLogic({ simulatorUuid: 'UD1', shutdownFirst: true }, exec as any); + const res = await erase_simsLogic({ simulatorUdid: 'UD1', shutdownFirst: true }, exec as any); expect(calls).toEqual([ ['xcrun', 'simctl', 'shutdown', 'UD1'], ['xcrun', 'simctl', 'erase', 'UD1'], diff --git a/src/mcp/tools/simulator-management/erase_sims.ts b/src/mcp/tools/simulator-management/erase_sims.ts index 99e2e275..4723e9d1 100644 --- a/src/mcp/tools/simulator-management/erase_sims.ts +++ b/src/mcp/tools/simulator-management/erase_sims.ts @@ -1,11 +1,11 @@ import { z } from 'zod'; -import { ToolResponse } from '../../../types/common.ts'; +import { ToolResponse, type ToolResponseContent } from '../../../types/common.ts'; import { log } from '../../../utils/logging/index.ts'; import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; const eraseSimsBaseSchema = z.object({ - simulatorUuid: z.string().optional().describe('UUID of the simulator to erase.'), + simulatorUdid: z.string().uuid().optional().describe('UDID of the simulator to erase.'), all: z.boolean().optional().describe('When true, erases all simulators.'), shutdownFirst: z .boolean() @@ -15,10 +15,10 @@ const eraseSimsBaseSchema = z.object({ const eraseSimsSchema = eraseSimsBaseSchema.refine( (v) => { - const selectors = (v.simulatorUuid ? 1 : 0) + (v.all === true ? 1 : 0); + const selectors = (v.simulatorUdid ? 1 : 0) + (v.all === true ? 1 : 0); return selectors === 1; }, - { message: 'Provide exactly one of: simulatorUuid OR all=true.' }, + { message: 'Provide exactly one of: simulatorUdid or all=true.' }, ); type EraseSimsParams = z.infer; @@ -28,8 +28,8 @@ export async function erase_simsLogic( executor: CommandExecutor, ): Promise { try { - if (params.simulatorUuid) { - const udid = params.simulatorUuid; + if (params.simulatorUdid) { + const udid = params.simulatorUdid; log( 'info', `Erasing simulator ${udid}${params.shutdownFirst ? ' (shutdownFirst=true)' : ''}`, @@ -66,7 +66,7 @@ export async function erase_simsLogic( { type: 'text', text: `Failed to erase simulator: ${errText}` }, { type: 'text', - text: `Tool hint: The simulator appears to be Booted. Re-run erase_sims with { simulatorUuid: '${udid}', shutdownFirst: true } to shut it down before erasing.`, + text: `Tool hint: The simulator appears to be Booted. Re-run erase_sims with { simulatorUdid: '${udid}', shutdownFirst: true } to shut it down before erasing.`, }, ], }; @@ -100,7 +100,9 @@ export async function erase_simsLogic( ); if (!result.success) { const errText = result.error ?? 'Unknown error'; - const content = [{ type: 'text', text: `Failed to erase all simulators: ${errText}` }]; + const content: ToolResponseContent[] = [ + { type: 'text', text: `Failed to erase all simulators: ${errText}` }, + ]; if ( /Unable to erase contents and settings.*Booted/i.test(errText) && !params.shutdownFirst @@ -116,7 +118,7 @@ export async function erase_simsLogic( } return { - content: [{ type: 'text', text: 'Invalid parameters: provide simulatorUuid or all=true.' }], + content: [{ type: 'text', text: 'Invalid parameters: provide simulatorUdid or all=true.' }], }; } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); @@ -128,7 +130,7 @@ export async function erase_simsLogic( export default { name: 'erase_sims', description: - 'Erases simulator content and settings. Provide exactly one of: simulatorUuid or all=true. Optional: shutdownFirst to shut down before erasing.', + 'Erases simulator content and settings. Provide exactly one of: simulatorUdid or all=true. Optional: shutdownFirst to shut down before erasing.', schema: eraseSimsBaseSchema.shape, handler: createTypedTool(eraseSimsSchema, erase_simsLogic, getDefaultCommandExecutor), };