Skip to content

Commit bf78589

Browse files
committed
feat(simulator): migrate simulator tools to session defaults
1 parent 3c004aa commit bf78589

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+992
-1177
lines changed

docs/TOOLS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ XcodeBuildMCP provides 61 tools organized into 12 workflow groups for comprehens
6969
### Simulator Management (`simulator-management`)
7070
**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)
7171

72-
- `erase_sims` - Erases simulator content and settings. Provide exactly one of: simulatorUdid or all=true. Optional: shutdownFirst to shut down before erasing.
72+
- `erase_sims` - Erases simulator content and settings for a specific simulator. Requires simulatorUdid. Optional: shutdownFirst to shut down before erasing.
7373
- `reset_sim_location` - Resets the simulator's location to default.
7474
- `set_sim_appearance` - Sets the appearance mode (dark/light) of an iOS simulator.
7575
- `set_sim_location` - Sets a custom GPS location for the simulator.

docs/session-aware-migration-todo.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,14 @@ Reference: `docs/session_management_plan.md`
4141
- [x] `src/mcp/tools/simulator/record_sim_video.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
4242

4343
## Simulator Management
44-
- [ ] `src/mcp/tools/simulator-management/erase_sims.ts` — session defaults: `simulatorId` (covers `simulatorUdid`).
45-
- [ ] `src/mcp/tools/simulator-management/set_sim_location.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
46-
- [ ] `src/mcp/tools/simulator-management/reset_sim_location.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
47-
- [ ] `src/mcp/tools/simulator-management/set_sim_appearance.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
48-
- [ ] `src/mcp/tools/simulator-management/sim_statusbar.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
44+
- [x] `src/mcp/tools/simulator-management/erase_sims.ts` — session defaults: `simulatorId` (covers `simulatorUdid`).
45+
- [x] `src/mcp/tools/simulator-management/set_sim_location.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
46+
- [x] `src/mcp/tools/simulator-management/reset_sim_location.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
47+
- [x] `src/mcp/tools/simulator-management/set_sim_appearance.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
48+
- [x] `src/mcp/tools/simulator-management/sim_statusbar.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
4949

5050
## Simulator Logging
51-
- [ ] `src/mcp/tools/logging/start_sim_log_cap.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
51+
- [x] `src/mcp/tools/logging/start_sim_log_cap.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
5252

5353
## AXe UI Testing Tools
5454
- [ ] `src/mcp/tools/ui-testing/button.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
# xcode-build-server files
3+
buildServer.json
4+
.compile
5+
6+
# Local build artifacts
7+
.build/

src/mcp/tools/logging/__tests__/start_sim_log_cap.test.ts

Lines changed: 22 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Tests for start_sim_log_cap plugin
33
*/
4-
import { describe, it, expect, beforeEach } from 'vitest';
4+
import { describe, it, expect } from 'vitest';
55
import { z } from 'zod';
66
import plugin, { start_sim_log_capLogic } from '../start_sim_log_cap.ts';
77
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
@@ -33,51 +33,30 @@ describe('start_sim_log_cap plugin', () => {
3333

3434
it('should validate schema with valid parameters', () => {
3535
const schema = z.object(plugin.schema);
36-
expect(
37-
schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: 'com.example.app' }).success,
38-
).toBe(true);
39-
expect(
40-
schema.safeParse({
41-
simulatorUuid: 'test-uuid',
42-
bundleId: 'com.example.app',
43-
captureConsole: true,
44-
}).success,
45-
).toBe(true);
46-
expect(
47-
schema.safeParse({
48-
simulatorUuid: 'test-uuid',
49-
bundleId: 'com.example.app',
50-
captureConsole: false,
51-
}).success,
52-
).toBe(true);
36+
expect(schema.safeParse({ bundleId: 'com.example.app' }).success).toBe(true);
37+
expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: true }).success).toBe(
38+
true,
39+
);
40+
expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: false }).success).toBe(
41+
true,
42+
);
5343
});
5444

