Skip to content
25 changes: 25 additions & 0 deletions manifests/tools/clone_sims.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
id: clone_sims
module: mcp/tools/simulator-management/clone_sims
names:
mcp: clone_sims
cli: clone
description: Clone an existing simulator.
outputSchema:
schema: xcodebuildmcp.output.simulator-action-result
version: "1"
annotations:
title: Clone Simulator
readOnlyHint: false
destructiveHint: false
openWorldHint: false
nextSteps:
- label: List simulators to see the clone
toolId: list_sims
priority: 1
when: success
- label: Boot the cloned simulator
toolId: boot_sim
params:
simulatorId: NEW_UDID
priority: 2
when: success
25 changes: 25 additions & 0 deletions manifests/tools/create_sim.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
id: create_sim
module: mcp/tools/simulator-management/create_sim
names:
mcp: create_sim
cli: create
description: Create a new simulator.
outputSchema:
schema: xcodebuildmcp.output.simulator-action-result
version: "1"
annotations:
title: Create Simulator
readOnlyHint: false
destructiveHint: false
openWorldHint: false
nextSteps:
- label: List simulators to see the new device
toolId: list_sims
priority: 1
when: success
- label: Boot the new simulator
toolId: boot_sim
params:
simulatorId: NEW_UDID
priority: 2
when: success
19 changes: 19 additions & 0 deletions manifests/tools/delete_sims.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
id: delete_sims
module: mcp/tools/simulator-management/delete_sims
names:
mcp: delete_sims
cli: delete
description: Delete simulators by UDID, all simulators, or unavailable simulators.
outputSchema:
schema: xcodebuildmcp.output.simulator-action-result
version: "1"
annotations:
title: Delete Simulators
readOnlyHint: false
destructiveHint: true
openWorldHint: false
nextSteps:
- label: List remaining simulators
toolId: list_sims
priority: 1
when: success
3 changes: 3 additions & 0 deletions manifests/workflows/simulator-management.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ tools:
- set_sim_location
- reset_sim_location
- set_sim_appearance
- clone_sims
- create_sim
- delete_sims
- sim_statusbar
- toggle_software_keyboard
- toggle_connect_hardware_keyboard
52 changes: 52 additions & 0 deletions src/mcp/tools/simulator-management/__tests__/clone_sims.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, it, expect } from 'vitest';
import { schema, clone_simsLogic } from '../clone_sims.ts';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';

describe('clone_sims tool', () => {
describe('Plugin Structure', () => {
it('should expose schema', () => {
expect(schema).toBeDefined();
});
});

describe('Happy path', () => {
it('clones a simulator and captures the new UDID', async () => {
const newUdid = '00000000-0000-0000-0000-000000000001';
const mock = createMockExecutor({ success: true, output: `${newUdid}\n` });
const res = await runLogic(() =>
clone_simsLogic({ sourceSimulatorId: '00000000-0000-0000-0000-000000000000' }, mock),
);
expect(res.isError).toBeFalsy();
const text = allText(res);
expect(text).toContain('Simulator cloned successfully');
});

it('clones with a custom name', async () => {
const mock = createMockExecutor({ success: true, output: 'UUID1\n' });
const res = await runLogic(() =>
clone_simsLogic(
{
sourceSimulatorId: '00000000-0000-0000-0000-000000000000',
newName: 'My Clone',
},
mock,
),
);
expect(res.isError).toBeFalsy();
});
});

describe('Failure path', () => {
it('returns failure when clone fails', async () => {
const mock = createMockExecutor({ success: false, error: 'No such device' });
const res = await runLogic(() =>
clone_simsLogic({ sourceSimulatorId: '00000000-0000-0000-0000-000000000000' }, mock),
);
expect(res.isError).toBe(true);
const text = allText(res);
expect(text).toContain('Clone simulator failed');
expect(text).toContain('No such device');
});
});
});
52 changes: 52 additions & 0 deletions src/mcp/tools/simulator-management/__tests__/create_sim.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, it, expect } from 'vitest';
import { schema, create_simLogic } from '../create_sim.ts';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';

