Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/TOOLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ XcodeBuildMCP provides 61 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: simulatorUdid or all=true. Optional: shutdownFirst to shut down before erasing.
- `erase_sims` - Erases simulator content and settings for a specific simulator. Requires simulatorUdid. Optional: shutdownFirst to shut down before erasing.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix documentation to use simulatorId instead of simulatorUdid.

The documentation references simulatorUdid, but this PR consistently migrates the codebase to use simulatorId. Update the description to match.

Apply this diff:

-- `erase_sims` - Erases simulator content and settings for a specific simulator. Requires simulatorUdid. Optional: shutdownFirst to shut down before erasing.
+- `erase_sims` - Erases simulator content and settings for a specific simulator. Requires simulatorId. Optional: shutdownFirst to shut down before erasing.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- `erase_sims` - Erases simulator content and settings for a specific simulator. Requires simulatorUdid. Optional: shutdownFirst to shut down before erasing.
- `erase_sims` - Erases simulator content and settings for a specific simulator. Requires simulatorId. Optional: shutdownFirst to shut down before erasing.
🤖 Prompt for AI Agents
In docs/TOOLS.md around line 72, the description for `erase_sims` uses the old
parameter name `simulatorUdid`; update the text to reference `simulatorId`
instead (e.g., "Requires simulatorId. Optional: shutdownFirst to shut down
before erasing.") so the documentation matches the codebase rename.

- `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.
Expand Down
12 changes: 6 additions & 6 deletions docs/session-aware-migration-todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ Reference: `docs/session_management_plan.md`
- [x] `src/mcp/tools/simulator/record_sim_video.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).

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

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