5545
it('should reject invalid schema parameters', () => {
5646
const schema = z.object(plugin.schema);
57-
expect(schema.safeParse({ simulatorUuid: null, bundleId: 'com.example.app' }).success).toBe(
47+
expect(schema.safeParse({ bundleId: null }).success).toBe(false);
48+
expect(schema.safeParse({ captureConsole: true }).success).toBe(false);
49+
expect(schema.safeParse({}).success).toBe(false);
50+
expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: 'yes' }).success).toBe(
5851
false,
5952
);
60-
expect(
61-
schema.safeParse({ simulatorUuid: undefined, bundleId: 'com.example.app' }).success,
62-
).toBe(false);
63-
expect(schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: null }).success).toBe(false);
64-
expect(schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: undefined }).success).toBe(
53+
expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: 123 }).success).toBe(
6554
false,
6655
);
67-
expect(
68-
schema.safeParse({
69-
simulatorUuid: 'test-uuid',
70-
bundleId: 'com.example.app',
71-
captureConsole: 'yes',
72-
}).success,
73-
).toBe(false);
74-
expect(
75-
schema.safeParse({
76-
simulatorUuid: 'test-uuid',
77-
bundleId: 'com.example.app',
78-
captureConsole: 123,
79-
}).success,
80-
).toBe(false);
56+
57+
const withSimId = schema.safeParse({ simulatorId: 'test-uuid', bundleId: 'com.example.app' });
58+
expect(withSimId.success).toBe(true);
59+
expect('simulatorId' in (withSimId.data as any)).toBe(false);
8160
});
8261
});
8362