describe('create_sim tool', () => {
describe('Plugin Structure', () => {
it('should expose schema', () => {
expect(schema).toBeDefined();
});
});

describe('Happy path', () => {
it('creates a simulator and captures the new UDID', async () => {
const newUdid = '00000000-0000-0000-0000-000000000001';
const mock = createMockExecutor({ success: true, output: `${newUdid}\n` });
const res = await runLogic(() =>
create_simLogic(
{
name: 'Test Sim',
deviceType: 'iPhone 17',
runtime: 'iOS 26.4',
},
mock,
),
);
expect(res.isError).toBeFalsy();
const text = allText(res);
expect(text).toContain('Simulator created successfully');
});
});

describe('Failure path', () => {
it('returns failure when create fails', async () => {
const mock = createMockExecutor({ success: false, error: 'Invalid device type' });
const res = await runLogic(() =>
create_simLogic(
{
name: 'Bad Sim',
deviceType: 'NonExistent',
runtime: 'iOS 99',
},
mock,
),
);
expect(res.isError).toBe(true);
const text = allText(res);
expect(text).toContain('Create simulator failed');
expect(text).toContain('Invalid device type');
});
});
});
113 changes: 113 additions & 0 deletions src/mcp/tools/simulator-management/__tests__/delete_sims.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { describe, it, expect } from 'vitest';
import { schema, delete_simsLogic } from '../delete_sims.ts';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';

describe('delete_sims tool', () => {
describe('Plugin Structure', () => {
it('should expose schema', () => {
expect(schema).toBeDefined();
});
});

describe('Happy path', () => {
it('deletes a simulator by UDID', async () => {
const mock = createMockExecutor({ success: true, output: 'OK' });
const res = await runLogic(() =>
delete_simsLogic({ target: '00000000-0000-0000-0000-000000000000' }, mock),
);
expect(res.isError).toBeFalsy();
const text = allText(res);
expect(text).toContain('Simulator(s) deleted successfully');
});

it('deletes all simulators', async () => {
const mock = createMockExecutor({ success: true, output: 'OK' });
const res = await runLogic(() => delete_simsLogic({ target: 'all' }, mock));
expect(res.isError).toBeFalsy();
});

it('deletes unavailable simulators', async () => {
const mock = createMockExecutor({ success: true, output: 'OK' });
const res = await runLogic(() => delete_simsLogic({ target: 'unavailable' }, mock));
expect(res.isError).toBeFalsy();
});
});

describe('Shutdown first', () => {
it('shuts down before deleting 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 runLogic(() =>
delete_simsLogic(
{ target: '00000000-0000-0000-0000-000000000000', shutdownFirst: true },
exec as any,
),
);
expect(calls).toEqual([
['xcrun', 'simctl', 'shutdown', '00000000-0000-0000-0000-000000000000'],
['xcrun', 'simctl', 'delete', '00000000-0000-0000-0000-000000000000'],
]);
expect(res.isError).toBeFalsy();
});

it('shuts down all before deleting when target=all', 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 runLogic(() =>
delete_simsLogic({ target: 'all', shutdownFirst: true }, exec as any),
);
expect(calls).toEqual([
['xcrun', 'simctl', 'shutdown', 'all'],
['xcrun', 'simctl', 'delete', 'all'],
]);
expect(res.isError).toBeFalsy();
});

it('skips shutdown when target=unavailable', 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 runLogic(() =>
delete_simsLogic({ target: 'unavailable', shutdownFirst: true }, exec as any),
);
expect(calls).toEqual([['xcrun', 'simctl', 'delete', 'unavailable']]);
expect(res.isError).toBeFalsy();
});

it('does not shut down when shutdownFirst is not set', async () => {
const calls: any[] = [];
const exec = async (cmd: string[]) => {
calls.push(cmd);
return { success: true, output: 'OK', error: '', process: { pid: 1 } as any };
};
await runLogic(() =>
delete_simsLogic({ target: '00000000-0000-0000-0000-000000000000' }, exec as any),
);
expect(calls).toEqual([
['xcrun', 'simctl', 'delete', '00000000-0000-0000-0000-000000000000'],
]);
});
});

describe('Failure path', () => {
it('returns failure when delete fails', async () => {
const mock = createMockExecutor({ success: false, error: 'Unable to delete' });
const res = await runLogic(() =>
delete_simsLogic({ target: '00000000-0000-0000-0000-000000000000' }, mock),
);
expect(res.isError).toBe(true);
const text = allText(res);
expect(text).toContain('Failed to delete simulator');
expect(text).toContain('Unable to delete');
});
});
});
Loading
Loading