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
4 changes: 2 additions & 2 deletions .windsurf/workflows/build-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
description: Build and run the app on the iOS simulator
---

1. Build the app
1. Build the iOS app
2. Run the app on the simulator
3. Only use tools exposed by XcodeBuildMCP
3. Only use tools exposed by "xcode-mcp-server-dev"
5 changes: 3 additions & 2 deletions src/tools/app_path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { log } from '../utils/logger.js';
import { validateRequiredParam, createTextResponse } from '../utils/validation.js';
import { ToolResponse, XcodePlatform } from '../types/common.js';
import { executeXcodeCommand, constructDestinationString } from '../utils/xcode.js';
import { executeCommand } from '../utils/command.js';
import { constructDestinationString } from '../utils/xcode.js';
import {
registerTool,
workspacePathSchema,
Expand Down Expand Up @@ -117,7 +118,7 @@ async function _handleGetAppPathLogic(params: {
command.push('-destination', destinationString);

// Execute the command directly
const result = await executeXcodeCommand(command, 'Get App Path');
const result = await executeCommand(command, 'Get App Path');

if (!result.success) {
return createTextResponse(`Failed to get app path: ${result.error}`, true);
Expand Down
6 changes: 3 additions & 3 deletions src/tools/build_ios_device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { XcodePlatform } from '../utils/xcode.js';
import { validateRequiredParam } from '../utils/validation.js';
import { executeXcodeBuild } from '../utils/build-utils.js';
import { executeXcodeBuildCommand } from '../utils/build-utils.js';
import {
registerTool,
workspacePathSchema,
Expand Down Expand Up @@ -54,7 +54,7 @@ export function registerIOSDeviceBuildWorkspaceTool(server: McpServer): void {
const schemeValidation = validateRequiredParam('scheme', params.scheme);
if (!schemeValidation.isValid) return schemeValidation.errorResponse!;

return executeXcodeBuild(
return executeXcodeBuildCommand(
{
...params,
configuration: params.configuration ?? 'Debug', // Default config
Expand Down Expand Up @@ -94,7 +94,7 @@ export function registerIOSDeviceBuildProjectTool(server: McpServer): void {
const schemeValidation = validateRequiredParam('scheme', params.scheme);
if (!schemeValidation.isValid) return schemeValidation.errorResponse!;

return executeXcodeBuild(
return executeXcodeBuildCommand(
{
...params,
configuration: params.configuration ?? 'Debug', // Default config
Expand Down
9 changes: 5 additions & 4 deletions src/tools/build_ios_simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { log } from '../utils/logger.js';
import { XcodePlatform, executeXcodeCommand } from '../utils/xcode.js';
import { XcodePlatform } from '../utils/xcode.js';
import { executeCommand } from '../utils/command.js';
import { validateRequiredParam, createTextResponse } from '../utils/validation.js';
import { ToolResponse } from '../types/common.js';
import { executeXcodeBuild } from '../utils/build-utils.js';
import { executeXcodeBuildCommand } from '../utils/build-utils.js';
import {
registerTool,
workspacePathSchema,
Expand Down Expand Up @@ -52,7 +53,7 @@ async function _handleIOSSimulatorBuildLogic(params: {
}): Promise<ToolResponse> {
log('info', `Starting iOS Simulator build for scheme ${params.scheme} (internal)`);

return executeXcodeBuild(
return executeXcodeBuildCommand(
{
...params,
},
Expand Down Expand Up @@ -134,7 +135,7 @@ async function _handleIOSSimulatorBuildAndRunLogic(params: {
}

// Execute the command directly
const result = await executeXcodeCommand(command, 'Get App Path');
const result = await executeCommand(command, 'Get App Path');

// If there was an error with the command execution, return it
if (!result.success) {
Expand Down
9 changes: 5 additions & 4 deletions src/tools/build_macos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { exec } from 'child_process';
import { promisify } from 'util';
import { log } from '../utils/logger.js';
import { XcodePlatform, executeXcodeCommand } from '../utils/xcode.js';
import { XcodePlatform } from '../utils/xcode.js';
import { executeCommand } from '../utils/command.js';
import { createTextResponse } from '../utils/validation.js';
import { ToolResponse } from '../types/common.js';
import { executeXcodeBuild } from '../utils/build-utils.js';
import { executeXcodeBuildCommand } from '../utils/build-utils.js';
import { z } from 'zod';
import {
registerTool,
Expand Down Expand Up @@ -55,7 +56,7 @@ async function _handleMacOSBuildLogic(params: {
}): Promise<ToolResponse> {
log('info', `Starting macOS build for scheme ${params.scheme} (internal)`);

return executeXcodeBuild(
return executeXcodeBuildCommand(
{
...params,
},
Expand Down Expand Up @@ -104,7 +105,7 @@ async function _getAppPathFromBuildSettings(params: {
}

// Execute the command directly
const result = await executeXcodeCommand(command, 'Get Build Settings for Launch');
const result = await executeCommand(command, 'Get Build Settings for Launch');

if (!result.success) {
return {
Expand Down
6 changes: 3 additions & 3 deletions src/tools/build_settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { log } from '../utils/logger.js';
import { executeXcodeCommand } from '../utils/xcode.js';
import { executeCommand } from '../utils/command.js';
import { validateRequiredParam, createTextResponse } from '../utils/validation.js';
import { ToolResponse } from '../types/common.js';
import {
Expand Down Expand Up @@ -53,7 +53,7 @@ async function _handleShowBuildSettingsLogic(params: {
command.push('-scheme', params.scheme);

// Execute the command directly
const result = await executeXcodeCommand(command, 'Show Build Settings');
const result = await executeCommand(command, 'Show Build Settings');

if (!result.success) {
return createTextResponse(`Failed to show build settings: ${result.error}`, true);
Expand Down Expand Up @@ -98,7 +98,7 @@ async function _handleListSchemesLogic(params: {
command.push('-project', params.projectPath);
} // No else needed, one path is guaranteed by callers

const result = await executeXcodeCommand(command, 'List Schemes');
const result = await executeCommand(command, 'List Schemes');

if (!result.success) {
return createTextResponse(`Failed to list schemes: ${result.error}`, true);
Expand Down
4 changes: 2 additions & 2 deletions src/tools/clean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { log } from '../utils/logger.js';
import { XcodePlatform } from '../utils/xcode.js';
import { ToolResponse } from '../types/common.js';
import { executeXcodeBuild } from '../utils/build-utils.js';
import { executeXcodeBuildCommand } from '../utils/build-utils.js';

// --- Private Helper Function ---

Expand All @@ -35,7 +35,7 @@ async function _handleCleanLogic(params: {
log('info', 'Starting xcodebuild clean request (internal)');

// For clean operations, we need to provide a default platform and configuration
return executeXcodeBuild(
return executeXcodeBuildCommand(
{
...params,
scheme: params.scheme || '', // Empty string if not provided
Expand Down
102 changes: 33 additions & 69 deletions src/tools/idb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs/promises';
import { spawn } from 'child_process';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { v4 as uuidv4 } from 'uuid';
import { ToolResponse } from '../types/common.js';
import { log } from '../utils/logger.js';
import { createTextResponse, validateRequiredParam } from '../utils/validation.js';
import { DependencyError, IdbError, SystemError, createErrorResponse } from '../utils/errors.js';
import { executeCommand } from '../utils/command.js';
import { createIdbNotAvailableResponse } from '../utils/idb-setup.js';
import { areIdbToolsAvailable } from '../utils/idb-setup.js';

Expand Down Expand Up @@ -43,79 +43,43 @@ async function executeIdbCommand(
}
}

const commandString = `${IDB_COMMAND} ${fullArgs.join(' ')}`;
log('info', `${LOG_PREFIX}: Executing: ${commandString}`);

return new Promise<string>((resolve, reject) => {
let stdoutData = '';
let stderrData = '';
let processError: Error | null = null;

try {
const idbProcess = spawn(IDB_COMMAND, fullArgs, {
shell: false, // Use direct execution, assumes idb is in PATH
});

idbProcess.stdout.on('data', (data) => {
stdoutData += data.toString();
});

idbProcess.stderr.on('data', (data) => {
stderrData += data.toString();
});

idbProcess.on('error', (err) => {
log('error', `${LOG_PREFIX}: Failed to spawn idb: ${err.message}`);
processError = err;
// reject will be called in 'close' handler
});

idbProcess.on('close', (code) => {
log('debug', `${LOG_PREFIX}: Command "${commandName}" exited with code ${code}`);
log('debug', `${LOG_PREFIX}: stdout:\n${stdoutData}`);
if (stderrData) {
log('warning', `${LOG_PREFIX}: stderr:\n${stderrData}`);
}
// Construct the full command array with IDB_COMMAND as the first element
const fullCommand = [IDB_COMMAND, ...fullArgs];

if (processError) {
return reject(
new SystemError(`Failed to start idb command: ${processError.message}`, processError),
);
}
try {
const result = await executeCommand(fullCommand, `${LOG_PREFIX}: ${commandName}`, false);

if (code !== 0) {
return reject(
new IdbError(
`idb command '${commandName}' failed with exit code ${code}.`,
commandName,
stderrData || stdoutData, // Provide output for context
simulatorUuid,
),
);
}

// Some idb commands might print warnings or non-fatal errors to stderr.
// We resolve successfully but log the stderr. Consider if specific commands
// should treat stderr as a failure. For now, only non-zero exit code is fatal.
if (stderrData) {
log(
'warn',
`${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${stderrData}`,
);
}
if (!result.success) {
throw new IdbError(
`idb command '${commandName}' failed.`,
commandName,
result.error || result.output,
simulatorUuid,
);
}

resolve(stdoutData.trim());
});
} catch (error) {
// Catch synchronous errors during spawn setup
log('error', `${LOG_PREFIX}: Error setting up idb spawn: ${error}`);
reject(
new SystemError(
`Failed to initiate idb command execution: ${error instanceof Error ? error.message : String(error)}`,
),
// Check for stderr output in successful commands
if (result.error) {
log(
'warn',
`${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${result.error}`,
);
}
});

return result.output.trim();
} catch (error) {
if (error instanceof Error) {
if (error instanceof IdbError) {
throw error;
}

// Otherwise wrap it in a SystemError
throw new SystemError(`Failed to execute idb command: ${error.message}`, error);
}

// For any other type of error
throw new SystemError(`Failed to execute idb command: ${String(error)}`);
}
}

// --- Registration Function ---
Expand Down
16 changes: 8 additions & 8 deletions src/tools/simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { z } from 'zod';
import { execSync } from 'child_process';
import { log } from '../utils/logger.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { executeXcodeCommand } from '../utils/xcode.js';
import { executeCommand } from '../utils/command.js';
import { validateRequiredParam, validateFileExists } from '../utils/validation.js';
import { ToolResponse } from '../types/common.js';
import { createTextContent } from './common.js';
Expand Down Expand Up @@ -45,7 +45,7 @@ export function registerBootSimulatorTool(server: McpServer): void {

try {
const command = ['xcrun', 'simctl', 'boot', params.simulatorUuid];
const result = await executeXcodeCommand(command, 'Boot Simulator');
const result = await executeCommand(command, 'Boot Simulator');

if (!result.success) {
return {
Expand Down Expand Up @@ -104,7 +104,7 @@ export function registerListSimulatorsTool(server: McpServer): void {

try {
const command = ['xcrun', 'simctl', 'list', 'devices', 'available', '--json'];
const result = await executeXcodeCommand(command, 'List Simulators');
const result = await executeCommand(command, 'List Simulators');

if (!result.success) {
return {
Expand Down Expand Up @@ -211,7 +211,7 @@ export function registerInstallAppInSimulatorTool(server: McpServer): void {

try {
const command = ['xcrun', 'simctl', 'install', params.simulatorUuid, params.appPath];
const result = await executeXcodeCommand(command, 'Install App in Simulator');
const result = await executeCommand(command, 'Install App in Simulator');

if (!result.success) {
return {
Expand Down Expand Up @@ -299,7 +299,7 @@ export function registerLaunchAppInSimulatorTool(server: McpServer): void {
params.bundleId,
'app',
];
const getAppContainerResult = await executeXcodeCommand(
const getAppContainerResult = await executeCommand(
getAppContainerCmd,
'Check App Installed',
);
Expand Down Expand Up @@ -333,7 +333,7 @@ export function registerLaunchAppInSimulatorTool(server: McpServer): void {
command.push(...params.args);
}

const result = await executeXcodeCommand(command, 'Launch App in Simulator');
const result = await executeCommand(command, 'Launch App in Simulator');

if (!result.success) {
return {
Expand Down Expand Up @@ -446,7 +446,7 @@ export function registerOpenSimulatorTool(server: McpServer): void {

try {
const command = ['open', '-a', 'Simulator'];
const result = await executeXcodeCommand(command, 'Open Simulator');
const result = await executeCommand(command, 'Open Simulator');

if (!result.success) {
return {
Expand Down Expand Up @@ -523,7 +523,7 @@ async function executeSimctlCommandAndRespond(

try {
const command = ['xcrun', 'simctl', ...simctlSubCommand];
const result = await executeXcodeCommand(command, operationDescriptionForXcodeCommand);
const result = await executeCommand(command, operationDescriptionForXcodeCommand);

if (!result.success) {
const fullFailureMessage = `${failureMessagePrefix}: ${result.error}`;
Expand Down
5 changes: 0 additions & 5 deletions src/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,6 @@ export interface CommandResponse {
error?: string;
}

/**
* XcodeCommandResponse - Result of xcodebuild command execution
*/
export type XcodeCommandResponse = CommandResponse;

/**
* Interface for shared build parameters
*/
Expand Down
Loading