#!/usr/bin/env node
/**
* Xcode MCP Server
*
* An MCP server that provides tools for working with Xcode projects:
* - list-projects: Find Xcode projects in a directory
* - read-project: Parse and read project structure
* - list-targets: Get build targets from a project
* - build: Trigger xcodebuild
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import * as path from "path";
import { spawn } from "child_process";
import {
findXcodeProjects,
parseXcodeProject,
findSchemes,
} from "./xcode-parser.js";
// Tool definitions
const TOOLS: Tool[] = [
{
name: "list-projects",
description:
"Find all Xcode projects (.xcodeproj) in a directory. Searches recursively up to 5 levels deep, excluding common build directories.",
inputSchema: {
type: "object",
properties: {
directory: {
type: "string",
description:
"Directory to search for Xcode projects. Defaults to current working directory.",
},
},
required: [],
},
},
{
name: "read-project",
description:
"Read and parse an Xcode project file (.xcodeproj). Returns project structure including targets, build configurations, and source files.",
inputSchema: {
type: "object",
properties: {
projectPath: {
type: "string",
description: "Path to the .xcodeproj directory",
},
},
required: ["projectPath"],
},
},
{
name: "list-targets",
description:
"List all build targets in an Xcode project with their product types (app, framework, test, etc.)",
inputSchema: {
type: "object",
properties: {
projectPath: {
type: "string",
description: "Path to the .xcodeproj directory",
},
},
required: ["projectPath"],
},
},
{
name: "list-schemes",
description:
"List all schemes available in an Xcode project (both shared and user schemes)",
inputSchema: {
type: "object",
properties: {
projectPath: {
type: "string",
description: "Path to the .xcodeproj directory",
},
},
required: ["projectPath"],
},
},
{
name: "build",
description:
"Build an Xcode project using xcodebuild. Can specify target, scheme, configuration, and destination.",
inputSchema: {
type: "object",
properties: {
projectPath: {
type: "string",
description: "Path to the .xcodeproj directory",
},
scheme: {
type: "string",
description: "Scheme to build (optional if target is specified)",
},
target: {
type: "string",
description: "Target to build (optional if scheme is specified)",
},
configuration: {
type: "string",
description: "Build configuration (Debug or Release). Defaults to Debug.",
enum: ["Debug", "Release"],
},
destination: {
type: "string",
description:
'Build destination, e.g., "platform=iOS Simulator,name=iPhone 15"',
},
clean: {
type: "boolean",
description: "Clean before building",
},
},
required: ["projectPath"],
},
},
{
name: "xcodebuild-info",
description:
"Get xcodebuild version and available SDKs information",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
];
// Helper to run shell commands
function runCommand(
command: string,
args: string[],
options?: { cwd?: string; timeout?: number }
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
return new Promise((resolve) => {
const proc = spawn(command, args, {
cwd: options?.cwd,
shell: false,
});
let stdout = "";
let stderr = "";
proc.stdout.on("data", (data) => {
stdout += data.toString();
});
proc.stderr.on("data", (data) => {
stderr += data.toString();
});
const timeout = options?.timeout || 300000; // 5 min default
const timer = setTimeout(() => {
proc.kill("SIGTERM");
resolve({
stdout,
stderr: stderr + "\n[TIMEOUT: Process killed after " + timeout + "ms]",
exitCode: -1,
});
}, timeout);
proc.on("close", (code) => {
clearTimeout(timer);
resolve({
stdout,
stderr,
exitCode: code ?? 0,
});
});
proc.on("error", (err) => {
clearTimeout(timer);
resolve({
stdout,
stderr: stderr + "\n[ERROR: " + err.message + "]",
exitCode: -1,
});
});
});
}
// Tool implementations
async function handleListProjects(args: { directory?: string }) {
const directory = args.directory || process.cwd();
const resolvedDir = path.resolve(directory);
const projects = await findXcodeProjects(resolvedDir);
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
searchDirectory: resolvedDir,
projectCount: projects.length,
projects: projects.map((p) => ({
path: p,
name: path.basename(p, ".xcodeproj"),
})),
},
null,
2
),
},
],
};
}
async function handleReadProject(args: { projectPath: string }) {
const projectPath = path.resolve(args.projectPath);
const project = await parseXcodeProject(projectPath);
return {
content: [
{
type: "text" as const,
text: JSON.stringify(project, null, 2),
},
],
};
}
async function handleListTargets(args: { projectPath: string }) {
const projectPath = path.resolve(args.projectPath);
const project = await parseXcodeProject(projectPath);
const targetInfo = project.targets.map((t) => ({
name: t.name,
productType: t.productType,
productTypeShort: t.productType.split(".").pop() || t.productType,
}));
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
projectName: project.name,
targetCount: targetInfo.length,
targets: targetInfo,
},
null,
2
),
},
],
};
}
async function handleListSchemes(args: { projectPath: string }) {
const projectPath = path.resolve(args.projectPath);
const schemes = await findSchemes(projectPath);
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
projectPath,
schemeCount: schemes.length,
schemes,
},
null,
2
),
},
],
};
}
async function handleBuild(args: {
projectPath: string;
scheme?: string;
target?: string;
configuration?: string;
destination?: string;
clean?: boolean;
}) {
const projectPath = path.resolve(args.projectPath);
const buildArgs: string[] = ["-project", projectPath];
if (args.scheme) {
buildArgs.push("-scheme", args.scheme);
} else if (args.target) {
buildArgs.push("-target", args.target);
}
if (args.configuration) {
buildArgs.push("-configuration", args.configuration);
}
if (args.destination) {
buildArgs.push("-destination", args.destination);
}
// Add action
if (args.clean) {
buildArgs.push("clean", "build");
} else {
buildArgs.push("build");
}
// Add some useful flags
buildArgs.push("-quiet"); // Less verbose output
buildArgs.push("CODE_SIGNING_ALLOWED=NO"); // Skip code signing for faster builds
const result = await runCommand("xcodebuild", buildArgs);
const success = result.exitCode === 0;
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
success,
exitCode: result.exitCode,
command: `xcodebuild ${buildArgs.join(" ")}`,
stdout: result.stdout.slice(-5000), // Last 5KB
stderr: result.stderr.slice(-2000), // Last 2KB
},
null,
2
),
},
],
};
}
async function handleXcodebuildInfo() {
const versionResult = await runCommand("xcodebuild", ["-version"]);
const sdksResult = await runCommand("xcodebuild", ["-showsdks"]);
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
version: versionResult.stdout.trim(),
sdks: sdksResult.stdout.trim(),
},
null,
2
),
},
],
};
}
// Create and configure server
const server = new Server(
{
name: "xcode-mcp-server",
version: "0.1.0",
},
{
capabilities: {
tools: {},
},
}
);
// Register tool list handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools: TOOLS };
});
// Register tool call handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "list-projects":
return await handleListProjects(args as { directory?: string });
case "read-project":
return await handleReadProject(args as { projectPath: string });
case "list-targets":
return await handleListTargets(args as { projectPath: string });
case "list-schemes":
return await handleListSchemes(args as { projectPath: string });
case "build":
return await handleBuild(
args as {
projectPath: string;
scheme?: string;
target?: string;
configuration?: string;
destination?: string;
clean?: boolean;
}
);
case "xcodebuild-info":
return await handleXcodebuildInfo();
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text" as const,
text: JSON.stringify({ error: message }, null, 2),
},
],
isError: true,
};
}
});
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Xcode MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});