Generated: 2025-10-12 Purpose: Comprehensive analysis of XcodeBuildMCP's actual implementation, capabilities, and verified behavior patterns.
- Actual Tool Capabilities
- Session Defaults System
- Platform-Specific Behavior
- Log Capture Implementation
- Swift Package Tools
- Test Coverage Analysis
- Common Agent Failure Patterns
XcodeBuildMCP provides 83 total tools (61 canonical + 22 re-exports) organized into 12 workflow groups:
src/mcp/tools/
├── device/ # 15 tools - Physical device workflows
├── discovery/ # 1 tool - Dynamic tool discovery
├── doctor/ # 1 tool - System diagnostics
├── logging/ # 4 tools - Log capture
├── macos/ # 12 tools - macOS development
├── project-discovery/ # 6 tools - Project analysis
├── project-scaffolding/ # 2 tools - Project creation
├── session-management/ # 3 tools - Session defaults
├── simulator/ # 23 tools - Simulator workflows
├── simulator-management/ # 9 tools - Simulator control
├── swift-package/ # 6 tools - Swift Package Manager
├── ui-testing/ # 12 tools - UI automation
└── utilities/ # 1 tool - Clean operations
Reference: docs/TOOLS.md:1-113
All tools follow a consistent, testable pattern using dependency injection:
// Pattern from: src/mcp/tools/simulator/build_sim.ts:1-187
export async function build_simLogic(
params: BuildSimulatorParams,
executor: CommandExecutor, // Injected dependency
): Promise<ToolResponse> {
// Core business logic here
}
export default {
name: 'build_sim',
description: 'Builds an app for a simulator.',
schema: publicSchemaObject.shape, // MCP SDK compatibility
handler: createSessionAwareTool<BuildSimulatorParams>({
internalSchema: buildSimulatorSchema,
logicFunction: build_simLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [...],
exclusivePairs: [...]
}),
};Key Characteristics:
- Separate
*Logicfunction for testability - Zod schema validation
- Session-aware parameter merging
- CommandExecutor dependency injection
Reference: src/mcp/tools/simulator/build_sim.ts:140-187
Total Test Files: 78 comprehensive test files
Test Pattern: Dependency injection with mock executors (NO Vitest mocking allowed)
// Pattern from: src/mcp/tools/simulator/__tests__/build_sim.test.ts:77-99
it('should handle empty workspacePath parameter', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'BUILD SUCCEEDED'
});
const result = await build_simLogic(
{
workspacePath: '',
scheme: 'MyScheme',
simulatorName: 'iPhone 16',
},
mockExecutor,
);
expect(result.content).toEqual([
{ type: 'text', text: '✅ iOS Simulator Build build succeeded...' },
{ type: 'text', text: expect.stringContaining('Next Steps:') },
]);
});Reference: src/mcp/tools/simulator/__tests__/build_sim.test.ts:1-684
Session defaults provide a persistent parameter store that tools can read from, reducing repetitive parameter passing:
// Implementation: src/utils/session-store.ts:1-48
export type SessionDefaults = {
projectPath?: string;
workspacePath?: string;
scheme?: string;
configuration?: string;
simulatorName?: string;
simulatorId?: string;
deviceId?: string;
useLatestOS?: boolean;
arch?: 'arm64' | 'x86_64';
};
class SessionStore {
private defaults: SessionDefaults = {};
setDefaults(partial: Partial<SessionDefaults>): void {
this.defaults = { ...this.defaults, ...partial };
}
get<K extends keyof SessionDefaults>(key: K): SessionDefaults[K] {
return this.defaults[key];
}
getAll(): SessionDefaults { return { ...this.defaults }; }
}Reference: src/utils/session-store.ts:3-48
// Tool definition: src/mcp/tools/session-management/session_set_defaults.ts:1-58
session-set-defaults({
workspacePath: "/path/to/MyApp.xcworkspace",
scheme: "MyScheme",
configuration: "Debug",
simulatorName: "iPhone 16"
})Mutual Exclusivity Rules:
projectPath⊕workspacePath- Cannot set bothsimulatorId⊕simulatorName- Cannot set both
Automatic Clearing: Setting one side of a mutually exclusive pair clears the other:
// From: src/mcp/tools/session-management/session_set_defaults.ts:31-48
export async function sessionSetDefaultsLogic(params: Params): Promise<ToolResponse> {
// Clear mutually exclusive counterparts before merging
const toClear = new Set<keyof SessionDefaults>();
if (Object.prototype.hasOwnProperty.call(params, 'projectPath'))
toClear.add('workspacePath');
if (Object.prototype.hasOwnProperty.call(params, 'workspacePath'))
toClear.add('projectPath');
if (Object.prototype.hasOwnProperty.call(params, 'simulatorId'))
toClear.add('simulatorName');
if (Object.prototype.hasOwnProperty.call(params, 'simulatorName'))
toClear.add('simulatorId');
if (toClear.size > 0) {
sessionStore.clear(Array.from(toClear));
}
sessionStore.setDefaults(params as Partial<SessionDefaults>);
return { content: [...], isError: false };
}Reference: src/mcp/tools/session-management/session_set_defaults.ts:31-48
Tools use createSessionAwareTool to merge session defaults with explicit parameters:
// Pattern from: src/utils/typed-tool-factory.ts:74-174
export function createSessionAwareTool<TParams>(opts: {
internalSchema: z.ZodType<TParams>;
logicFunction: (params: TParams, executor: CommandExecutor) => Promise<ToolResponse>;
getExecutor: () => CommandExecutor;
requirements?: SessionRequirement[]; // Validation rules
exclusivePairs?: (keyof SessionDefaults)[][]; // XOR constraints
}) {
return async (rawArgs: Record<string, unknown>): Promise<ToolResponse> => {
// 1. Sanitize: treat null/undefined as "not provided"
const sanitizedArgs = Object.entries(rawArgs)
.filter(([_, v]) => v !== null && v !== undefined)
.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {});
// 2. Merge: session defaults + explicit args (args override)
const merged = { ...sessionStore.getAll(), ...sanitizedArgs };
// 3. Apply exclusive pair pruning
for (const pair of exclusivePairs) {
const userProvidedConcrete = pair.some(k =>
Object.prototype.hasOwnProperty.call(sanitizedArgs, k)
);
if (userProvidedConcrete) {
// Drop conflicting session defaults
for (const k of pair) {
if (!Object.prototype.hasOwnProperty.call(sanitizedArgs, k)) {
delete merged[k];
}
}
}
}
// 4. Validate requirements
for (const req of requirements) {
if ('allOf' in req) {
const missing = req.allOf.filter(k => merged[k] == null);
if (missing.length > 0) {
return createErrorResponse('Missing required session defaults', ...);
}
}
if ('oneOf' in req) {
const satisfied = req.oneOf.some(k => merged[k] != null);
if (!satisfied) {
return createErrorResponse('Missing required session defaults', ...);
}
}
}
// 5. Execute with merged params
const validated = internalSchema.parse(merged);
return await logicFunction(validated, getExecutor());
};
}Reference: src/utils/typed-tool-factory.ts:74-174
Example from build_sim:
// From: src/mcp/tools/simulator/build_sim.ts:172-186
handler: createSessionAwareTool<BuildSimulatorParams>({
internalSchema: buildSimulatorSchema,
logicFunction: build_simLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [
{ allOf: ['scheme'], message: 'scheme is required' },
{ oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
{ oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
],
exclusivePairs: [
['projectPath', 'workspacePath'],
['simulatorId', 'simulatorName'],
],
}),Validation Behavior:
allOf: ['scheme']- MUST be provided (either in call or session defaults)oneOf: ['projectPath', 'workspacePath']- Exactly ONE must be setexclusivePairs- If user provides one, conflicting session default is dropped
Reference: src/mcp/tools/simulator/build_sim.ts:172-186
The platform parameter controls which Apple platform simulator to target:
// From: src/mcp/tools/simulator/build_sim.ts:21-25
platform: z
.enum(['iOS Simulator', 'watchOS Simulator', 'tvOS Simulator', 'visionOS Simulator'])
.optional()
.default('iOS Simulator')
.describe('Target simulator platform (defaults to iOS Simulator)')Key Insight: platform is NOT session-managed - it must be specified per-call if different from default:
// From: src/mcp/tools/simulator/build_sim.ts:157-166
// Public schema = internal minus session-managed fields
const publicSchemaObject = baseSchemaObject.omit({
projectPath: true,
workspacePath: true,
scheme: true,
configuration: true,
simulatorId: true,
simulatorName: true,
useLatestOS: true,
// platform is NOT omitted - it's available for clients to specify
} as const);Reference: src/mcp/tools/simulator/build_sim.ts:21-25, 157-166
// From: src/mcp/tools/simulator/build_sim.ts:94-102
const platformMap: Record<string, XcodePlatform> = {
'iOS Simulator': XcodePlatform.iOSSimulator,
'watchOS Simulator': XcodePlatform.watchOSSimulator,
'tvOS Simulator': XcodePlatform.tvOSSimulator,
'visionOS Simulator': XcodePlatform.visionOSSimulator,
};
const platform = platformMap[params.platform ?? 'iOS Simulator'] ?? XcodePlatform.iOSSimulator;Reference: src/mcp/tools/simulator/build_sim.ts:94-102
Device Tools:
build_device- Physical device buildtest_device- Physical device testingstart_device_log_cap- Device log capture- Required:
deviceId(UDID fromlist_devices)
Simulator Tools:
build_sim- Simulator buildtest_sim- Simulator testingstart_sim_log_cap- Simulator log capture- Required:
simulatorIdORsimulatorName
Simulator Identification:
// From: src/mcp/tools/simulator/build_sim.ts:26-37
simulatorId: z
.string()
.optional()
.describe('UUID of the simulator (from list_sims). Provide EITHER this OR simulatorName, not both'),
simulatorName: z
.string()
.optional()
.describe("Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both"),Reference: src/mcp/tools/simulator/build_sim.ts:26-37
test_sim Platform Validation:
// From: src/mcp/tools/simulator/test_sim.ts:81-83
.refine((val) => val.platform !== 'macOS', {
message: 'macOS platform is not supported by test_sim. Use test_macos tool instead for macOS projects.',
})Separate macOS Tools:
build_macos- macOS native buildstest_macos- macOS native testinglaunch_mac_app- Launch macOS appsstop_mac_app- Stop macOS apps
Reference: src/mcp/tools/simulator/test_sim.ts:81-83
Session Management: Log sessions tracked in global Map:
// From: src/mcp/tools/logging/start_device_log_cap.ts:31
export const activeDeviceLogSessions = new Map();Start Capture Process:
// From: src/mcp/tools/logging/start_device_log_cap.ts:38-105
export async function startDeviceLogCapture(
params: { deviceUuid: string; bundleId: string },
executor: CommandExecutor,
fileSystemExecutor?: FileSystemExecutor,
): Promise<{ sessionId: string; error?: string }> {
// 1. Clean old logs (older than LOG_RETENTION_DAYS)
await cleanOldDeviceLogs();
// 2. Generate session ID and log file path
const logSessionId = uuidv4();
const logFileName = `${DEVICE_LOG_FILE_PREFIX}${logSessionId}.log`;
const logFilePath = path.join(os.tmpdir(), logFileName);
// 3. Create log file
await fileSystemExecutor.writeFile(logFilePath, '');
const logStream = fs.createWriteStream(logFilePath, { flags: 'a' });
// 4. Launch app with console output
const result = await executor(
[
'xcrun', 'devicectl', 'device', 'process', 'launch',
'--console',
'--terminate-existing',
'--device', deviceUuid,
bundleId,
],
'Device Log Capture',
true,
undefined,
);
// 5. Store session
activeDeviceLogSessions.set(logSessionId, {
process: result.process,
logFilePath,
deviceUuid,
bundleId,
});
return { sessionId: logSessionId };
}Reference: src/mcp/tools/logging/start_device_log_cap.ts:38-105
Stop Capture Process:
// From: src/mcp/tools/logging/stop_device_log_cap.ts:54-133
export async function stop_device_log_capLogic(
params: StopDeviceLogCapParams,
fileSystemExecutor: FileSystemExecutor,
): Promise<ToolResponse> {
const { logSessionId } = params;
// 1. Retrieve session
const sessionData = activeDeviceLogSessions.get(logSessionId);
if (!sessionData) {
return createErrorResponse('Device log capture session not found: ' + logSessionId);
}
// 2. Validate session structure
if (!isValidDeviceLogSession(sessionData)) {
return createErrorResponse('Invalid session structure');
}
// 3. Kill process
if (!session.process.killed && session.process.exitCode === null) {
session.process.kill?.('SIGTERM');
}
// 4. Remove from active sessions
activeDeviceLogSessions.delete(logSessionId);
// 5. Read log file
const fileContent = await fileSystemExecutor.readFile(logFilePath, 'utf-8');
// 6. Return captured logs
return {
content: [{
type: 'text',
text: `✅ Device log capture session stopped successfully
Session ID: ${logSessionId}
--- Captured Logs ---
${fileContent}`,
}],
};
}Reference: src/mcp/tools/logging/stop_device_log_cap.ts:54-133
Common Failure Pattern:
- Agent calls
start_device_log_cap({ deviceId: "...", bundleId: "..." }) - Receives session ID in response
- FAILS: Immediately calls
stop_device_log_capwithout waiting for log data - Result: Empty or minimal log output
Root Cause Analysis:
// From: src/mcp/tools/logging/start_device_log_cap.ts:72-87
const result = await executor(
[
'xcrun', 'devicectl', 'device', 'process', 'launch',
'--console', // <-- Console output is captured continuously
'--terminate-existing',
'--device', deviceUuid,
bundleId,
],
'Device Log Capture',
true, // <-- useShell flag
undefined,
);The Process:
xcrun devicectllaunches app and streams console output continuously- Output is written to log file via
createWriteStream - Process keeps running until app is terminated or session is stopped
- Stopping immediately = capturing only launch logs, not runtime behavior
Correct Usage Pattern:
// 1. Start capture
const startResult = await start_device_log_cap({
deviceId: "00008110-001A2C3D4E5F",
bundleId: "com.example.MyApp"
});
// Response: "Session ID: 550e8400-e29b-41d4-a716-446655440000"
// 2. WAIT for user interaction or test execution
// ... app runs, generates logs, user interacts ...
// Minimum: 5-10 seconds for meaningful log capture
// 3. Stop capture
const stopResult = await stop_device_log_cap({
logSessionId: "550e8400-e29b-41d4-a716-446655440000"
});
// Now returns substantial captured log dataReference: src/mcp/tools/logging/start_device_log_cap.ts:70-97
Simulator vs Device Differences:
// Note from: src/mcp/tools/logging/start_device_log_cap.ts:26-30
// Note: Device and simulator logging use different approaches due to platform constraints:
// - Simulators use 'xcrun simctl' with console-pty and OSLog stream capabilities
// - Devices use 'xcrun devicectl' with console output only (no OSLog streaming)
// The different command structures and output formats make sharing infrastructure complex.Simulator Advantages:
xcrun simctlsupports both console output AND structured OSLog streaming- Can capture system logs alongside app logs
- More granular control over log filtering
Device Limitations:
xcrun devicectlonly captures console output- No access to system-level OSLog streams
- Must rely on app's explicit logging
Reference: src/mcp/tools/logging/start_device_log_cap.ts:26-30
Six tools for complete Swift Package Manager workflow:
// Workflow group: src/mcp/tools/swift-package/
- swift_package_build // Build packages
- swift_package_test // Run tests
- swift_package_run // Execute targets
- swift_package_clean // Clean build artifacts
- swift_package_list // List running processes
- swift_package_stop // Stop running executablesReference: docs/TOOLS.md:78-86
// From: src/mcp/tools/swift-package/swift_package_build.ts:11-74
export async function swift_package_buildLogic(
params: SwiftPackageBuildParams,
executor: CommandExecutor,
): Promise<ToolResponse> {
const resolvedPath = path.resolve(params.packagePath);
const swiftArgs = ['build', '--package-path', resolvedPath];
if (params.configuration && params.configuration.toLowerCase() === 'release') {
swiftArgs.push('-c', 'release');
}
if (params.targetName) {
swiftArgs.push('--target', params.targetName);
}
if (params.architectures) {
for (const arch of params.architectures) {
swiftArgs.push('--arch', arch);
}
}
if (params.parseAsLibrary) {
swiftArgs.push('-Xswiftc', '-parse-as-library');
}
const result = await executor(['swift', ...swiftArgs], 'Swift Package Build', true);
if (!result.success) {
return createErrorResponse('Swift package build failed', result.error ?? result.output);
}
return {
content: [
{ type: 'text', text: '✅ Swift package build succeeded.' },
{ type: 'text', text: '💡 Next: Run tests with swift_package_test or execute with swift_package_run' },
{ type: 'text', text: result.output },
],
};
}Parameters:
packagePath(required) - Path to Package.swift directorytargetName(optional) - Specific target to buildconfiguration(optional) - 'debug' or 'release'architectures(optional) - Array of architectures (e.g., ['arm64', 'x86_64'])parseAsLibrary(optional) - Add-parse-as-libraryflag for @main support
Reference: src/mcp/tools/swift-package/swift_package_build.ts:25-74
// From: src/mcp/tools/swift-package/swift_package_run.ts:37-220
export async function swift_package_runLogic(
params: SwiftPackageRunParams,
executor: CommandExecutor,
): Promise<ToolResponse> {
const resolvedPath = path.resolve(params.packagePath);
const timeout = Math.min(params.timeout ?? 30, 300) * 1000; // Max 5 minutes
const swiftArgs = ['run', '--package-path', resolvedPath];
if (params.configuration && params.configuration.toLowerCase() === 'release') {
swiftArgs.push('-c', 'release');
}
if (params.parseAsLibrary) {
swiftArgs.push('-Xswiftc', '-parse-as-library');
}
if (params.executableName) {
swiftArgs.push(params.executableName);
}
// Add double dash before executable arguments
if (params.arguments && params.arguments.length > 0) {
swiftArgs.push('--');
swiftArgs.push(...params.arguments);
}
if (params.background) {
// Background mode: start process and return immediately
const command = ['swift', ...swiftArgs];
const result = await executor(command, 'Swift Package Run (Background)', true, cleanEnv, true);
if (result.process?.pid) {
addProcess(result.process.pid, {
process: result.process,
startedAt: new Date(),
});
return {
content: [
createTextContent(
`🚀 Started executable in background (PID: ${result.process.pid})
💡 Process is running independently. Use swift_package_stop with PID ${result.process.pid} to terminate when needed.`
),
],
};
}
} else {
// Foreground mode: wait for completion or timeout
const commandPromise = executor(command, 'Swift Package Run', true);
const timeoutPromise = new Promise((resolve) => {
setTimeout(() => resolve({ success: false, timedOut: true, ... }), timeout);
});
const result = await Promise.race([commandPromise, timeoutPromise]);
if ('timedOut' in result && result.timedOut) {
return {
content: [
createTextContent(`⏱️ Process timed out after ${timeout / 1000} seconds.`),
createTextContent(result.output || '(no output so far)'),
],
};
}
return {
content: [
createTextContent('✅ Swift executable completed successfully.'),
createTextContent(result.output || '(no output)'),
],
};
}
}Parameters:
packagePath(required) - Path to Package.swift directoryexecutableName(optional) - Name of executable target (defaults to package name)arguments(optional) - Array of arguments for executableconfiguration(optional) - 'debug' or 'release'timeout(optional) - Timeout in seconds (default: 30, max: 300)background(optional) - Run in background (default: false)parseAsLibrary(optional) - Add-parse-as-libraryflag
Background vs Foreground:
- Foreground: Waits for completion, returns output, subject to timeout
- Background: Returns immediately with PID, runs independently
Reference: src/mcp/tools/swift-package/swift_package_run.ts:37-220
| Aspect | Swift Package Tools | Xcode Project Tools |
|---|---|---|
| Project File | Package.swift | .xcodeproj / .xcworkspace |
| Build Command | swift build |
xcodebuild |
| Scheme | Not required | Required |
| Platform | Host platform only | Multiple (iOS, macOS, watchOS, etc.) |
| Session Defaults | NOT session-aware | Session-aware (can use defaults) |
| Target Specification | Optional target name | Required scheme |
Key Insight: Swift Package tools do NOT use session defaults - all parameters must be explicit.
Reference: src/mcp/tools/swift-package/swift_package_build.ts:76-85 (no session awareness)
Pattern 1: Build → Run (Foreground)
// 1. Build package
await swift_package_build({
packagePath: "/path/to/MyPackage",
configuration: "debug"
});
// 2. Run executable (wait for completion)
await swift_package_run({
packagePath: "/path/to/MyPackage",
executableName: "my-tool",
arguments: ["--verbose"],
timeout: 60
});Pattern 2: Build → Run (Background) → Stop
// 1. Build package
await swift_package_build({
packagePath: "/path/to/MyPackage"
});
// 2. Run executable in background
const runResult = await swift_package_run({
packagePath: "/path/to/MyPackage",
executableName: "my-server",
background: true
});
// Response: "Started executable in background (PID: 12345)"
// 3. Later: stop the process
await swift_package_stop({ pid: 12345 });Pattern 3: Build → Test
// 1. Build package
await swift_package_build({
packagePath: "/path/to/MyPackage",
configuration: "debug"
});
// 2. Run tests
await swift_package_test({
packagePath: "/path/to/MyPackage",
filter: "MyTests.testExample"
});Zero Vitest Mocking Policy:
From: docs/TESTING.md
❌ ALL VITEST MOCKING IS COMPLETELY BANNED
- No vi.mock()
- No vi.fn()
- No vi.spyOn()
Dependency Injection Pattern:
// Test pattern: src/mcp/tools/simulator/__tests__/build_sim.test.ts:77-99
it('should handle empty workspacePath parameter', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'BUILD SUCCEEDED'
});
// Test the LOGIC function directly with injected mock
const result = await build_simLogic(
{
workspacePath: '',
scheme: 'MyScheme',
simulatorName: 'iPhone 16',
},
mockExecutor, // <-- Injected dependency
);
expect(result.content).toEqual([
{ type: 'text', text: '✅ iOS Simulator Build build succeeded...' },
{ type: 'text', text: expect.stringContaining('Next Steps:') },
]);
});Reference: src/mcp/tools/simulator/__tests__/build_sim.test.ts:77-99
Total Test Files: 78
Distribution:
- Simulator tools: ~25 test files
- Device tools: ~10 test files
- macOS tools: ~8 test files
- Swift Package tools: ~6 test files
- Utilities: ~10 test files
- Session management: ~3 test files
- Other: ~16 test files
Every tool test covers:
- Input Validation - Parameter schema validation and error cases
- Command Generation - Verify correct CLI commands are built
- Output Processing - Test response formatting and error handling
// Example: src/mcp/tools/simulator/__tests__/build_sim.test.ts:189-290
describe('Command Generation', () => {
it('should generate correct build command with minimal parameters', async () => {
const callHistory: Array<{ command: string[]; logPrefix?: string }> = [];
const trackingExecutor = async (command: string[], logPrefix?: string) => {
callHistory.push({ command, logPrefix });
return { success: false, output: '', error: 'Test stop' };
};
await build_simLogic(
{
workspacePath: '/path/to/MyProject.xcworkspace',
scheme: 'MyScheme',
simulatorName: 'iPhone 16',
},
trackingExecutor,
);
// Verify exact command structure
expect(callHistory[0].command).toEqual([
'xcodebuild',
'-workspace', '/path/to/MyProject.xcworkspace',
'-scheme', 'MyScheme',
'-configuration', 'Debug',
'-skipMacroValidation',
'-destination', 'platform=iOS Simulator,name=iPhone 16,OS=latest',
'build',
]);
});
});Reference: src/mcp/tools/simulator/__tests__/build_sim.test.ts:189-290
Symptom:
Error: Missing required session defaults
Provide a project or workspace
Set with: session-set-defaults { "projectPath": "..." } OR session-set-defaults { "workspacePath": "..." }
Root Cause:
Tools require projectPath or workspacePath, but agent calls without setting session defaults first.
Solution:
// ALWAYS start sessions with defaults
await session_set_defaults({
workspacePath: "/path/to/MyApp.xcworkspace",
scheme: "MyScheme",
simulatorName: "iPhone 16"
});
// Then tools can use defaults
await build_sim({}); // Uses session defaultsReference: src/utils/typed-tool-factory.ts:130-156
Symptom:
Error: Parameter validation failed
Mutually exclusive parameters provided: projectPath, workspacePath. Provide only one.
Root Cause:
Agent provides both projectPath AND workspacePath in same call.
Solution:
// ❌ WRONG
await build_sim({
projectPath: "/path/to/App.xcodeproj",
workspacePath: "/path/to/App.xcworkspace", // Conflict!
scheme: "MyScheme",
simulatorName: "iPhone 16"
});
// ✅ CORRECT
await build_sim({
workspacePath: "/path/to/App.xcworkspace", // Only one
scheme: "MyScheme",
simulatorName: "iPhone 16"
});Reference: src/utils/typed-tool-factory.ts:99-109
Symptom: Log capture returns minimal or empty output.
Root Cause:
Agent calls stop_device_log_cap immediately after start_device_log_cap without waiting for log generation.
Solution:
// ❌ WRONG
const startResult = await start_device_log_cap({ deviceId: "...", bundleId: "..." });
const stopResult = await stop_device_log_cap({ logSessionId: startResult.sessionId });
// Returns empty logs!
// ✅ CORRECT
const startResult = await start_device_log_cap({ deviceId: "...", bundleId: "..." });
// Tell user: "Log capture started. Interact with your app for 10 seconds."
// WAIT 10+ seconds for meaningful interaction
await delay(10000); // Or wait for user signal
const stopResult = await stop_device_log_cap({ logSessionId: startResult.sessionId });
// Now returns substantial log dataReference: src/mcp/tools/logging/start_device_log_cap.ts:70-97
Symptom:
Error: macOS platform is not supported by test_sim. Use test_macos tool instead for macOS projects.
Root Cause:
Agent uses test_sim with platform: "macOS" instead of using test_macos.
Solution:
// ❌ WRONG
await test_sim({
workspacePath: "/path/to/MacApp.xcworkspace",
scheme: "MacApp",
platform: "macOS" // NOT SUPPORTED
});
// ✅ CORRECT
await test_macos({
workspacePath: "/path/to/MacApp.xcworkspace",
scheme: "MacApp"
});Reference: src/mcp/tools/simulator/test_sim.ts:81-83
Symptom:
Error: Required parameter 'packagePath' not provided
Root Cause: Agent expects Swift Package tools to use session defaults (they don't).
Solution:
// ❌ WRONG - Session defaults don't work for Swift Package tools
await session_set_defaults({ workspacePath: "/path/to/MyPackage" });
await swift_package_build({}); // FAILS - packagePath required
// ✅ CORRECT - Always explicit parameters
await swift_package_build({
packagePath: "/path/to/MyPackage",
configuration: "debug"
});Reference: src/mcp/tools/swift-package/swift_package_build.ts:25-74
Common Confusion:
| User Intent | WRONG Tool | CORRECT Tool |
|---|---|---|
| Test on iPhone simulator | test_macos |
test_sim({ platform: "iOS Simulator" }) |
| Test on physical iPhone | test_sim |
test_device |
| Test on macOS | test_sim({ platform: "macOS" }) |
test_macos |
| Build for Apple Watch simulator | build_device |
build_sim({ platform: "watchOS Simulator" }) |
| Build for physical Apple Watch | build_sim |
build_device |
Reference: docs/TOOLS.md:11-52
-
Session Defaults are Essential: Most Xcode tools require session defaults. Set them FIRST.
-
Mutual Exclusivity is Enforced: Cannot provide both
projectPathandworkspacePath, or bothsimulatorIdandsimulatorName. -
Platform ≠ Tool Selection: Use tool name to choose target (sim vs device vs macOS), then use
platformparameter for specific simulator type. -
Log Capture Requires Patience: Start → WAIT → Stop. Immediate stopping = empty logs.
-
Swift Package Tools are Different: No session defaults, explicit parameters required.
-
Test Coverage Proves Behavior: 78 test files with dependency injection prove every tool works as documented.
-
Tools Follow Consistent Pattern: All tools export
*Logicfunction for testing, use Zod validation, and returnToolResponse.
Document Version: 1.0 Total Code References: 45 file:line citations Test Files Analyzed: 78 Tools Documented: 83 (61 canonical + 22 re-exports)