Skip to content

Commit 679eaaa

Browse files
authored
feat(simulator-management): add consolidated erase_sims tool (UDID or all) (#111)
* feat(simulator-management): add consolidated erase_sims tool Implements Simulator "Erase Content and Settings" using native simctl: - UDID: xcrun simctl erase <UDID> - All: xcrun simctl erase all Adds tests and updates workflow metadata to include 'erase'. Closes #110 * docs: update TOOLS.md and README workflow counts; document erase_sims in Simulator Management * feat(simulator-management): add shutdownFirst option and tool hints for booted erase errors; update tests and docs generation * docs(TOOLS): reflect erase_sims shutdownFirst option (no default) * refactor(simulator-management): adopt UDID terminology (simulatorUdid + z.string().uuid), add typed content, update tests; address PR review
1 parent 0ce407f commit 679eaaa

File tree

6 files changed

+265
-11
lines changed

6 files changed

+265
-11
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ For clients that don't support MCP Sampling but still want to reduce context win
274274
**Available Workflows:**
275275
- `device` (14 tools) - iOS Device Development
276276
- `simulator` (18 tools) - iOS Simulator Development
277-
- `simulator-management` (7 tools) - Simulator Management
277+
- `simulator-management` (8 tools) - Simulator Management
278278
- `swift-package` (6 tools) - Swift Package Manager
279279
- `project-discovery` (5 tools) - Project Discovery
280280
- `macos` (11 tools) - macOS Development
@@ -371,4 +371,3 @@ See our documentation for development:
371371
## Licence
372372

373373
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
374-

docs/TOOLS.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# XcodeBuildMCP Tools Reference
22

3-
XcodeBuildMCP provides 59 tools organized into 12 workflow groups for comprehensive Apple development workflows.
3+
XcodeBuildMCP provides 60 tools organized into 12 workflow groups for comprehensive Apple development workflows.
44

55
## Workflow Groups
66

@@ -64,10 +64,11 @@ XcodeBuildMCP provides 59 tools organized into 12 workflow groups for comprehens
6464
### Project Utilities (`utilities`)
6565
**Purpose**: Essential project maintenance utilities for cleaning and managing existing projects. Provides clean operations for both .xcodeproj and .xcworkspace files. (1 tools)
6666

67-
- `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' })
67+
- `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' })
6868
### Simulator Management (`simulator-management`)
69-
**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)
69+
**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)
7070