## AXe UI Testing Tools
- [ ] `src/mcp/tools/ui-testing/button.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
Expand Down
7 changes: 7 additions & 0 deletions example_projects/iOS_Calculator/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

# xcode-build-server files
buildServer.json
.compile

# Local build artifacts
.build/
65 changes: 22 additions & 43 deletions src/mcp/tools/logging/__tests__/start_sim_log_cap.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Tests for start_sim_log_cap plugin
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { describe, it, expect } from 'vitest';
import { z } from 'zod';
import plugin, { start_sim_log_capLogic } from '../start_sim_log_cap.ts';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
Expand Down Expand Up @@ -33,51 +33,30 @@ describe('start_sim_log_cap plugin', () => {

it('should validate schema with valid parameters', () => {
const schema = z.object(plugin.schema);
expect(
schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: 'com.example.app' }).success,
).toBe(true);
expect(
schema.safeParse({
simulatorUuid: 'test-uuid',
bundleId: 'com.example.app',
captureConsole: true,
}).success,
).toBe(true);
expect(
schema.safeParse({
simulatorUuid: 'test-uuid',
bundleId: 'com.example.app',
captureConsole: false,
}).success,
).toBe(true);
expect(schema.safeParse({ bundleId: 'com.example.app' }).success).toBe(true);
expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: true }).success).toBe(
true,
);
expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: false }).success).toBe(
true,
);
});

it('should reject invalid schema parameters', () => {
const schema = z.object(plugin.schema);
expect(schema.safeParse({ simulatorUuid: null, bundleId: 'com.example.app' }).success).toBe(
expect(schema.safeParse({ bundleId: null }).success).toBe(false);
expect(schema.safeParse({ captureConsole: true }).success).toBe(false);
expect(schema.safeParse({}).success).toBe(false);
expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: 'yes' }).success).toBe(
false,
);
expect(
schema.safeParse({ simulatorUuid: undefined, bundleId: 'com.example.app' }).success,
).toBe(false);
expect(schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: null }).success).toBe(false);
expect(schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: undefined }).success).toBe(
expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: 123 }).success).toBe(
false,
);
expect(
schema.safeParse({
simulatorUuid: 'test-uuid',
bundleId: 'com.example.app',
captureConsole: 'yes',
}).success,
).toBe(false);
expect(
schema.safeParse({
simulatorUuid: 'test-uuid',
bundleId: 'com.example.app',
captureConsole: 123,
}).success,
).toBe(false);

const withSimId = schema.safeParse({ simulatorId: 'test-uuid', bundleId: 'com.example.app' });
expect(withSimId.success).toBe(true);
expect('simulatorId' in (withSimId.data as any)).toBe(false);
});
});

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

const result = await start_sim_log_capLogic(
{
simulatorUuid: 'test-uuid',
simulatorId: 'test-uuid',
bundleId: 'com.example.app',
},
mockExecutor,
Expand All @@ -122,7 +101,7 @@ describe('start_sim_log_cap plugin', () => {

const result = await start_sim_log_capLogic(
{
simulatorUuid: 'test-uuid',
simulatorId: 'test-uuid',
bundleId: 'com.example.app',
},
mockExecutor,
Expand All @@ -148,7 +127,7 @@ describe('start_sim_log_cap plugin', () => {

const result = await start_sim_log_capLogic(
{
simulatorUuid: 'test-uuid',
simulatorId: 'test-uuid',
bundleId: 'com.example.app',
captureConsole: true,
},
Expand Down Expand Up @@ -208,7 +187,7 @@ describe('start_sim_log_cap plugin', () => {

await start_sim_log_capLogic(
{
simulatorUuid: 'test-uuid',
simulatorId: 'test-uuid',
bundleId: 'com.example.app',
captureConsole: true,
},
Expand Down Expand Up @@ -277,7 +256,7 @@ describe('start_sim_log_cap plugin', () => {

await start_sim_log_capLogic(
{
simulatorUuid: 'test-uuid',
simulatorId: 'test-uuid',
bundleId: 'com.example.app',
captureConsole: false,
},
Expand Down
32 changes: 22 additions & 10 deletions src/mcp/tools/logging/start_sim_log_cap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import { z } from 'zod';
import { startLogCapture } from '../../../utils/log-capture/index.ts';
import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/command.ts';
import { ToolResponse, createTextContent } from '../../../types/common.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';

// Define schema as ZodObject
const startSimLogCapSchema = z.object({
simulatorUuid: z
simulatorId: z
.string()
.uuid()
.describe('UUID of the simulator to capture logs from (obtained from list_simulators).'),
bundleId: z.string().describe('Bundle identifier of the app to capture logs for.'),
captureConsole: z
Expand All @@ -30,11 +31,15 @@ export async function start_sim_log_capLogic(
_executor: CommandExecutor = getDefaultCommandExecutor(),
logCaptureFunction: typeof startLogCapture = startLogCapture,
): Promise<ToolResponse> {
const paramsWithDefaults = {
...params,
captureConsole: params.captureConsole ?? false,
};
const { sessionId, error } = await logCaptureFunction(paramsWithDefaults, _executor);
const captureConsole = params.captureConsole ?? false;
const { sessionId, error } = await logCaptureFunction(
{
simulatorUuid: params.simulatorId,
bundleId: params.bundleId,
captureConsole,
},
_executor,
);
if (error) {
return {
content: [createTextContent(`Error starting log capture: ${error}`)],
Expand All @@ -44,16 +49,23 @@ export async function start_sim_log_capLogic(
return {
content: [
createTextContent(
`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.`,
`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.`,
),
],
};
}

const publicSchemaObject = startSimLogCapSchema.omit({ simulatorId: true } as const).strict();

export default {
name: 'start_sim_log_cap',
description:
'Starts capturing logs from a specified simulator. Returns a session ID. By default, captures only structured logs.',
schema: startSimLogCapSchema.shape, // MCP SDK compatibility
handler: createTypedTool(startSimLogCapSchema, start_sim_log_capLogic, getDefaultCommandExecutor),
schema: publicSchemaObject.shape, // MCP SDK compatibility
handler: createSessionAwareTool<StartSimLogCapParams>({
internalSchema: startSimLogCapSchema as unknown as z.ZodType<StartSimLogCapParams>,
logicFunction: start_sim_log_capLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
}),
};
63 changes: 8 additions & 55 deletions src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ 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('erase_sims tool (single simulator)', () => {
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');
expect(eraseSims.description).toBe('Erases a simulator by UDID.');
});

it('should have handler function', () => {
Expand All @@ -20,27 +19,23 @@ 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({ 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.
expect(schema.safeParse({ shutdownFirst: true }).success).toBe(true);
expect(schema.safeParse({}).success).toBe(true);
});
});

describe('Single mode', () => {
it('erases a simulator successfully', async () => {
const mock = createMockExecutor({ success: true, output: 'OK' });
const res = await erase_simsLogic({ simulatorUdid: 'UD1' }, mock);
const res = await erase_simsLogic({ simulatorId: '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);
const res = await erase_simsLogic({ simulatorId: 'UD1' }, mock);
expect(res).toEqual({
content: [{ type: 'text', text: 'Failed to erase simulator: Booted device' }],
});
Expand All @@ -50,7 +45,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({ simulatorUdid: 'UD1' }, mock);
const res = await erase_simsLogic({ simulatorId: 'UD1' }, mock);
expect((res.content?.[1] as any).text).toContain('Tool hint');
expect((res.content?.[1] as any).text).toContain('shutdownFirst: true');
});
Expand All @@ -61,7 +56,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({ simulatorUdid: 'UD1', shutdownFirst: true }, exec as any);
const res = await erase_simsLogic({ simulatorId: 'UD1', shutdownFirst: true }, exec as any);
expect(calls).toEqual([
['xcrun', 'simctl', 'shutdown', 'UD1'],
['xcrun', 'simctl', 'erase', 'UD1'],
Expand All @@ -71,46 +66,4 @@ describe('erase_sims tool (UDID or ALL only)', () => {
});
});
});

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');
});
});
});
Loading
Loading