Skip to content
Closed
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ The XcodeBuildMCP server provides the following tool capabilities:
- **Log Capture**: Capture run-time logs from a simulator
- **UI Automation**: Interact with simulator UI elements
- **Screenshot**: Capture screenshots from a simulator
- **Video Capture**: Start/stop simulator video capture to MP4

### Device management
- **Device Discovery**: List connected physical Apple devices over USB or Wi-Fi
Expand Down
9 changes: 5 additions & 4 deletions docs/TOOLS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# XcodeBuildMCP Tools Reference

XcodeBuildMCP provides 60 tools organized into 12 workflow groups for comprehensive Apple development workflows.
XcodeBuildMCP provides 61 tools organized into 12 workflow groups for comprehensive Apple development workflows.

## Workflow Groups

Expand All @@ -19,7 +19,7 @@ XcodeBuildMCP provides 60 tools organized into 12 workflow groups for comprehens
- `stop_app_device` - Stops an app running on a physical Apple device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). Requires deviceId and processId.
- `test_device` - Runs tests for an Apple project or workspace on a physical device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro) using xcodebuild test and parses xcresult output. Provide exactly one of projectPath or workspacePath.
### iOS Simulator Development (`simulator`)
**Purpose**: Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting simulators. Build, test, deploy, and interact with iOS apps on simulators. (11 tools)
**Purpose**: Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting simulators. Build, test, deploy, and interact with iOS apps on simulators. (12 tools)

