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..98ed6d5c 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. 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. @@ -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* 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..5861287e --- /dev/null +++ b/src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts @@ -0,0 +1,116 @@ +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: simulatorUdid or all=true'); + expect(eraseSims.description).toContain('shutdownFirst'); + }); + + 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({ 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. + }); + }); + + describe('Single mode', () => { + it('erases a simulator successfully', async () => { + const mock = createMockExecutor({ success: true, output: 'OK' }); + const res = await erase_simsLogic({ simulatorUdid: '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({ simulatorUdid: 'UD1' }, mock); + expect(res).toEqual({ + 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({ simulatorUdid: '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({ simulatorUdid: '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', () => { + 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' }], + }); + }); + + 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/__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..4723e9d1 --- /dev/null +++ b/src/mcp/tools/simulator-management/erase_sims.ts @@ -0,0 +1,136 @@ +import { z } from 'zod'; +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({ + 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() + .optional() + .describe('If true, shuts down the target (UDID or all) before erasing.'), +}); + +const eraseSimsSchema = eraseSimsBaseSchema.refine( + (v) => { + const selectors = (v.simulatorUdid ? 1 : 0) + (v.all === true ? 1 : 0); + return selectors === 1; + }, + { message: 'Provide exactly one of: simulatorUdid or all=true.' }, +); + +type EraseSimsParams = z.infer; + +export async function erase_simsLogic( + params: EraseSimsParams, + executor: CommandExecutor, +): Promise { + try { + if (params.simulatorUdid) { + const udid = params.simulatorUdid; + 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 + } + } + + const result = await executor( + ['xcrun', 'simctl', 'erase', udid], + 'Erase Simulator', + true, + undefined, + ); + 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: `Tool hint: The simulator appears to be Booted. Re-run erase_sims with { simulatorUdid: '${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: ToolResponseContent[] = [ + { 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' }] }; + } + + return { + content: [{ type: 'text', text: 'Invalid parameters: provide simulatorUdid 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: simulatorUdid or all=true. Optional: shutdownFirst to shut down before erasing.', + 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'], };