@@ -98,7 +77,7 @@ describe('start_sim_log_cap plugin', () => {
9877

9978
const result = await start_sim_log_capLogic(
10079
{
101-
simulatorUuid: 'test-uuid',
80+
simulatorId: 'test-uuid',
10281
bundleId: 'com.example.app',
10382
},
10483
mockExecutor,
@@ -122,7 +101,7 @@ describe('start_sim_log_cap plugin', () => {
122101

123102
const result = await start_sim_log_capLogic(
124103
{
125-
simulatorUuid: 'test-uuid',
104+
simulatorId: 'test-uuid',
126105
bundleId: 'com.example.app',
127106
},
128107
mockExecutor,
@@ -148,7 +127,7 @@ describe('start_sim_log_cap plugin', () => {
148127

149128
const result = await start_sim_log_capLogic(
150129
{
151-
simulatorUuid: 'test-uuid',
130+
simulatorId: 'test-uuid',
152131
bundleId: 'com.example.app',
153132
captureConsole: true,
154133
},
@@ -208,7 +187,7 @@ describe('start_sim_log_cap plugin', () => {
208187

209188
await start_sim_log_capLogic(
210189
{
211-
simulatorUuid: 'test-uuid',
190+
simulatorId: 'test-uuid',
212191
bundleId: 'com.example.app',
213192
captureConsole: true,
214193
},
@@ -277,7 +256,7 @@ describe('start_sim_log_cap plugin', () => {
277256

278257
await start_sim_log_capLogic(
279258
{
280-
simulatorUuid: 'test-uuid',
259+
simulatorId: 'test-uuid',
281260
bundleId: 'com.example.app',
282261
captureConsole: false,
283262
},

src/mcp/tools/logging/start_sim_log_cap.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import { z } from 'zod';
88
import { startLogCapture } from '../../../utils/log-capture/index.ts';
99
import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/command.ts';
1010
import { ToolResponse, createTextContent } from '../../../types/common.ts';
11-
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
11+
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
1212

1313
// Define schema as ZodObject
1414
const startSimLogCapSchema = z.object({
15-
simulatorUuid: z
15+
simulatorId: z
1616
.string()
17+
.uuid()
1718
.describe('UUID of the simulator to capture logs from (obtained from list_simulators).'),
1819
bundleId: z.string().describe('Bundle identifier of the app to capture logs for.'),
1920
captureConsole: z
@@ -30,11 +31,15 @@ export async function start_sim_log_capLogic(
3031
_executor: CommandExecutor = getDefaultCommandExecutor(),
3132
logCaptureFunction: typeof startLogCapture = startLogCapture,
3233
): Promise<ToolResponse> {
33-
const paramsWithDefaults = {
34-
...params,
35-
captureConsole: params.captureConsole ?? false,
36-
};
37-
const { sessionId, error } = await logCaptureFunction(paramsWithDefaults, _executor);
34+
const captureConsole = params.captureConsole ?? false;
35+
const { sessionId, error } = await logCaptureFunction(
36+
{
37+
simulatorUuid: params.simulatorId,
38+
bundleId: params.bundleId,
39+
captureConsole,
40+
},
41+
_executor,
42+
);
3843
if (error) {
3944
return {
4045
content: [createTextContent(`Error starting log capture: ${error}`)],
@@ -44,16 +49,23 @@ export async function start_sim_log_capLogic(
4449
return {
4550
content: [
4651
createTextContent(
47-
`Log capture started successfully. Session ID: ${sessionId}.\n\n${paramsWithDefaults.captureConsole ? 'Note: Your app was relaunched to capture console output.' : 'Note: Only structured logs are being captured.'}\n\nNext Steps:\n1. Interact with your simulator and app.\n2. Use 'stop_sim_log_cap' with session ID '${sessionId}' to stop capture and retrieve logs.`,
52+
`Log capture started successfully. Session ID: ${sessionId}.\n\n${captureConsole ? 'Note: Your app was relaunched to capture console output.' : 'Note: Only structured logs are being captured.'}\n\nNext Steps:\n1. Interact with your simulator and app.\n2. Use 'stop_sim_log_cap' with session ID '${sessionId}' to stop capture and retrieve logs.`,
4853
),
4954
],
5055
};
5156
}
5257

58+
const publicSchemaObject = startSimLogCapSchema.omit({ simulatorId: true } as const).strict();
59+
5360
export default {
5461
name: 'start_sim_log_cap',
5562
description:
5663
'Starts capturing logs from a specified simulator. Returns a session ID. By default, captures only structured logs.',
57-
schema: startSimLogCapSchema.shape, // MCP SDK compatibility
58-
handler: createTypedTool(startSimLogCapSchema, start_sim_log_capLogic, getDefaultCommandExecutor),
64+
schema: publicSchemaObject.shape, // MCP SDK compatibility
65+
handler: createSessionAwareTool<StartSimLogCapParams>({
66+
internalSchema: startSimLogCapSchema as unknown as z.ZodType<StartSimLogCapParams>,
67+
logicFunction: start_sim_log_capLogic,
68+
getExecutor: getDefaultCommandExecutor,
69+
requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
70+
}),
5971
};

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

Lines changed: 8 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@ import { z } from 'zod';
33
import eraseSims, { erase_simsLogic } from '../erase_sims.ts';
44
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
55

6-
describe('erase_sims tool (UDID or ALL only)', () => {
6+
describe('erase_sims tool (single simulator)', () => {
77
describe('Export Field Validation (Literal)', () => {
88
it('should have correct name', () => {
99
expect(eraseSims.name).toBe('erase_sims');
1010
});
1111

1212
it('should have correct description', () => {
13-
expect(eraseSims.description).toContain('Provide exactly one of: simulatorUdid or all=true');
14-
expect(eraseSims.description).toContain('shutdownFirst');
13+
expect(eraseSims.description).toBe('Erases a simulator by UDID.');
1514
});
1615

1716
it('should have handler function', () => {
@@ -20,27 +19,23 @@ describe('erase_sims tool (UDID or ALL only)', () => {
2019

2120
it('should validate schema fields (shape only)', () => {
2221
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.
22+
expect(schema.safeParse({ shutdownFirst: true }).success).toBe(true);
23+
expect(schema.safeParse({}).success).toBe(true);
2924
});
3025
});
3126

3227
describe('Single mode', () => {
3328
it('erases a simulator successfully', async () => {
3429
const mock = createMockExecutor({ success: true, output: 'OK' });
35-
const res = await erase_simsLogic({ simulatorUdid: 'UD1' }, mock);
30+
const res = await erase_simsLogic({ simulatorId: 'UD1' }, mock);
3631
expect(res).toEqual({
3732
content: [{ type: 'text', text: 'Successfully erased simulator UD1' }],
3833
});
3934
});
4035

4136
it('returns failure when erase fails', async () => {
4237
const mock = createMockExecutor({ success: false, error: 'Booted device' });
43-
const res = await erase_simsLogic({ simulatorUdid: 'UD1' }, mock);
38+
const res = await erase_simsLogic({ simulatorId: 'UD1' }, mock);
4439
expect(res).toEqual({
4540
content: [{ type: 'text', text: 'Failed to erase simulator: Booted device' }],
4641
});
@@ -50,7 +45,7 @@ describe('erase_sims tool (UDID or ALL only)', () => {
5045
const bootedError =
5146
'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';
5247
const mock = createMockExecutor({ success: false, error: bootedError });
53-
const res = await erase_simsLogic({ simulatorUdid: 'UD1' }, mock);
48+
const res = await erase_simsLogic({ simulatorId: 'UD1' }, mock);
5449
expect((res.content?.[1] as any).text).toContain('Tool hint');
5550
expect((res.content?.[1] as any).text).toContain('shutdownFirst: true');
5651
});
@@ -61,7 +56,7 @@ describe('erase_sims tool (UDID or ALL only)', () => {
6156
calls.push(cmd);
6257
return { success: true, output: 'OK', error: '', process: { pid: 1 } as any };
6358
};
64-
const res = await erase_simsLogic({ simulatorUdid: 'UD1', shutdownFirst: true }, exec as any);
59+
const res = await erase_simsLogic({ simulatorId: 'UD1', shutdownFirst: true }, exec as any);
6560
expect(calls).toEqual([
6661
['xcrun', 'simctl', 'shutdown', 'UD1'],
6762
['xcrun', 'simctl', 'erase', 'UD1'],
@@ -71,46 +66,4 @@ describe('erase_sims tool (UDID or ALL only)', () => {
7166
});
7267
});
7368
});
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-
});
11669
});

0 commit comments

Comments
 (0)