xcodebuildmcp
Version:
XcodeBuildMCP is a ModelContextProtocol server that provides tools for Xcode project management, simulator management, and app utilities.
1,412 lines (1,392 loc) • 441 kB
JavaScript
#!/usr/bin/env node
import { createRequire } from 'module';
import { execSync, spawn, exec } from 'child_process';
import * as fs6 from 'fs';
import { existsSync, promises, readdirSync } from 'fs';
import * as os4 from 'os';
import { tmpdir } from 'os';
import * as path3 from 'path';
import path3__default, { dirname, join, basename } from 'path';
import * as fs from 'fs/promises';
import { mkdtemp, rm } from 'fs/promises';
import { z } from 'zod';
import { v4 } from 'uuid';
import { randomUUID } from 'crypto';
import { fileURLToPath } from 'url';
import { promisify } from 'util';
import * as Sentry from '@sentry/node';
import { McpServer } from '@camsoft/mcp-sdk/server/mcp.js';
import { StdioServerTransport } from '@camsoft/mcp-sdk/server/stdio.js';
import { SetLevelRequestSchema } from '@camsoft/mcp-sdk/types.js';
import process2 from 'process';
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 setLogLevel(level) {
clientLogLevel = level;
log("debug", `Log level set to: ${level}`);
}
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");
}
}
}
});
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();
}
};
}
});
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 fs.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 fs.writeFile(xcodemakePath, scriptContent, "utf8");
await fs.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;
}
});
// src/utils/logging/index.ts
var init_logging = __esm({
"src/utils/logging/index.ts"() {
init_logger();
}
});
// src/utils/execution/index.ts
var init_execution = __esm({
"src/utils/execution/index.ts"() {
init_command();
}
});
// 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/list_devices.ts
var list_devices_exports = {};
__export(list_devices_exports, {
default: () => list_devices_default,
list_devicesLogic: () => list_devicesLogic
});
async function list_devicesLogic(params, executor, pathDeps, fsDeps) {
log("info", "Starting device discovery");
try {
const tempDir = pathDeps?.tmpdir ? pathDeps.tmpdir() : tmpdir();
const timestamp = pathDeps?.join ? "123" : Date.now();
const tempJsonPath = pathDeps?.join ? pathDeps.join(tempDir, `devicectl-${timestamp}.json`) : join(tempDir, `devicectl-${timestamp}.json`);
const devices = [];
let useDevicectl = false;
try {
const result = await executor(
["xcrun", "devicectl", "list", "devices", "--json-output", tempJsonPath],
"List Devices (devicectl with JSON)",
true,
void 0
);
if (result.success) {
useDevicectl = true;
const jsonContent = fsDeps?.readFile ? await fsDeps.readFile(tempJsonPath, "utf8") : await promises.readFile(tempJsonPath, "utf8");
const deviceCtlData = JSON.parse(jsonContent);
const isValidDeviceData = (data) => {
return typeof data === "object" && data !== null && "result" in data && typeof data.result === "object" && data.result !== null && "devices" in data.result && Array.isArray(
data.result.devices
);
};
if (isValidDeviceData(deviceCtlData) && deviceCtlData.result?.devices) {
for (const deviceRaw of deviceCtlData.result.devices) {
const isValidDevice = (device2) => {
if (typeof device2 !== "object" || device2 === null) {
return false;
}
const dev = device2;
if (typeof dev.identifier !== "string" && dev.identifier !== void 0) {
return false;
}
if (dev.visibilityClass !== void 0 && typeof dev.visibilityClass !== "string") {
return false;
}
if (dev.connectionProperties !== void 0) {
if (typeof dev.connectionProperties !== "object" || dev.connectionProperties === null) {
return false;
}
const connProps = dev.connectionProperties;
if (connProps.pairingState !== void 0 && typeof connProps.pairingState !== "string") {
return false;
}
if (connProps.tunnelState !== void 0 && typeof connProps.tunnelState !== "string") {
return false;
}
if (connProps.transportType !== void 0 && typeof connProps.transportType !== "string") {
return false;
}
}
if (dev.deviceProperties !== void 0) {
if (typeof dev.deviceProperties !== "object" || dev.deviceProperties === null) {
return false;
}
const devProps = dev.deviceProperties;
if (devProps.platformIdentifier !== void 0 && typeof devProps.platformIdentifier !== "string") {
return false;
}
if (devProps.name !== void 0 && typeof devProps.name !== "string") {
return false;
}
if (devProps.osVersionNumber !== void 0 && typeof devProps.osVersionNumber !== "string") {
return false;
}
if (devProps.developerModeStatus !== void 0 && typeof devProps.developerModeStatus !== "string") {
return false;
}
if (devProps.marketingName !== void 0 && typeof devProps.marketingName !== "string") {
return false;
}
}
if (dev.hardwareProperties !== void 0) {
if (typeof dev.hardwareProperties !== "object" || dev.hardwareProperties === null) {
return false;
}
const hwProps = dev.hardwareProperties;
if (hwProps.productType !== void 0 && typeof hwProps.productType !== "string") {
return false;
}
if (hwProps.cpuType !== void 0) {
if (typeof hwProps.cpuType !== "object" || hwProps.cpuType === null) {
return false;
}
const cpuType = hwProps.cpuType;
if (cpuType.name !== void 0 && typeof cpuType.name !== "string") {
return false;
}
}
}
return true;
};
if (!isValidDevice(deviceRaw)) continue;
const device = deviceRaw;
if (device.visibilityClass === "Simulator" || !device.connectionProperties?.pairingState) {
continue;
}
let platform2 = "Unknown";
const platformId = device.deviceProperties?.platformIdentifier?.toLowerCase() ?? "";
if (typeof platformId === "string") {
if (platformId.includes("ios") || platformId.includes("iphone")) {
platform2 = "iOS";
} else if (platformId.includes("ipad")) {
platform2 = "iPadOS";
} else if (platformId.includes("watch")) {
platform2 = "watchOS";
} else if (platformId.includes("tv") || platformId.includes("apple tv")) {
platform2 = "tvOS";
} else if (platformId.includes("vision")) {
platform2 = "visionOS";
}
}
const pairingState = device.connectionProperties?.pairingState ?? "";
const tunnelState = device.connectionProperties?.tunnelState ?? "";
const transportType = device.connectionProperties?.transportType ?? "";
let state = "Unknown";
if (pairingState === "paired") {
if (tunnelState === "connected") {
state = "Available";
} else {
state = "Available (WiFi)";
}
} else {
state = "Unpaired";
}
devices.push({
name: device.deviceProperties?.name ?? "Unknown Device",
identifier: device.identifier ?? "Unknown",
platform: platform2,
model: device.deviceProperties?.marketingName ?? device.hardwareProperties?.productType,
osVersion: device.deviceProperties?.osVersionNumber,
state,
connectionType: transportType,
trustState: pairingState,
developerModeStatus: device.deviceProperties?.developerModeStatus,
productType: device.hardwareProperties?.productType,
cpuArchitecture: device.hardwareProperties?.cpuType?.name
});
}
}
}
} catch {
log("info", "devicectl with JSON failed, trying xctrace fallback");
} finally {
try {
if (fsDeps?.unlink) {
await fsDeps.unlink(tempJsonPath);
} else {
await promises.unlink(tempJsonPath);
}
} catch {
}
}
if (!useDevicectl || devices.length === 0) {
const result = await executor(
["xcrun", "xctrace", "list", "devices"],
"List Devices (xctrace)",
true,
void 0
);
if (!result.success) {
return {
content: [
{
type: "text",
text: `Failed to list devices: ${result.error}
Make sure Xcode is installed and devices are connected and trusted.`
}
],
isError: true
};
}
return {
content: [
{
type: "text",
text: `Device listing (xctrace output):
${result.output}
Note: For better device information, please upgrade to Xcode 15 or later which supports the modern devicectl command.`
}
]
};
}
let responseText = "Connected Devices:\n\n";
const uniqueDevices = devices.filter(
(device, index, self) => index === self.findIndex((d) => d.identifier === device.identifier)
);
if (uniqueDevices.length === 0) {
responseText += "No physical Apple devices found.\n\n";
responseText += "Make sure:\n";
responseText += "1. Devices are connected via USB or WiFi\n";
responseText += "2. Devices are unlocked and trusted\n";
responseText += '3. "Trust this computer" has been accepted on the device\n';
responseText += "4. Developer mode is enabled on the device (iOS 16+)\n";
responseText += "5. Xcode is properly installed\n\n";
responseText += "For simulators, use the list_sims tool instead.\n";
} else {
const availableDevices = uniqueDevices.filter(
(d) => d.state === "Available" || d.state === "Available (WiFi)" || d.state === "Connected"
);
const pairedDevices = uniqueDevices.filter((d) => d.state === "Paired (not connected)");
const unpairedDevices = uniqueDevices.filter((d) => d.state === "Unpaired");
if (availableDevices.length > 0) {
responseText += "\u2705 Available Devices:\n";
for (const device of availableDevices) {
responseText += `
\u{1F4F1} ${device.name}
`;
responseText += ` UDID: ${device.identifier}
`;
responseText += ` Model: ${device.model ?? "Unknown"}
`;
if (device.productType) {
responseText += ` Product Type: ${device.productType}
`;
}
responseText += ` Platform: ${device.platform} ${device.osVersion ?? ""}
`;
if (device.cpuArchitecture) {
responseText += ` CPU Architecture: ${device.cpuArchitecture}
`;
}
responseText += ` Connection: ${device.connectionType ?? "Unknown"}
`;
if (device.developerModeStatus) {
responseText += ` Developer Mode: ${device.developerModeStatus}
`;
}
}
responseText += "\n";
}
if (pairedDevices.length > 0) {
responseText += "\u{1F517} Paired but Not Connected:\n";
for (const device of pairedDevices) {
responseText += `
\u{1F4F1} ${device.name}
`;
responseText += ` UDID: ${device.identifier}
`;
responseText += ` Model: ${device.model ?? "Unknown"}
`;
responseText += ` Platform: ${device.platform} ${device.osVersion ?? ""}
`;
}
responseText += "\n";
}
if (unpairedDevices.length > 0) {
responseText += "\u274C Unpaired Devices:\n";
for (const device of unpairedDevices) {
responseText += `- ${device.name} (${device.identifier})
`;
}
responseText += "\n";
}
}
const availableDevicesExist = uniqueDevices.some(
(d) => d.state === "Available" || d.state === "Available (WiFi)" || d.state === "Connected"
);
if (availableDevicesExist) {
responseText += "Next Steps:\n";
responseText += "1. Build for device: build_device({ scheme: 'SCHEME', deviceId: 'DEVICE_UDID' })\n";
responseText += "2. Run tests: test_device({ scheme: 'SCHEME', deviceId: 'DEVICE_UDID' })\n";
responseText += "3. Get app path: get_device_app_path({ scheme: 'SCHEME' })\n\n";
responseText += "Note: Use the device ID/UDID from above when required by other tools.\n";
} else if (uniqueDevices.length > 0) {
responseText += "Note: No devices are currently available for testing. Make sure devices are:\n";
responseText += "- Connected via USB\n";
responseText += "- Unlocked and trusted\n";
responseText += "- Have developer mode enabled (iOS 16+)\n";
}
return {
content: [
{
type: "text",
text: responseText
}
]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log("error", `Error listing devices: ${errorMessage}`);
return {
content: [
{
type: "text",
text: `Failed to list devices: ${errorMessage}`
}
],
isError: true
};
}
}
var listDevicesSchema, list_devices_default;
var init_list_devices = __esm({
"src/mcp/tools/device/list_devices.ts"() {
init_logging();
init_execution();
init_typed_tool_factory();
listDevicesSchema = z.object({});
list_devices_default = {
name: "list_devices",
description: "Lists connected physical Apple devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro) with their UUIDs, names, and connection status. Use this to discover physical devices for testing.",
schema: listDevicesSchema.shape,
// MCP SDK compatibility
handler: createTypedTool(listDevicesSchema, list_devicesLogic, getDefaultCommandExecutor)
};
}
});
// src/mcp/resources/devices.ts
var devices_exports = {};
__export(devices_exports, {
default: () => devices_default,
devicesResourceLogic: () => devicesResourceLogic
});
async function devicesResourceLogic(executor = getDefaultCommandExecutor()) {
try {
log("info", "Processing devices resource request");
const result = await list_devicesLogic({}, executor);
if (result.isError) {
const errorText = result.content[0]?.text;
throw new Error(typeof errorText === "string" ? errorText : "Failed to retrieve device data");
}
return {
contents: [
{
text: typeof result.content[0]?.text === "string" ? result.content[0].text : "No device data available"
}
]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log("error", `Error in devices resource handler: ${errorMessage}`);
return {
contents: [
{
text: `Error retrieving device data: ${errorMessage}`
}
]
};
}
}
var devices_default;
var init_devices = __esm({
"src/mcp/resources/devices.ts"() {
init_logging();
init_execution();
init_list_devices();
devices_default = {
uri: "xcodebuildmcp://devices",
name: "devices",
description: "Connected physical Apple devices with their UUIDs, names, and connection status",
mimeType: "text/plain",
async handler() {
return devicesResourceLogic();
}
};
}
});
// src/utils/version/index.ts
var init_version2 = __esm({
"src/utils/version/index.ts"() {
init_version();
}
});
// 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();
}
});
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":