- `boot_sim` - Boots an iOS simulator. After booting, use open_sim() to make the simulator visible.
- `build_run_sim` - Builds and runs an app from a project or workspace on a specific simulator by UUID or name. Provide exactly one of projectPath or workspacePath, and exactly one of simulatorId or simulatorName.
Expand All @@ -30,6 +30,7 @@ XcodeBuildMCP provides 60 tools organized into 12 workflow groups for comprehens
- `launch_app_sim` - Launches an app in an iOS simulator by UUID or name. If simulator window isn't visible, use open_sim() first. or launch_app_sim({ simulatorName: 'iPhone 16', bundleId: 'com.example.MyApp' })
- `list_sims` - Lists available iOS simulators with their UUIDs.
- `open_sim` - Opens the iOS Simulator app.
- `record_sim` - Starts or stops video capture for an iOS simulator using AXe. Provide exactly one of start=true or stop=true. On stop, outputFile is required. fps defaults to 30.
- `stop_app_sim` - Stops an app running in an iOS simulator by UUID or name. or stop_app_sim({ simulatorName: "iPhone 16", bundleId: "com.example.MyApp" })
- `test_sim` - Runs tests on a simulator by UUID or name using xcodebuild test and parses xcresult output. Works with both Xcode projects (.xcodeproj) and workspaces (.xcworkspace).
### Log Capture & Management (`logging`)
Expand Down Expand Up @@ -68,7 +69,7 @@ XcodeBuildMCP provides 60 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: simulatorUuid or all=true. Optional: shutdownFirst to shut down before erasing.
- `erase_sims` - Erases simulator content and settings. Provide exactly one of: simulatorUdid or all=true. Optional: shutdownFirst to shut down before erasing.
- `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 Expand Up @@ -103,7 +104,7 @@ XcodeBuildMCP provides 60 tools organized into 12 workflow groups for comprehens

## Summary Statistics

- **Total Tools**: 60 canonical tools + 22 re-exports = 82 total
- **Total Tools**: 61 canonical tools + 22 re-exports = 83 total
- **Workflow Groups**: 12

---
Expand Down
2 changes: 1 addition & 1 deletion scripts/bundle-axe.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ else
echo "📥 Downloading latest AXe release from GitHub..."

# Get latest release download URL
LATEST_RELEASE_URL="https://github.com/cameroncooke/AXe/releases/download/v1.0.0/AXe-macOS-v1.0.0.tar.gz"
LATEST_RELEASE_URL="https://github.com/cameroncooke/AXe/releases/download/v1.1.0/AXe-macOS-v1.1.0.tar.gz"

# Create temp directory
mkdir -p "$AXE_TEMP_DIR"
Expand Down
189 changes: 189 additions & 0 deletions src/mcp/tools/simulator/__tests__/record_sim_video.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import { z } from 'zod';

// Import the tool and logic
import tool, { record_sim_videoLogic } from '../record_sim.ts';
import { createMockFileSystemExecutor } from '../../../../test-utils/mock-executors.ts';

const DUMMY_EXECUTOR: any = (async () => ({ success: true })) as any; // CommandExecutor stub
const VALID_UUID = '00000000-0000-0000-0000-000000000000';

afterEach(() => {
vi.restoreAllMocks();
});

describe('record_sim tool - validation', () => {
it('errors when start and stop are both true (mutually exclusive)', async () => {
const res = await tool.handler({
simulatorUuid: VALID_UUID,
start: true,
stop: true,
} as any);

expect(res.isError).toBe(true);
const text = (res.content?.[0] as any)?.text ?? '';
expect(text.toLowerCase()).toContain('mutually exclusive');
});

it('errors when stop=true but outputFile is missing', async () => {
const res = await tool.handler({
simulatorUuid: VALID_UUID,
stop: true,
} as any);

expect(res.isError).toBe(true);
const text = (res.content?.[0] as any)?.text ?? '';
expect(text.toLowerCase()).toContain('outputfile is required');
});
});

describe('record_sim logic - start behavior', () => {
it('starts with default fps (30) and warns when outputFile is provided on start (ignored)', async () => {
const video: any = {
startSimulatorVideoCapture: async () => ({
started: true,
sessionId: 'sess-123',
}),
stopSimulatorVideoCapture: async () => ({
stopped: false,
}),
};

// DI for AXe helpers: available and version OK
const axe = {
areAxeToolsAvailable: () => true,
isAxeAtLeastVersion: async () => true,
createAxeNotAvailableResponse: () => ({
content: [{ type: 'text', text: 'AXe not available' }],
isError: true,
}),
};

const fs = createMockFileSystemExecutor();

const res = await record_sim_videoLogic(
{
simulatorUuid: VALID_UUID,
start: true,
// fps omitted to hit default 30
outputFile: '/tmp/ignored.mp4', // should be ignored with a note
} as any,
DUMMY_EXECUTOR,
axe,
video,
fs,
);

expect(res.isError).toBe(false);
const texts = (res.content ?? []).map((c: any) => c.text).join('\n');

expect(texts).toContain('🎥');
expect(texts).toMatch(/30\s*fps/i);
expect(texts.toLowerCase()).toContain('outputfile is ignored');
expect(texts).toContain('Next Steps');
expect(texts).toContain('stop: true');
expect(texts).toContain('outputFile');
});
});

describe('record_sim logic - end-to-end stop with rename', () => {
it('stops, parses stdout path, and renames to outputFile', async () => {
const video: any = {
startSimulatorVideoCapture: async () => ({
started: true,
sessionId: 'sess-abc',
}),
stopSimulatorVideoCapture: async () => ({
stopped: true,
parsedPath: '/tmp/recorded.mp4',
stdout: 'Saved to /tmp/recorded.mp4',
}),
};

const fs = createMockFileSystemExecutor();

const axe = {
areAxeToolsAvailable: () => true,
isAxeAtLeastVersion: async () => true,
createAxeNotAvailableResponse: () => ({
content: [{ type: 'text', text: 'AXe not available' }],
isError: true,
}),
};

// Start (not strictly required for stop path, but included to mimic flow)
const startRes = await record_sim_videoLogic(
{
simulatorUuid: VALID_UUID,
start: true,
} as any,
DUMMY_EXECUTOR,
axe,
video,
fs,
);
expect(startRes.isError).toBe(false);

// Stop and rename
const outputFile = '/var/videos/final.mp4';
const stopRes = await record_sim_videoLogic(
{
simulatorUuid: VALID_UUID,
stop: true,
outputFile,
} as any,
DUMMY_EXECUTOR,
axe,
video,
fs,
);

expect(stopRes.isError).toBe(false);
const texts = (stopRes.content ?? []).map((c: any) => c.text).join('\n');
expect(texts).toContain('Original file: /tmp/recorded.mp4');
expect(texts).toContain(`Saved to: ${outputFile}`);

// _meta should include final saved path
expect((stopRes as any)._meta?.outputFile).toBe(outputFile);
});
});

describe('record_sim logic - version gate', () => {
it('errors when AXe version is below 1.1.0', async () => {
const axe = {
areAxeToolsAvailable: () => true,
isAxeAtLeastVersion: async () => false,
createAxeNotAvailableResponse: () => ({
content: [{ type: 'text', text: 'AXe not available' }],
isError: true,
}),
};

const video: any = {
startSimulatorVideoCapture: async () => ({
started: true,
sessionId: 'sess-xyz',
}),
stopSimulatorVideoCapture: async () => ({
stopped: true,
}),
};

const fs = createMockFileSystemExecutor();

const res = await record_sim_videoLogic(
{
simulatorUuid: VALID_UUID,
start: true,
} as any,
DUMMY_EXECUTOR,
axe,
video,
fs,
);

expect(res.isError).toBe(true);
const text = (res.content?.[0] as any)?.text ?? '';
expect(text).toContain('AXe v1.1.0');
});
});
Loading
Loading