xcodebuildmcp
Version:
XcodeBuildMCP is a ModelContextProtocol server that provides tools for Xcode project management, simulator management, and app utilities.
1,452 lines (1,433 loc) • 425 kB
JavaScript
#!/usr/bin/env node
import { createRequire } from 'module';
import { spawn, execSync, exec } from 'child_process';
import * as fs6 from 'fs';
import { promises, existsSync, readdirSync } from 'fs';
import * as os4 from 'os';
import { tmpdir } from 'os';
import { z } from 'zod';
import * as path3 from 'path';
import path3__default, { dirname, join, basename } from 'path';
import * as fs2 from 'fs/promises';
import { mkdtemp, rm } from 'fs/promises';
import { v4 } from 'uuid';
import { randomUUID } from 'crypto';
import { fileURLToPath } from 'url';
import { promisify } from 'util';
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/version.ts
var version, iOSTemplateVersion, macOSTemplateVersion;
var init_version = __esm({
"src/version.ts"() {
version = "1.14.1";
iOSTemplateVersion = "v1.0.8";
macOSTemplateVersion = "v1.0.5";
}
});
function isTestEnv() {
return process.env.VITEST === "true" || process.env.NODE_ENV === "test" || process.env.XCODEBUILDMCP_SILENCE_LOGS === "true";
}
function loadSentrySync() {
if (!SENTRY_ENABLED || isTestEnv()) return null;
if (cachedSentry) return cachedSentry;
try {
cachedSentry = require2("@sentry/node");
return cachedSentry;
} catch {
return null;
}
}
function withSentry(cb) {
const s = loadSentrySync();
if (!s) return;
try {
cb(s);
} catch {
}
}
function shouldLog(level) {
if (isTestEnv()) {
return false;
}
if (clientLogLevel === null) {
return true;
}
const levelKey = level.toLowerCase();
if (!(levelKey in LOG_LEVELS)) {
return true;
}
return LOG_LEVELS[levelKey] <= LOG_LEVELS[clientLogLevel];
}
function log(level, message, context) {
if (!shouldLog(level)) {
return;
}
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
const logMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
const captureToSentry = SENTRY_ENABLED && (context?.sentry ?? level === "error");
if (captureToSentry) {
withSentry((s) => s.captureMessage(logMessage));
}
console.error(logMessage);
}
var SENTRY_ENABLED, LOG_LEVELS, clientLogLevel, require2, cachedSentry;
var init_logger = __esm({
"src/utils/logger.ts"() {
SENTRY_ENABLED = process.env.SENTRY_DISABLED !== "true" && process.env.XCODEBUILDMCP_SENTRY_DISABLED !== "true";
LOG_LEVELS = {
emergency: 0,
alert: 1,
critical: 2,
error: 3,
warning: 4,
notice: 5,
info: 6,
debug: 7
};
clientLogLevel = null;
require2 = createRequire(import.meta.url);
cachedSentry = null;
if (!SENTRY_ENABLED) {
if (process.env.SENTRY_DISABLED === "true") {
log("info", "Sentry disabled due to SENTRY_DISABLED environment variable");
} else if (process.env.XCODEBUILDMCP_SENTRY_DISABLED === "true") {
log("info", "Sentry disabled due to XCODEBUILDMCP_SENTRY_DISABLED environment variable");
}
}
}
});
// src/utils/logging/index.ts
var init_logging = __esm({
"src/utils/logging/index.ts"() {
init_logger();
}
});
async function defaultExecutor(command, logPrefix, useShell = true, opts, detached = false) {
let escapedCommand = command;
if (useShell) {
const commandString = command.map((arg) => {
if (/[\s,"'=$`;&|<>(){}[\]\\*?~]/.test(arg) && !/^".*"$/.test(arg)) {
return `"${arg.replace(/(["\\])/g, "\\$1")}"`;
}
return arg;
}).join(" ");
escapedCommand = ["sh", "-c", commandString];
}
const displayCommand = useShell && escapedCommand.length === 3 ? escapedCommand[2] : escapedCommand.join(" ");
log("info", `Executing ${logPrefix ?? ""} command: ${displayCommand}`);
return new Promise((resolve2, reject) => {
const executable = escapedCommand[0];
const args = escapedCommand.slice(1);
const spawnOpts = {
stdio: ["ignore", "pipe", "pipe"],
// ignore stdin, pipe stdout/stderr
env: { ...process.env, ...opts?.env ?? {} },
cwd: opts?.cwd
};
const childProcess = spawn(executable, args, spawnOpts);
let stdout = "";
let stderr = "";
childProcess.stdout?.on("data", (data) => {
stdout += data.toString();
});
childProcess.stderr?.on("data", (data) => {
stderr += data.toString();
});
if (detached) {
let resolved = false;
childProcess.on("error", (err) => {
if (!resolved) {
resolved = true;
reject(err);
}
});
setTimeout(() => {
if (!resolved) {
resolved = true;
if (childProcess.pid) {
resolve2({
success: true,
output: "",
// No output for detached processes
process: childProcess
});
} else {
resolve2({
success: false,
output: "",
error: "Failed to start detached process",
process: childProcess
});
}
}
}, 100);
} else {
childProcess.on("close", (code) => {
const success = code === 0;
const response = {
success,
output: stdout,
error: success ? void 0 : stderr,
process: childProcess,
exitCode: code ?? void 0
};
resolve2(response);
});
childProcess.on("error", (err) => {
reject(err);
});
}
});
}
function getDefaultCommandExecutor() {
if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
throw new Error(
`\u{1F6A8} REAL SYSTEM EXECUTOR DETECTED IN TEST! \u{1F6A8}
This test is trying to use the default command executor instead of a mock.
Fix: Pass createMockExecutor() as the commandExecutor parameter in your test.
Example: await plugin.handler(args, createMockExecutor({success: true}), mockFileSystem)
See docs/TESTING.md for proper testing patterns.`
);
}
return defaultExecutor;
}
function getDefaultFileSystemExecutor() {
if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
throw new Error(
`\u{1F6A8} REAL FILESYSTEM EXECUTOR DETECTED IN TEST! \u{1F6A8}
This test is trying to use the default filesystem executor instead of a mock.
Fix: Pass createMockFileSystemExecutor() as the fileSystemExecutor parameter in your test.
Example: await plugin.handler(args, mockCmd, createMockFileSystemExecutor())
See docs/TESTING.md for proper testing patterns.`
);
}
return defaultFileSystemExecutor;
}
var defaultFileSystemExecutor;
var init_command = __esm({
"src/utils/command.ts"() {
init_logger();
defaultFileSystemExecutor = {
async mkdir(path11, options) {
const fs8 = await import('fs/promises');
await fs8.mkdir(path11, options);
},
async readFile(path11, encoding = "utf8") {
const fs8 = await import('fs/promises');
const content = await fs8.readFile(path11, encoding);
return content;
},
async writeFile(path11, content, encoding = "utf8") {
const fs8 = await import('fs/promises');
await fs8.writeFile(path11, content, encoding);
},
async cp(source, destination, options) {
const fs8 = await import('fs/promises');
await fs8.cp(source, destination, options);
},
async readdir(path11, options) {
const fs8 = await import('fs/promises');
return await fs8.readdir(path11, options);
},
async rm(path11, options) {
const fs8 = await import('fs/promises');
await fs8.rm(path11, options);
},
existsSync(path11) {
return existsSync(path11);
},
async stat(path11) {
const fs8 = await import('fs/promises');
return await fs8.stat(path11);
},
async mkdtemp(prefix) {
const fs8 = await import('fs/promises');
return await fs8.mkdtemp(prefix);
},
tmpdir() {
return tmpdir();
}
};
}
});
// src/utils/execution/index.ts
var init_execution = __esm({
"src/utils/execution/index.ts"() {
init_command();
}
});
// src/utils/version/index.ts
var init_version2 = __esm({
"src/utils/version/index.ts"() {
init_version();
}
});
// src/types/common.ts
function createTextContent(text) {
return { type: "text", text };
}
function createImageContent(data, mimeType) {
return { type: "image", data, mimeType };
}
var init_common = __esm({
"src/types/common.ts"() {
}
});
function getDefaultEnvironmentDetector() {
return defaultEnvironmentDetector;
}
function normalizeTestRunnerEnv(vars) {
const normalized = {};
for (const [key, value] of Object.entries(vars ?? {})) {
if (value == null) continue;
const prefixedKey = key.startsWith("TEST_RUNNER_") ? key : `TEST_RUNNER_${key}`;
normalized[prefixedKey] = value;
}
return normalized;
}
var ProductionEnvironmentDetector, defaultEnvironmentDetector;
var init_environment = __esm({
"src/utils/environment.ts"() {
init_logger();
ProductionEnvironmentDetector = class {
isRunningUnderClaudeCode() {
if (process.env.NODE_ENV === "test" || process.env.VITEST === "true") {
return false;
}
if (process.env.CLAUDECODE === "1" || process.env.CLAUDE_CODE_ENTRYPOINT === "cli") {
return true;
}
try {
const parentPid = process.ppid;
if (parentPid) {
const parentCommand = execSync(`ps -o command= -p ${parentPid}`, {
encoding: "utf8",
timeout: 1e3
}).trim();
if (parentCommand.includes("claude")) {
return true;
}
}
} catch (error) {
log("debug", `Failed to detect parent process: ${error}`);
}
return false;
}
};
defaultEnvironmentDetector = new ProductionEnvironmentDetector();
}
});
function createTextResponse(message, isError = false) {
return {
content: [
{
type: "text",
text: message
}
],
isError
};
}
function validateFileExists(filePath, fileSystem) {
const exists = fileSystem ? fileSystem.existsSync(filePath) : fs6.existsSync(filePath);
if (!exists) {
return {
isValid: false,
errorResponse: createTextResponse(
`File not found: '${filePath}'. Please check the path and try again.`,
true
)
};
}
return { isValid: true };
}
function consolidateContentForClaudeCode(response) {
const shouldConsolidate = getDefaultEnvironmentDetector().isRunningUnderClaudeCode();
if (!shouldConsolidate || !response.content || response.content.length <= 1) {
return response;
}
const textParts = [];
response.content.forEach((item, index) => {
if (item.type === "text") {
if (index > 0 && textParts.length > 0) {
textParts.push("\n---\n");
}
textParts.push(item.text);
}
});
if (textParts.length === 0) {
return response;
}
const consolidatedText = textParts.join("");
return {
...response,
content: [
{
type: "text",
text: consolidatedText
}
]
};
}
var init_validation = __esm({
"src/utils/validation.ts"() {
init_logger();
init_common();
init_environment();
}
});
// src/utils/errors.ts
function createErrorResponse(message, details) {
const detailText = details ? `
Details: ${details}` : "";
return {
content: [
{
type: "text",
text: `Error: ${message}${detailText}`
}
],
isError: true
};
}
var XcodeBuildMCPError, ValidationError, SystemError, ConfigurationError, AxeError, DependencyError;
var init_errors = __esm({
"src/utils/errors.ts"() {
XcodeBuildMCPError = class _XcodeBuildMCPError extends Error {
constructor(message) {
super(message);
this.name = "XcodeBuildMCPError";
Object.setPrototypeOf(this, _XcodeBuildMCPError.prototype);
}
};
ValidationError = class _ValidationError extends XcodeBuildMCPError {
constructor(message, paramName) {
super(message);
this.paramName = paramName;
this.name = "ValidationError";
Object.setPrototypeOf(this, _ValidationError.prototype);
}
};
SystemError = class _SystemError extends XcodeBuildMCPError {
constructor(message, originalError) {
super(message);
this.originalError = originalError;
this.name = "SystemError";
Object.setPrototypeOf(this, _SystemError.prototype);
}
};
ConfigurationError = class _ConfigurationError extends XcodeBuildMCPError {
constructor(message) {
super(message);
this.name = "ConfigurationError";
Object.setPrototypeOf(this, _ConfigurationError.prototype);
}
};
AxeError = class _AxeError extends XcodeBuildMCPError {
constructor(message, command, axeOutput, simulatorId) {
super(message);
this.command = command;
this.axeOutput = axeOutput;
this.simulatorId = simulatorId;
this.name = "AxeError";
Object.setPrototypeOf(this, _AxeError.prototype);
}
};
DependencyError = class _DependencyError extends ConfigurationError {
constructor(message, details) {
super(message);
this.details = details;
this.name = "DependencyError";
Object.setPrototypeOf(this, _DependencyError.prototype);
}
};
}
});
// src/utils/responses/index.ts
var init_responses = __esm({
"src/utils/responses/index.ts"() {
init_validation();
init_errors();
}
});
function createTypedTool(schema, logicFunction, getExecutor) {
return async (args) => {
try {
const validatedParams = schema.parse(args);
return await logicFunction(validatedParams, getExecutor());
} catch (error) {
if (error instanceof z.ZodError) {
const errorMessages = error.errors.map((e) => {
const path11 = e.path.length > 0 ? `${e.path.join(".")}` : "root";
return `${path11}: ${e.message}`;
});
return createErrorResponse(
"Parameter validation failed",
`Invalid parameters:
${errorMessages.join("\n")}`
);
}
throw error;
}
};
}
var init_typed_tool_factory = __esm({
"src/utils/typed-tool-factory.ts"() {
init_responses();
}
});
// src/mcp/tools/device/index.ts
var device_exports = {};
__export(device_exports, {
workflow: () => workflow
});
var workflow;
var init_device = __esm({
"src/mcp/tools/device/index.ts"() {
workflow = {
name: "iOS Device Development",
description: "Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting physical devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). Build, test, deploy, and debug apps on real hardware.",
platforms: ["iOS", "watchOS", "tvOS", "visionOS"],
targets: ["device"],
projectTypes: ["project", "workspace"],
capabilities: ["build", "test", "deploy", "debug", "log-capture", "device-management"]
};
}
});
// src/utils/xcode.ts
function constructDestinationString(platform2, simulatorName, simulatorId, useLatest = true, arch2) {
const isSimulatorPlatform = [
"iOS Simulator" /* iOSSimulator */,
"watchOS Simulator" /* watchOSSimulator */,
"tvOS Simulator" /* tvOSSimulator */,
"visionOS Simulator" /* visionOSSimulator */
].includes(platform2);
if (isSimulatorPlatform && simulatorId) {
return `platform=${platform2},id=${simulatorId}`;
}
if (isSimulatorPlatform && simulatorName) {
return `platform=${platform2},name=${simulatorName}${useLatest ? ",OS=latest" : ""}`;
}
if (isSimulatorPlatform && !simulatorId && !simulatorName) {
log(
"warning",
`Constructing generic destination for ${platform2} without name or ID. This might not be specific enough.`
);
throw new Error(`Simulator name or ID is required for specific ${platform2} operations`);
}
switch (platform2) {
case "macOS" /* macOS */:
return arch2 ? `platform=macOS,arch=${arch2}` : "platform=macOS";
case "iOS" /* iOS */:
return "generic/platform=iOS";
case "watchOS" /* watchOS */:
return "generic/platform=watchOS";
case "tvOS" /* tvOS */:
return "generic/platform=tvOS";
case "visionOS" /* visionOS */:
return "generic/platform=visionOS";
}
log("error", `Reached unexpected point in constructDestinationString for platform: ${platform2}`);
return `platform=${platform2}`;
}
var init_xcode = __esm({
"src/utils/xcode.ts"() {
init_logger();
init_common();
}
});
function isXcodemakeEnabled() {
const envValue = process.env[XCODEMAKE_ENV_VAR];
return envValue === "1" || envValue === "true" || envValue === "yes";
}
function getXcodemakeCommand() {
return overriddenXcodemakePath ?? "xcodemake";
}
function overrideXcodemakeCommand(path11) {
overriddenXcodemakePath = path11;
log("info", `Using overridden xcodemake path: ${path11}`);
}
async function installXcodemake() {
const tempDir = os4.tmpdir();
const xcodemakeDir = path3.join(tempDir, "xcodebuildmcp");
const xcodemakePath = path3.join(xcodemakeDir, "xcodemake");
log("info", `Attempting to install xcodemake to ${xcodemakePath}`);
try {
await fs2.mkdir(xcodemakeDir, { recursive: true });
log("info", "Downloading xcodemake from GitHub...");
const response = await fetch(
"https://raw.githubusercontent.com/cameroncooke/xcodemake/main/xcodemake"
);
if (!response.ok) {
throw new Error(`Failed to download xcodemake: ${response.status} ${response.statusText}`);
}
const scriptContent = await response.text();
await fs2.writeFile(xcodemakePath, scriptContent, "utf8");
await fs2.chmod(xcodemakePath, 493);
log("info", "Made xcodemake executable");
overrideXcodemakeCommand(xcodemakePath);
return true;
} catch (error) {
log(
"error",
`Error installing xcodemake: ${error instanceof Error ? error.message : String(error)}`
);
return false;
}
}
async function isXcodemakeAvailable() {
if (!isXcodemakeEnabled()) {
log("debug", "xcodemake is not enabled, skipping availability check");
return false;
}
try {
if (overriddenXcodemakePath && existsSync(overriddenXcodemakePath)) {
log("debug", `xcodemake found at overridden path: ${overriddenXcodemakePath}`);
return true;
}
const result = await getDefaultCommandExecutor()(["which", "xcodemake"]);
if (result.success) {
log("debug", "xcodemake found in PATH");
return true;
}
log("info", "xcodemake not found in PATH, attempting to download...");
const installed = await installXcodemake();
if (installed) {
log("info", "xcodemake installed successfully");
return true;
} else {
log("warn", "xcodemake installation failed");
return false;
}
} catch (error) {
log(
"error",
`Error checking for xcodemake: ${error instanceof Error ? error.message : String(error)}`
);
return false;
}
}
function doesMakefileExist(projectDir) {
return existsSync(`${projectDir}/Makefile`);
}
function doesMakeLogFileExist(projectDir, command) {
const originalDir = process.cwd();
try {
process.chdir(projectDir);
const xcodemakeCommand = ["xcodemake", ...command.slice(1)];
const escapedCommand = xcodemakeCommand.map((arg) => {
const prefix = projectDir + "/";
if (arg.startsWith(prefix)) {
return arg.substring(prefix.length);
}
return arg;
});
const commandString = escapedCommand.join(" ");
const logFileName = `${commandString}.log`;
log("debug", `Checking for Makefile log: ${logFileName} in directory: ${process.cwd()}`);
const files = readdirSync(".");
const exists = files.includes(logFileName);
log("debug", `Makefile log ${exists ? "exists" : "does not exist"}: ${logFileName}`);
return exists;
} catch (error) {
log(
"error",
`Error checking for Makefile log: ${error instanceof Error ? error.message : String(error)}`
);
return false;
} finally {
process.chdir(originalDir);
}
}
async function executeXcodemakeCommand(projectDir, buildArgs, logPrefix) {
process.chdir(projectDir);
const xcodemakeCommand = [getXcodemakeCommand(), ...buildArgs];
const command = xcodemakeCommand.map((arg) => arg.replace(projectDir + "/", ""));
return getDefaultCommandExecutor()(command, logPrefix);
}
async function executeMakeCommand(projectDir, logPrefix) {
const command = ["cd", projectDir, "&&", "make"];
return getDefaultCommandExecutor()(command, logPrefix);
}
var XCODEMAKE_ENV_VAR, overriddenXcodemakePath;
var init_xcodemake = __esm({
"src/utils/xcodemake.ts"() {
init_logger();
init_command();
XCODEMAKE_ENV_VAR = "INCREMENTAL_BUILDS_ENABLED";
overriddenXcodemakePath = null;
}
});
async function executeXcodeBuildCommand(params, platformOptions, preferXcodebuild = false, buildAction = "build", executor, execOpts) {
const buildMessages = [];
function grepWarningsAndErrors(text) {
return text.split("\n").map((content) => {
if (/warning:/i.test(content)) return { type: "warning", content };
if (/error:/i.test(content)) return { type: "error", content };
return null;
}).filter(Boolean);
}
log("info", `Starting ${platformOptions.logPrefix} ${buildAction} for scheme ${params.scheme}`);
const isXcodemakeEnabledFlag = isXcodemakeEnabled();
let xcodemakeAvailableFlag = false;
if (isXcodemakeEnabledFlag && buildAction === "build") {
xcodemakeAvailableFlag = await isXcodemakeAvailable();
if (xcodemakeAvailableFlag && preferXcodebuild) {
log(
"info",
"xcodemake is enabled but preferXcodebuild is set to true. Falling back to xcodebuild."
);
buildMessages.push({
type: "text",
text: "\u26A0\uFE0F incremental build support is enabled but preferXcodebuild is set to true. Falling back to xcodebuild."
});
} else if (!xcodemakeAvailableFlag) {
buildMessages.push({
type: "text",
text: "\u26A0\uFE0F xcodemake is enabled but not available. Falling back to xcodebuild."
});
log("info", "xcodemake is enabled but not available. Falling back to xcodebuild.");
} else {
log("info", "xcodemake is enabled and available, using it for incremental builds.");
buildMessages.push({
type: "text",
text: "\u2139\uFE0F xcodemake is enabled and available, using it for incremental builds."
});
}
}
try {
const command = ["xcodebuild"];
let projectDir = "";
if (params.workspacePath) {
projectDir = path3__default.dirname(params.workspacePath);
command.push("-workspace", params.workspacePath);
} else if (params.projectPath) {
projectDir = path3__default.dirname(params.projectPath);
command.push("-project", params.projectPath);
}
command.push("-scheme", params.scheme);
command.push("-configuration", params.configuration);
command.push("-skipMacroValidation");
let destinationString;
const isSimulatorPlatform = [
"iOS Simulator" /* iOSSimulator */,
"watchOS Simulator" /* watchOSSimulator */,
"tvOS Simulator" /* tvOSSimulator */,
"visionOS Simulator" /* visionOSSimulator */
].includes(platformOptions.platform);
if (isSimulatorPlatform) {
if (platformOptions.simulatorId) {
destinationString = constructDestinationString(
platformOptions.platform,
void 0,
platformOptions.simulatorId
);
} else if (platformOptions.simulatorName) {
destinationString = constructDestinationString(
platformOptions.platform,
platformOptions.simulatorName,
void 0,
platformOptions.useLatestOS
);
} else {
return createTextResponse(
`For ${platformOptions.platform} platform, either simulatorId or simulatorName must be provided`,
true
);
}
} else if (platformOptions.platform === "macOS" /* macOS */) {
destinationString = constructDestinationString(
platformOptions.platform,
void 0,
void 0,
false,
platformOptions.arch
);
} else if (platformOptions.platform === "iOS" /* iOS */) {
if (platformOptions.deviceId) {
destinationString = `platform=iOS,id=${platformOptions.deviceId}`;
} else {
destinationString = "generic/platform=iOS";
}
} else if (platformOptions.platform === "watchOS" /* watchOS */) {
if (platformOptions.deviceId) {
destinationString = `platform=watchOS,id=${platformOptions.deviceId}`;
} else {
destinationString = "generic/platform=watchOS";
}
} else if (platformOptions.platform === "tvOS" /* tvOS */) {
if (platformOptions.deviceId) {
destinationString = `platform=tvOS,id=${platformOptions.deviceId}`;
} else {
destinationString = "generic/platform=tvOS";
}
} else if (platformOptions.platform === "visionOS" /* visionOS */) {
if (platformOptions.deviceId) {
destinationString = `platform=visionOS,id=${platformOptions.deviceId}`;
} else {
destinationString = "generic/platform=visionOS";
}
} else {
return createTextResponse(`Unsupported platform: ${platformOptions.platform}`, true);
}
command.push("-destination", destinationString);
if (params.derivedDataPath) {
command.push("-derivedDataPath", params.derivedDataPath);
}
if (params.extraArgs && params.extraArgs.length > 0) {
command.push(...params.extraArgs);
}
command.push(buildAction);
let result;
if (isXcodemakeEnabledFlag && xcodemakeAvailableFlag && buildAction === "build" && !preferXcodebuild) {
const makefileExists = doesMakefileExist(projectDir);
log("debug", "Makefile exists: " + makefileExists);
const makeLogFileExists = doesMakeLogFileExist(projectDir, command);
log("debug", "Makefile log exists: " + makeLogFileExists);
if (makefileExists && makeLogFileExists) {
buildMessages.push({
type: "text",
text: "\u2139\uFE0F Using make for incremental build"
});
result = await executeMakeCommand(projectDir, platformOptions.logPrefix);
} else {
buildMessages.push({
type: "text",
text: "\u2139\uFE0F Generating Makefile with xcodemake (first build may take longer)"
});
result = await executeXcodemakeCommand(
projectDir,
command.slice(1),
platformOptions.logPrefix
);
}
} else {
result = await executor(command, platformOptions.logPrefix, true, execOpts);
}
const warningOrErrorLines = grepWarningsAndErrors(result.output);
warningOrErrorLines.forEach(({ type, content }) => {
buildMessages.push({
type: "text",
text: type === "warning" ? `\u26A0\uFE0F Warning: ${content}` : `\u274C Error: ${content}`
});
});
if (result.error) {
result.error.split("\n").forEach((content) => {
if (content.trim()) {
buildMessages.push({ type: "text", text: `\u274C [stderr] ${content}` });
}
});
}
if (!result.success) {
const isMcpError = result.exitCode === 64;
log(
isMcpError ? "error" : "warning",
`${platformOptions.logPrefix} ${buildAction} failed: ${result.error}`,
{ sentry: isMcpError }
);
const errorResponse = createTextResponse(
`\u274C ${platformOptions.logPrefix} ${buildAction} failed for scheme ${params.scheme}.`,
true
);
if (buildMessages.length > 0 && errorResponse.content) {
errorResponse.content.unshift(...buildMessages);
}
if (warningOrErrorLines.length == 0 && isXcodemakeEnabledFlag && xcodemakeAvailableFlag && buildAction === "build" && !preferXcodebuild) {
errorResponse.content.push({
type: "text",
text: `\u{1F4A1} Incremental build using xcodemake failed, suggest using preferXcodebuild option to try build again using slower xcodebuild command.`
});
}
return consolidateContentForClaudeCode(errorResponse);
}
log("info", `\u2705 ${platformOptions.logPrefix} ${buildAction} succeeded.`);
let additionalInfo = "";
if (isXcodemakeEnabledFlag && xcodemakeAvailableFlag && buildAction === "build" && !preferXcodebuild) {
additionalInfo += `xcodemake: Using faster incremental builds with xcodemake.
Future builds will use the generated Makefile for improved performance.
`;
}
if (buildAction === "build") {
if (platformOptions.platform === "macOS" /* macOS */) {
additionalInfo = `Next Steps:
1. Get app path: get_mac_app_path({ scheme: '${params.scheme}' })
2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })
3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })`;
} else if (platformOptions.platform === "iOS" /* iOS */) {
additionalInfo = `Next Steps:
1. Get app path: get_device_app_path({ scheme: '${params.scheme}' })
2. Get bundle ID: get_app_bundle_id({ appPath: 'PATH_FROM_STEP_1' })
3. Launch: launch_app_device({ bundleId: 'BUNDLE_ID_FROM_STEP_2' })`;
} else if (isSimulatorPlatform) {
const simIdParam = platformOptions.simulatorId ? "simulatorId" : "simulatorName";
const simIdValue = platformOptions.simulatorId ?? platformOptions.simulatorName;
additionalInfo = `Next Steps:
1. Get app path: get_sim_app_path({ ${simIdParam}: '${simIdValue}', scheme: '${params.scheme}', platform: 'iOS Simulator' })
2. Get bundle ID: get_app_bundle_id({ appPath: 'PATH_FROM_STEP_1' })
3. Launch: launch_app_sim({ ${simIdParam}: '${simIdValue}', bundleId: 'BUNDLE_ID_FROM_STEP_2' })
Or with logs: launch_app_logs_sim({ ${simIdParam}: '${simIdValue}', bundleId: 'BUNDLE_ID_FROM_STEP_2' })`;
}
}
const successResponse = {
content: [
...buildMessages,
{
type: "text",
text: `\u2705 ${platformOptions.logPrefix} ${buildAction} succeeded for scheme ${params.scheme}.`
}
]
};
if (additionalInfo) {
successResponse.content.push({
type: "text",
text: additionalInfo
});
}
return consolidateContentForClaudeCode(successResponse);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const isSpawnError = error instanceof Error && "code" in error && ["ENOENT", "EACCES", "EPERM"].includes(error.code ?? "");
log("error", `Error during ${platformOptions.logPrefix} ${buildAction}: ${errorMessage}`, {
sentry: !isSpawnError
});
return consolidateContentForClaudeCode(
createTextResponse(
`Error during ${platformOptions.logPrefix} ${buildAction}: ${errorMessage}`,
true
)
);
}
}
var init_build_utils = __esm({
"src/utils/build-utils.ts"() {
init_logger();
init_xcode();
init_validation();
init_xcodemake();
}
});
// src/utils/build/index.ts
var init_build = __esm({
"src/utils/build/index.ts"() {
init_build_utils();
}
});
// src/utils/schema-helpers.ts
function nullifyEmptyStrings(value) {
if (value && typeof value === "object" && !Array.isArray(value)) {
const copy = { ...value };
for (const key of Object.keys(copy)) {
const v = copy[key];
if (typeof v === "string" && v.trim() === "") copy[key] = void 0;
}
return copy;
}
return value;
}
var init_schema_helpers = __esm({
"src/utils/schema-helpers.ts"() {
}
});
// src/mcp/tools/device/build_device.ts
var build_device_exports = {};
__export(build_device_exports, {
buildDeviceLogic: () => buildDeviceLogic,
default: () => build_device_default
});
async function buildDeviceLogic(params, executor) {
const processedParams = {
...params,
configuration: params.configuration ?? "Debug"
// Default config
};
return executeXcodeBuildCommand(
processedParams,
{
platform: "iOS" /* iOS */,
logPrefix: "iOS Device Build"
},
params.preferXcodebuild ?? false,
"build",
executor
);
}
var baseSchemaObject, baseSchema, buildDeviceSchema, build_device_default;
var init_build_device = __esm({
"src/mcp/tools/device/build_device.ts"() {
init_common();
init_build();
init_execution();
init_typed_tool_factory();
init_schema_helpers();
baseSchemaObject = z.object({
projectPath: z.string().optional().describe("Path to the .xcodeproj file"),
workspacePath: z.string().optional().describe("Path to the .xcworkspace file"),
scheme: z.string().describe("The scheme to build"),
configuration: z.string().optional().describe("Build configuration (Debug, Release)"),
derivedDataPath: z.string().optional().describe("Path to derived data directory"),
extraArgs: z.array(z.string()).optional().describe("Additional arguments to pass to xcodebuild"),
preferXcodebuild: z.boolean().optional().describe("Prefer xcodebuild over faster alternatives")
});
baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);
buildDeviceSchema = baseSchema.refine((val) => val.projectPath !== void 0 || val.workspacePath !== void 0, {
message: "Either projectPath or workspacePath is required."
}).refine((val) => !(val.projectPath !== void 0 && val.workspacePath !== void 0), {
message: "projectPath and workspacePath are mutually exclusive. Provide only one."
});
build_device_default = {
name: "build_device",
description: "Builds an app from a project or workspace for a physical Apple device. Provide exactly one of projectPath or workspacePath. Example: build_device({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme' })",
schema: baseSchemaObject.shape,
handler: createTypedTool(
buildDeviceSchema,
buildDeviceLogic,
getDefaultCommandExecutor
)
};
}
});
// src/mcp/tools/utilities/clean.ts
var clean_exports = {};
__export(clean_exports, {
cleanLogic: () => cleanLogic,
default: () => clean_default
});
async function cleanLogic(params, executor) {
if (params.workspacePath && !params.scheme) {
return createErrorResponse(
"Parameter validation failed",
"Invalid parameters:\nscheme: scheme is required when workspacePath is provided."
);
}
const targetPlatform = params.platform ?? "iOS";
const platformMap = {
macOS: "macOS" /* macOS */,
iOS: "iOS" /* iOS */,
"iOS Simulator": "iOS Simulator" /* iOSSimulator */,
watchOS: "watchOS" /* watchOS */,
"watchOS Simulator": "watchOS Simulator" /* watchOSSimulator */,
tvOS: "tvOS" /* tvOS */,
"tvOS Simulator": "tvOS Simulator" /* tvOSSimulator */,
visionOS: "visionOS" /* visionOS */,
"visionOS Simulator": "visionOS Simulator" /* visionOSSimulator */
};
const platformEnum = platformMap[targetPlatform];
if (!platformEnum) {
return createErrorResponse(
"Parameter validation failed",
`Invalid parameters:
platform: unsupported value "${targetPlatform}".`
);
}
const hasProjectPath = typeof params.projectPath === "string";
const typedParams = {
...hasProjectPath ? { projectPath: params.projectPath } : { workspacePath: params.workspacePath },
// scheme may be omitted for project; when omitted we do not pass -scheme
// Provide empty string to satisfy type, executeXcodeBuildCommand only emits -scheme when non-empty
scheme: params.scheme ?? "",
configuration: params.configuration ?? "Debug",
derivedDataPath: params.derivedDataPath,
extraArgs: params.extraArgs
};
const cleanPlatformMap = {
["iOS Simulator" /* iOSSimulator */]: "iOS" /* iOS */,
["watchOS Simulator" /* watchOSSimulator */]: "watchOS" /* watchOS */,
["tvOS Simulator" /* tvOSSimulator */]: "tvOS" /* tvOS */,
["visionOS Simulator" /* visionOSSimulator */]: "visionOS" /* visionOS */
};
const cleanPlatform = cleanPlatformMap[platformEnum] ?? platformEnum;
return executeXcodeBuildCommand(
typedParams,
{
platform: cleanPlatform,
logPrefix: "Clean"
},
false,
"clean",
executor
);
}
var baseOptions, baseSchemaObject2, baseSchema2, cleanSchema, clean_default;
var init_clean = __esm({
"src/mcp/tools/utilities/clean.ts"() {
init_typed_tool_factory();
init_execution();
init_build();
init_common();
init_responses();
init_schema_helpers();
baseOptions = {
scheme: z.string().optional().describe("Optional: The scheme to clean"),
configuration: z.string().optional().describe("Optional: Build configuration to clean (Debug, Release, etc.)"),
derivedDataPath: z.string().optional().describe("Optional: Path where derived data might be located"),
extraArgs: z.array(z.string()).optional().describe("Additional xcodebuild arguments"),
preferXcodebuild: z.boolean().optional().describe(
"If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails."
),
platform: z.enum([
"macOS",
"iOS",
"iOS Simulator",
"watchOS",
"watchOS Simulator",
"tvOS",
"tvOS Simulator",
"visionOS",
"visionOS Simulator"
]).optional().describe(
"Optional: Platform to clean for (defaults to iOS). Choose from macOS, iOS, iOS Simulator, watchOS, watchOS Simulator, tvOS, tvOS Simulator, visionOS, visionOS Simulator"
)
};
baseSchemaObject2 = z.object({
projectPath: z.string().optional().describe("Path to the .xcodeproj file"),
workspacePath: z.string().optional().describe("Path to the .xcworkspace file"),
...baseOptions
});
baseSchema2 = z.preprocess(nullifyEmptyStrings, baseSchemaObject2);
cleanSchema = baseSchema2.refine((val) => val.projectPath !== void 0 || val.workspacePath !== void 0, {
message: "Either projectPath or workspacePath is required."
}).refine((val) => !(val.projectPath !== void 0 && val.workspacePath !== void 0), {
message: "projectPath and workspacePath are mutually exclusive. Provide only one."
}).refine((val) => !(val.workspacePath && !val.scheme), {
message: "scheme is required when workspacePath is provided.",
path: ["scheme"]
});
clean_default = {
name: "clean",
description: "Cleans build products for either a project or a workspace using xcodebuild. Provide exactly one of projectPath or workspacePath. Platform defaults to iOS if not specified. Example: clean({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme', platform: 'iOS' })",
schema: baseSchemaObject2.shape,
handler: createTypedTool(
cleanSchema,
cleanLogic,
getDefaultCommandExecutor
)
};
}
});
// src/mcp/tools/device/clean.ts
var clean_exports2 = {};
__export(clean_exports2, {
default: () => clean_default
});
var init_clean2 = __esm({
"src/mcp/tools/device/clean.ts"() {
init_clean();
}
});
// src/mcp/tools/project-discovery/discover_projs.ts
var discover_projs_exports = {};
__export(discover_projs_exports, {
default: () => discover_projs_default,
discover_projsLogic: () => discover_projsLogic
});
async function _findProjectsRecursive(currentDirAbs, workspaceRootAbs, currentDepth, maxDepth, results, fileSystemExecutor = getDefaultFileSystemExecutor()) {
if (currentDepth >= maxDepth) {
log("debug", `Max depth ${maxDepth} reached at ${currentDirAbs}, stopping recursion.`);
return;
}
log("debug", `Scanning directory: ${currentDirAbs} at depth ${currentDepth}`);
const normalizedWorkspaceRoot = path3.normalize(workspaceRootAbs);
try {
const entries = await fileSystemExecutor.readdir(currentDirAbs, { withFileTypes: true });
for (const rawEntry of entries) {
const entry = rawEntry;
const absoluteEntryPath = path3.join(currentDirAbs, entry.name);
const relativePath = path3.relative(workspaceRootAbs, absoluteEntryPath);
if (entry.isSymbolicLink()) {
log("debug", `Skipping symbolic link: ${relativePath}`);
continue;
}
if (entry.isDirectory() && SKIPPED_DIRS.has(entry.name)) {
log("debug", `Skipping standard directory: ${relativePath}`);
continue;
}
if (!path3.normalize(absoluteEntryPath).startsWith(normalizedWorkspaceRoot)) {
log(
"warn",
`Skipping entry outside workspace root: ${absoluteEntryPath} (Workspace: ${workspaceRootAbs})`
);
continue;
}
if (entry.isDirectory()) {
let isXcodeBundle = false;
if (entry.name.endsWith(".xcodeproj")) {
results.projects.push(absoluteEntryPath);
log("debug", `Found project: ${absoluteEntryPath}`);
isXcodeBundle = true;
} else if (entry.name.endsWith(".xcworkspace")) {
results.workspaces.push(absoluteEntryPath);
log("debug", `Found workspace: ${absoluteEntryPath}`);
isXcodeBundle = true;
}
if (!isXcodeBundle) {
await _findProjectsRecursive(
absoluteEntryPath,
workspaceRootAbs,
currentDepth + 1,
maxDepth,
results,
fileSystemExecutor
);
}
}
}
} catch (error) {
let code;
let message = "Unknown error";
if (error instanceof Error) {
message = error.message;
if ("code" in error) {
code = error.code;
}
} else if (typeof error === "object" && error !== null) {
if ("message" in error && typeof error.message === "string") {
message = error.message;
}
if ("code" in error && typeof error.code === "string") {
code = error.code;
}
} else {
message = String(error);
}
if (code === "EPERM" || code === "EACCES") {
log("debug", `Permission denied scanning directory: ${currentDirAbs}`);
} else {
log(
"warning",
`Error scanning directory ${currentDirAbs}: ${message} (Code: ${code ?? "N/A"})`
);
}
}
}
async function discover_projsLogic(params, fileSystemExecutor) {
const scanPath = params.scanPath ?? ".";
const maxDepth = params.maxDepth ?? DEFAULT_MAX_DEPTH;
const workspaceRoot = params.workspaceRoot;
const relativeScanPath = scanPath;
const requestedScanPath = path3.resolve(workspaceRoot, relativeScanPath ?? ".");
let absoluteScanPath = requestedScanPath;
const normalizedWorkspaceRoot = path3.normalize(workspaceRoot);
if (!path3.normalize(absoluteScanPath).startsWith(normalizedWorkspaceRoot)) {
log(
"warn",
`Requested scan path '${relativeScanPath}' resolved outside workspace root '${workspaceRoot}'. Defaulting scan to workspace root.`
);
absoluteScanPath = normalizedWorkspaceRoot;
}
const results = { projects: [], workspaces: [] };
log(
"info",
`Starting project discovery request: path=${absoluteScanPath}, maxDepth=${maxDepth}, workspace=${workspaceRoot}`
);
try {
const stats = await fileSystemExecutor.stat(absoluteScanPath);
if (!stats.isDirectory()) {
const errorMsg = `Scan path is not a directory: ${absoluteScanPath}`;
log("error", errorMsg);
return {
content: [createTextContent(errorMsg)],
isError: true
};
}
} catch (error) {
let code;
let message = "Unknown error accessing scan path";
if (error instanceof Error) {
message = error.message;
if ("code" in error) {
code = error.code;
}
} else if (typeof error === "object" && error !== null) {
if ("message" in error && typeof error.message === "string") {
message = error.message;
}
if ("code" in error && typeof error.code === "string") {
code = error.code;
}
} else {
message = String(error);
}
const errorMsg = `Failed to access scan path: ${absoluteScanPath}. Error: ${message}`;
log("error", `${errorMsg} - Code: ${code ?? "N/A"}`);
return {
content: [createTextContent(errorMsg)],
isError: true
};
}
await _findProjectsRecursive(
absoluteScanPath,
workspaceRoot,
0,
maxDepth,
results,
fileSystemExecutor
);
log(
"info",
`Discovery finished. Found ${results.projects.length} projects and ${results.workspaces.length} workspaces.`
);
const responseContent = [
createTextContent(
`Discovery finished. Found ${results.projects.length} projects and ${results.workspaces.length} workspaces.`
)
];
results.projects.sort();
results.workspaces.sort();
if (results.projects.length > 0) {
responseContent.push(
createTextContent(`Projects found:
- ${results.projects.join("\n - ")}`)
);
}
if (results.workspaces.length > 0) {
responseContent.push(
createTextContent(`Workspaces found:
- ${results.workspaces.join("\n - ")}`)
);
}
return {
content: responseContent,
isError: false
};
}
var DEFAULT_MAX_DEPTH, SKIPPED_DIRS, discoverProjsSchema, discover_projs_default;
var init_discover_projs = __esm({
"src/mcp/tools/project-discovery/discover_projs.ts"() {
init_logging();
init_common();
init_command();
init_typed_tool_factory();
DEFAULT_MAX_DEPTH = 5;
SKIPPED_DIRS = /* @__PURE__ */ new Set(["build", "DerivedData", "Pods", ".git", "node_modules"]);
discoverProjsSchema = z.object({
workspaceRoot: z.string().describe("The absolute path of the workspace root to scan within."),
scanPath: z.string().optional().describe("Optional: Path relative to workspace root to scan. Defaults to workspace root."),
maxDepth: z.number().int().nonnegative().optional().describe(`Optional: Maximum directory depth to scan. Defaults to ${DEFAULT_MAX_DEPTH}.`)
});
discover_projs_default = {
name: "discover_projs",
description: "Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj) and workspace (.xcworkspace) files.",
schema: discoverProjsSchema.shape,
// MCP SDK compatibility
handler: createTypedTool(
discoverProjsSchema,
(params) => {
return discover_projsLogic(params, getDefaultFileSystemExecutor());
},
getDefaultCommandExecutor
)
};
}
});
// src/mcp/tools/device/discover_projs.ts
var discover_projs_exports2 = {};
__export(discover_projs_exports2, {
default: () => discover_projs_default
});
var init_discover_projs2 = __esm({
"src/mcp/tools/device/discover_projs.ts"() {
init_discover_projs();
}
});
// src/mcp/tools/project-discovery/get_app_bundle_id.ts
var get_app_bundle_id_exports = {};
__export(get_app_bundle_id_exports, {
default: () => get_app_bundle_id_default,
get_app_bundle_idLogic: () => get_app_bundle_idLogic
});
async function executeSyncCommand(command, executor) {
const result = await executor(["/bin/sh", "-c", command], "Bundle ID Extraction");
if (!result.success) {
throw new Error(result.error ?? "Command failed");
}
return result.output || "";
}
async function get_app_bundle_idLogic(params, executor, fileSystemExecutor) {
const appPath = params.appPath;
if (!fileSystemExecutor.existsSync(appPath)) {
return {
content: [
{
type: "text",
text: `File not found: '${appPath}'. Please check the path and try again.`
}
],
isError: true
};
}
log("info", `Starting bundle ID extraction for app: ${appPath}`);
try {
let bundleId;
try {
bundleId = await executeSyncCommand(
`defaults read "${appPath}/Info" CFBundleIdentifier`,
executor
);
} catch {
try {
bundleId = await executeSyncCommand(
`/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${appPath}/Info.plist"`,
executor
);
} catch (innerError) {
throw new Error(
`Could not extract bundle ID from Info.plist: ${innerError instanceof Error ? innerError.message : String(innerError)}`
);
}
}
log("info", `Extracted app bundle ID: ${bundleId}`);
return {
content: [
{
type: "text",
text: `\u2705 Bundle ID: ${bundleId}`
},
{
type: "text",
text: `Next Steps:
- Simulator: install_app_sim + launch_app_sim
- Device: install_app_device + launch_app_device`
}
],
isError: false
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log("error", `Error extracting app bundle ID: ${errorMessage}`);
return {
content: [
{
type: "text",
text: `Error extracting app bundle ID: ${errorMessage}`
},
{
type: "text",
text: `Make sure the path points to a valid app bundle (.app directory).`
}
],
isError: true
};
}
}
var getAppBundleIdSchema, get_app_bundle_id_default;
var init_get_app_bundle_id = __esm({
"src/mcp/tools/project-discovery/get_app_bundle_id.ts"() {
init_logging();
init_command();
init_typed_tool_factory();
getAppBundleIdSchema = z.object({
appPath: z.string().describe(
"Path to the .app bundle to extract bundle ID from (full path to the .app directory)"
)
});
get_app_bundle_id_default = {
name: "get_app_bundle_id",
description: "Extracts t