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: '2'
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: '2'
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: '2'
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
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,62 @@
"required": [
"type"
]
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
"const": "clone"
},
"sourceSimulatorId": {
"type": "string"
}
},
"required": [
"type",
"sourceSimulatorId"
]
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
"const": "create"
},
"name": {
"type": "string"
},
"deviceType": {
"type": "string"
},
"runtime": {
"type": "string"
}
},
"required": [
"type",
"name",
"deviceType",
"runtime"
]
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
"const": "delete"
},
"target": {
"type": "string"
}
},
"required": [
"type",
"target"
]
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,62 @@
"required": [
"type"
]
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
"const": "clone"
},
"sourceSimulatorId": {
"type": "string"
}
},
"required": [
"type",
"sourceSimulatorId"
]
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
"const": "create"
},
"name": {
"type": "string"
},
"deviceType": {
"type": "string"
},
"runtime": {
"type": "string"
}
},
"required": [
"type",
"name",
"deviceType",
"runtime"
]
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
"const": "delete"
},
"target": {
"type": "string"
}
},
"required": [
"type",
"target"
]
}
]
},
Expand Down
83 changes: 83 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,83 @@
import { describe, it, expect } from 'vitest';
import { schema, clone_simsLogic, createCloneSimsExecutor } 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',
newName: 'My Clone',
},
mock,
),
);
expect(res.isError).toBeFalsy();
const text = allText(res);
expect(text).toContain('Simulator cloned successfully');
});

it('passes the required custom name to simctl clone', async () => {
const calls: string[][] = [];
const mock = createMockExecutor({
success: true,
output: 'UUID1\n',
onExecute: (command) => calls.push(command),
});

const executeCloneSims = createCloneSimsExecutor(mock);
const result = await executeCloneSims({
sourceSimulatorId: '00000000-0000-0000-0000-000000000000',
newName: 'My Clone',
});

expect(result.didError).toBe(false);
expect(calls).toEqual([
['xcrun', 'simctl', 'clone', '00000000-0000-0000-0000-000000000000', 'My Clone'],
]);
});
});

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',
newName: 'My Clone',
},
mock,
),
);
expect(res.isError).toBe(true);
const text = allText(res);
expect(text).toContain('Clone simulator failed');
expect(text).toContain('No such device');
});

it('omits artifacts when clone fails before producing a cloned simulator ID', async () => {
const mock = createMockExecutor({ success: false, error: 'No such device' });
const executeCloneSims = createCloneSimsExecutor(mock);

const result = await executeCloneSims({
sourceSimulatorId: '00000000-0000-0000-0000-000000000000',
newName: 'My Clone',
});

expect(result.didError).toBe(true);
expect(result.artifacts).toBeUndefined();
});
});
});
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');
});
});
});
Loading
Loading