71+
- `erase_sims` - Erases simulator content and settings. Provide exactly one of: simulatorUuid or all=true. Optional: shutdownFirst to shut down before erasing.
7172
- `reset_sim_location` - Resets the simulator's location to default.
7273
- `set_sim_appearance` - Sets the appearance mode (dark/light) of an iOS simulator.
7374
- `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
102103

103104
## Summary Statistics
104105

105-
- **Total Tools**: 59 canonical tools + 22 re-exports = 81 total
106+
- **Total Tools**: 60 canonical tools + 22 re-exports = 82 total
106107
- **Workflow Groups**: 12
107108

108109
---
109110

110-
*This documentation is automatically generated by `scripts/update-tools-docs.ts` using static analysis. Last updated: 2025-08-16*
111+
*This documentation is automatically generated by `scripts/update-tools-docs.ts` using static analysis. Last updated: 2025-09-21*
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { z } from 'zod';
3+
import eraseSims, { erase_simsLogic } from '../erase_sims.ts';
4+
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
5+
6+
describe('erase_sims tool (UDID or ALL only)', () => {
7+
describe('Export Field Validation (Literal)', () => {
8+
it('should have correct name', () => {
9+
expect(eraseSims.name).toBe('erase_sims');
10+
});
11+
12+
it('should have correct description', () => {
13+
expect(eraseSims.description).toContain('Provide exactly one of: simulatorUdid or all=true');
14+
expect(eraseSims.description).toContain('shutdownFirst');
15+
});
16+
17+
it('should have handler function', () => {
18+
expect(typeof eraseSims.handler).toBe('function');
19+
});
20+
21+
it('should validate schema fields (shape only)', () => {
22+
const schema = z.object(eraseSims.schema);
23+
// Valid
24+
expect(
25+
schema.safeParse({ simulatorUdid: '123e4567-e89b-12d3-a456-426614174000' }).success,
26+
).toBe(true);
27+
expect(schema.safeParse({ all: true }).success).toBe(true);
28+
// Shape-level schema does not enforce selection rules; handler validation covers that.
29+
});
30+
});
31+
32+
describe('Single mode', () => {
33+
it('erases a simulator successfully', async () => {
34+
const mock = createMockExecutor({ success: true, output: 'OK' });
35+
const res = await erase_simsLogic({ simulatorUdid: 'UD1' }, mock);
36+
expect(res).toEqual({
37+
content: [{ type: 'text', text: 'Successfully erased simulator UD1' }],
38+
});
39+
});
40+
41+
it('returns failure when erase fails', async () => {
42+
const mock = createMockExecutor({ success: false, error: 'Booted device' });
43+
const res = await erase_simsLogic({ simulatorUdid: 'UD1' }, mock);
44+
expect(res).toEqual({
45+
content: [{ type: 'text', text: 'Failed to erase simulator: Booted device' }],
46+
});
47+
});
48+
49+
it('adds tool hint when booted error occurs without shutdownFirst', async () => {
50+
const bootedError =
51+
'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';
52+
const mock = createMockExecutor({ success: false, error: bootedError });
53+
const res = await erase_simsLogic({ simulatorUdid: 'UD1' }, mock);
54+
expect((res.content?.[1] as any).text).toContain('Tool hint');
55+
expect((res.content?.[1] as any).text).toContain('shutdownFirst: true');
56+
});
57+
58+
it('performs shutdown first when shutdownFirst=true', async () => {
59+
const calls: any[] = [];
60+
const exec = async (cmd: string[]) => {
61+
calls.push(cmd);
62+
return { success: true, output: 'OK', error: '', process: { pid: 1 } as any };
63+
};
64+
const res = await erase_simsLogic({ simulatorUdid: 'UD1', shutdownFirst: true }, exec as any);
65+
expect(calls).toEqual([
66+
['xcrun', 'simctl', 'shutdown', 'UD1'],
67+
['xcrun', 'simctl', 'erase', 'UD1'],
68+
]);
69+
expect(res).toEqual({
70+
content: [{ type: 'text', text: 'Successfully erased simulator UD1' }],
71+
});
72+
});
73+
});
74+
75+
describe('All mode', () => {
76+
it('erases all simulators successfully', async () => {
77+
const exec = createMockExecutor({ success: true, output: 'OK' });
78+
const res = await erase_simsLogic({ all: true }, exec);
79+
expect(res).toEqual({
80+
content: [{ type: 'text', text: 'Successfully erased all simulators' }],
81+
});
82+
});
83+
84+
it('returns failure when erase all fails', async () => {
85+
const exec = createMockExecutor({ success: false, error: 'Denied' });
86+
const res = await erase_simsLogic({ all: true }, exec);
87+
expect(res).toEqual({
88+
content: [{ type: 'text', text: 'Failed to erase all simulators: Denied' }],
89+
});
90+
});
91+
92+
it('performs shutdown all when shutdownFirst=true', async () => {
93+
const calls: any[] = [];
94+
const exec = async (cmd: string[]) => {
95+
calls.push(cmd);
96+
return { success: true, output: 'OK', error: '', process: { pid: 1 } as any };
97+
};
98+
const res = await erase_simsLogic({ all: true, shutdownFirst: true }, exec as any);
99+
expect(calls).toEqual([
100+
['xcrun', 'simctl', 'shutdown', 'all'],
101+
['xcrun', 'simctl', 'erase', 'all'],
102+
]);
103+
expect(res).toEqual({
104+
content: [{ type: 'text', text: 'Successfully erased all simulators' }],
105+
});
106+
});
107+
108+
it('adds tool hint on booted error without shutdownFirst (all mode)', async () => {
109+
const bootedError = 'Unable to erase contents and settings in current state: Booted';
110+
const exec = createMockExecutor({ success: false, error: bootedError });
111+
const res = await erase_simsLogic({ all: true }, exec);
112+
expect((res.content?.[1] as any).text).toContain('Tool hint');
113+
expect((res.content?.[1] as any).text).toContain('shutdownFirst: true');
114+
});
115+
});
116+
});

src/mcp/tools/simulator-management/__tests__/index.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe('simulator-management workflow metadata', () => {
2121

2222
it('should have correct description', () => {
2323
expect(workflow.description).toBe(
24-
'Tools for managing simulators from booting, opening simulators, listing simulators, stopping simulators and setting simulator environment options like location, network, statusbar and appearance.',
24+
'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.',
2525
);
2626
});
2727

@@ -46,6 +46,7 @@ describe('simulator-management workflow metadata', () => {
4646
'location',
4747
'network',
4848
'statusbar',
49+
'erase',
4950
]);
5051
});
5152
});
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { z } from 'zod';
2+
import { ToolResponse, type ToolResponseContent } from '../../../types/common.ts';
3+
import { log } from '../../../utils/logging/index.ts';
4+
import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
5+
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
6+
7+
const eraseSimsBaseSchema = z.object({
8+
simulatorUdid: z.string().uuid().optional().describe('UDID of the simulator to erase.'),
9+
all: z.boolean().optional().describe('When true, erases all simulators.'),
10+
shutdownFirst: z
11+
.boolean()
12+
.optional()
13+
.describe('If true, shuts down the target (UDID or all) before erasing.'),
14+
});
15+
16+
const eraseSimsSchema = eraseSimsBaseSchema.refine(
17+
(v) => {
18+
const selectors = (v.simulatorUdid ? 1 : 0) + (v.all === true ? 1 : 0);
19+
return selectors === 1;
20+
},
21+
{ message: 'Provide exactly one of: simulatorUdid or all=true.' },
22+
);
23+
24+
type EraseSimsParams = z.infer<typeof eraseSimsSchema>;
25+
26+
export async function erase_simsLogic(
27+
params: EraseSimsParams,
28+
executor: CommandExecutor,
29+
): Promise<ToolResponse> {
30+
try {
31+
if (params.simulatorUdid) {
32+
const udid = params.simulatorUdid;
33+
log(
34+
'info',
35+
`Erasing simulator ${udid}${params.shutdownFirst ? ' (shutdownFirst=true)' : ''}`,
36+
);
37+
38+
if (params.shutdownFirst) {
39+
try {
40+
await executor(
41+
['xcrun', 'simctl', 'shutdown', udid],
42+
'Shutdown Simulator',
43+
true,
44+
undefined,
45+
);
46+
} catch {
47+
// ignore shutdown errors; proceed to erase attempt
48+
}
49+
}
50+
51+
const result = await executor(
52+
['xcrun', 'simctl', 'erase', udid],
53+
'Erase Simulator',
54+
true,
55+
undefined,
56+
);
57+
if (result.success) {
58+
return { content: [{ type: 'text', text: `Successfully erased simulator ${udid}` }] };
59+
}
60+
61+
// Add tool hint if simulator is booted and shutdownFirst was not requested
62+
const errText = result.error ?? 'Unknown error';
63+
if (/Unable to erase contents and settings.*Booted/i.test(errText) && !params.shutdownFirst) {
64+
return {
65+
content: [
66+
{ type: 'text', text: `Failed to erase simulator: ${errText}` },
67+
{
68+
type: 'text',
69+
text: `Tool hint: The simulator appears to be Booted. Re-run erase_sims with { simulatorUdid: '${udid}', shutdownFirst: true } to shut it down before erasing.`,
70+
},
71+
],
72+
};
73+
}
74+
75+
return {
76+
content: [{ type: 'text', text: `Failed to erase simulator: ${errText}` }],
77+
};
78+
}
79+
80+
if (params.all === true) {
81+
log('info', `Erasing ALL simulators${params.shutdownFirst ? ' (shutdownFirst=true)' : ''}`);
82+
if (params.shutdownFirst) {
83+
try {
84+
await executor(
85+
['xcrun', 'simctl', 'shutdown', 'all'],
86+
'Shutdown All Simulators',
87+
true,
88+
undefined,
89+
);
90+
} catch {
91+
// ignore and continue to erase
92+
}
93+
}
94+
95+
const result = await executor(
96+
['xcrun', 'simctl', 'erase', 'all'],
97+
'Erase All Simulators',
98+
true,
99+
undefined,
100+
);
101+
if (!result.success) {
102+
const errText = result.error ?? 'Unknown error';
103+
const content: ToolResponseContent[] = [
104+
{ type: 'text', text: `Failed to erase all simulators: ${errText}` },
105+
];
106+
if (
107+
/Unable to erase contents and settings.*Booted/i.test(errText) &&
108+
!params.shutdownFirst
109+
) {
110+
content.push({
111+
type: 'text',
112+
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.',
113+
});
114+
}
115+
return { content };
116+
}
117+
return { content: [{ type: 'text', text: 'Successfully erased all simulators' }] };
118+
}
119+
120+
return {
121+
content: [{ type: 'text', text: 'Invalid parameters: provide simulatorUdid or all=true.' }],
122+
};
123+
} catch (error: unknown) {
124+
const message = error instanceof Error ? error.message : String(error);
125+
log('error', `Error erasing simulators: ${message}`);
126+
return { content: [{ type: 'text', text: `Failed to erase simulators: ${message}` }] };
127+
}
128+
}
129+
130+
export default {
131+
name: 'erase_sims',
132+
description:
133+
'Erases simulator content and settings. Provide exactly one of: simulatorUdid or all=true. Optional: shutdownFirst to shut down before erasing.',
134+
schema: eraseSimsBaseSchema.shape,
135+
handler: createTypedTool(eraseSimsSchema, erase_simsLogic, getDefaultCommandExecutor),
136+
};

src/mcp/tools/simulator-management/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22
* Simulator Management workflow
33
*
44
* Provides tools for working with simulators like booting and opening simulators, launching apps,
5-
* listing sims, stopping apps and setting sim environment options like location, network, statusbar and appearance.
5+
* listing sims, stopping apps, erasing simulator content and settings, and setting sim environment
6+
* options like location, network, statusbar and appearance.
67
*/
78

89
export const workflow = {
910
name: 'Simulator Management',
1011
description:
11-
'Tools for managing simulators from booting, opening simulators, listing simulators, stopping simulators and setting simulator environment options like location, network, statusbar and appearance.',
12+
'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.',
1213
platforms: ['iOS'],
1314
targets: ['simulator'],
1415
projectTypes: ['project', 'workspace'],
15-
capabilities: ['boot', 'open', 'list', 'appearance', 'location', 'network', 'statusbar'],
16+
capabilities: ['boot', 'open', 'list', 'appearance', 'location', 'network', 'statusbar', 'erase'],
1617
};

0 commit comments

Comments
 (0)