Skip to main content
Glama

Xcode MCP Server

by r-huijts
server.ts15.1 kB
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { exec } from "child_process"; import { promisify } from "util"; import * as fs from "fs/promises"; import * as fsSync from "fs"; import * as path from "path"; import * as os from "os"; import * as dotenv from "dotenv"; import { ServerConfig, ActiveProject } from "./types/index.js"; import { XcodeServerError, ProjectNotFoundError } from "./utils/errors.js"; import { findXcodeProjects, findProjectByName, getProjectInfo } from "./utils/project.js"; // Import our new path management classes import { PathManager } from "./utils/pathManager.js"; import { SafeFileOperations } from "./utils/safeFileOperations.js"; import { ProjectDirectoryState } from "./utils/projectDirectoryState.js"; // Load environment variables from .env file dotenv.config(); const execAsync = promisify(exec); // Tool registration functions import { registerProjectTools } from "./tools/project/index.js"; import { registerFileTools } from "./tools/file/index.js"; import { registerBuildTools } from "./tools/build/index.js"; import { registerCocoaPodsTools } from "./tools/cocoapods/index.js"; import { registerSPMTools } from "./tools/spm/index.js"; import { registerSimulatorTools } from "./tools/simulator/index.js"; import { registerXcodeTools } from "./tools/xcode/index.js"; export class XcodeServer { public server: McpServer; public config: ServerConfig = {}; public activeProject: ActiveProject | null = null; public projectFiles: Map<string, string[]> = new Map(); // Our new path management instances public pathManager: PathManager; public fileOperations: SafeFileOperations; public directoryState: ProjectDirectoryState; constructor(config: ServerConfig = {}) { // Start with default config this.config = { ...this.config, ...config }; // Use environment variable for projects base directory if not explicitly provided if (!this.config.projectsBaseDir && process.env.PROJECTS_BASE_DIR) { this.config.projectsBaseDir = process.env.PROJECTS_BASE_DIR; console.error(`Using projects base directory from env: ${this.config.projectsBaseDir}`); } // If still no projects base directory, try some sensible defaults if (!this.config.projectsBaseDir) { // Common locations for Xcode projects const possibleDirs = [ path.join(os.homedir(), 'Documents'), path.join(os.homedir(), 'Projects'), path.join(os.homedir(), 'Developer'), path.join(os.homedir(), 'Documents/XcodeProjects'), path.join(os.homedir(), 'Documents/Projects') ]; // Use the first directory that exists for (const dir of possibleDirs) { try { if (fsSync.existsSync(dir)) { this.config.projectsBaseDir = dir; console.error(`No projects base directory specified, using default: ${dir}`); break; } } catch (error) { // Ignore errors and try the next directory } } } // Initialize our path management system this.pathManager = new PathManager(this.config); this.fileOperations = new SafeFileOperations(this.pathManager); this.directoryState = new ProjectDirectoryState(this.pathManager); // Create the MCP server this.server = new McpServer({ name: "xcode-server", version: "1.0.0", description: "An MCP server for Xcode integration" }, { capabilities: { tools: {} } }); // Enable debug logging if DEBUG is set if (process.env.DEBUG === "true") { console.error("Debug mode enabled"); } // Register all tools this.registerAllTools(); this.registerResources(); // Attempt to auto-detect an active project with more robust handling this.detectActiveProject() .then(project => { if (project) { console.error(`Successfully detected active project: ${project.name} (${project.path})`); } else { console.error("No active project detected automatically. Use set_project_path to set one."); } }) .catch((error) => { console.error("Error detecting active project:", error.message); }); } private registerAllTools() { // Register tools from each category registerProjectTools(this); registerFileTools(this); registerBuildTools(this); registerCocoaPodsTools(this); registerSPMTools(this); registerSimulatorTools(this); registerXcodeTools(this); } private registerResources() { // Resource to list available Xcode projects. this.server.resource( "xcode-projects", new ResourceTemplate("xcode://projects", { list: undefined }), async () => { const projects = await findXcodeProjects(this.config.projectsBaseDir); return { contents: projects.map(project => ({ uri: `xcode://projects/${encodeURIComponent(project.name)}`, text: project.name, mimeType: "application/x-xcode-project" as const })) }; } ); // Resource to get project details this.server.resource( "xcode-project", new ResourceTemplate("xcode://projects/{name}", { list: undefined }), async (uri, { name }) => { const decodedName = decodeURIComponent(name as string); const project = await findProjectByName(decodedName, this.config.projectsBaseDir); if (!project) { throw new Error(`Project ${decodedName} not found`); } return { contents: [{ uri: uri.href, text: JSON.stringify(project, null, 2), mimeType: "application/json" as const }] }; } ); } /** * Detect an active Xcode project * @returns The detected active project or null if none found */ public async detectActiveProject(): Promise<ActiveProject | null> { try { // Attempt to get the frontmost Xcode project via AppleScript. try { const { stdout: frontmostProject } = await execAsync(` osascript -e ' tell application "Xcode" if it is running then set projectFile to path of document 1 return POSIX path of projectFile end if end tell ' `); if (frontmostProject && frontmostProject.trim()) { const projectPath = frontmostProject.trim(); // Using our new path manager to check boundaries if (this.config.projectsBaseDir && !this.pathManager.isPathWithin(this.config.projectsBaseDir, projectPath)) { console.error("Active project is outside the configured base directory"); } // Clean up path if it's pointing to project.xcworkspace inside an .xcodeproj let cleanedPath = projectPath; if (projectPath.endsWith('/project.xcworkspace')) { cleanedPath = projectPath.replace('/project.xcworkspace', ''); } const isWorkspace = cleanedPath.endsWith('.xcworkspace'); let associatedProjectPath; if (isWorkspace) { try { const { findMainProjectInWorkspace } = await import('./utils/project.js'); associatedProjectPath = await findMainProjectInWorkspace(cleanedPath); } catch (error) { console.error(`Error finding main project in workspace ${cleanedPath}:`, error instanceof Error ? error.message : String(error)); // Continue without associatedProjectPath } } this.activeProject = { path: cleanedPath, // Use the cleaned path name: path.basename(cleanedPath, path.extname(cleanedPath)), isWorkspace, associatedProjectPath }; // Update path manager with active project this.pathManager.setActiveProject(cleanedPath); // Set the project root as the active directory const projectRoot = path.dirname(cleanedPath); this.directoryState.setActiveDirectory(projectRoot); return this.activeProject; } } catch (error) { // Just log and continue with fallback methods console.error("Could not detect active Xcode project via AppleScript:", error instanceof Error ? error.message : String(error)); } // Fallback: scan base directory if set. if (this.config.projectsBaseDir) { try { const projects = await findXcodeProjects(this.config.projectsBaseDir); if (projects.length > 0) { const projectStats = await Promise.all( projects.map(async (project) => ({ project, stats: await fs.stat(project.path) })) ); const mostRecent = projectStats.sort((a, b) => b.stats.mtime.getTime() - a.stats.mtime.getTime())[0]; // Clean up path if needed let cleanedPath = mostRecent.project.path; if (cleanedPath.endsWith('/project.xcworkspace')) { cleanedPath = cleanedPath.replace('/project.xcworkspace', ''); // Update the project object to use the cleaned path mostRecent.project.path = cleanedPath; mostRecent.project.name = path.basename(cleanedPath, path.extname(cleanedPath)); } this.activeProject = mostRecent.project; // Update path manager with active project this.pathManager.setActiveProject(cleanedPath); // Set the project root as the active directory const projectRoot = path.dirname(cleanedPath); this.directoryState.setActiveDirectory(projectRoot); return this.activeProject; } } catch (error) { console.error("Error scanning projects directory:", error instanceof Error ? error.message : String(error)); } } // Further fallback: try reading recent projects from Xcode defaults. try { const { stdout: recentProjects } = await execAsync('defaults read com.apple.dt.Xcode IDERecentWorkspaceDocuments || true'); if (recentProjects) { const projectMatch = recentProjects.match(/= \\"([^"]+)"/); if (projectMatch) { const recentProject = projectMatch[1]; // Using our new path manager to check boundaries if (this.config.projectsBaseDir && !this.pathManager.isPathWithin(this.config.projectsBaseDir, recentProject)) { console.error("Recent project is outside the configured base directory"); } // Clean up path if needed let cleanedPath = recentProject; if (cleanedPath.endsWith('/project.xcworkspace')) { cleanedPath = cleanedPath.replace('/project.xcworkspace', ''); } const isWorkspace = cleanedPath.endsWith('.xcworkspace'); let associatedProjectPath; if (isWorkspace) { try { const { findMainProjectInWorkspace } = await import('./utils/project.js'); associatedProjectPath = await findMainProjectInWorkspace(cleanedPath); } catch (error) { console.error(`Error finding main project in workspace ${cleanedPath}:`, error instanceof Error ? error.message : String(error)); // Continue without associatedProjectPath } } this.activeProject = { path: cleanedPath, name: path.basename(cleanedPath, path.extname(cleanedPath)), isWorkspace, associatedProjectPath }; // Update path manager with active project this.pathManager.setActiveProject(cleanedPath); // Set the project root as the active directory const projectRoot = path.dirname(cleanedPath); this.directoryState.setActiveDirectory(projectRoot); return this.activeProject; } } } catch (error) { console.error("Error reading Xcode defaults:", error instanceof Error ? error.message : String(error)); } // If we've tried all methods and found nothing console.error("No active Xcode project found. Please open a project in Xcode or set one explicitly."); return null; } catch (error) { console.error("Error detecting active project:", error instanceof Error ? error.message : String(error)); throw error; } } /** * Set the active project and update path manager */ public setActiveProject(project: ActiveProject): void { // Clean up path if needed if (project.path.endsWith('/project.xcworkspace')) { const cleanedPath = project.path.replace('/project.xcworkspace', ''); // Update the project object to use the cleaned path project.path = cleanedPath; project.name = path.basename(cleanedPath, path.extname(cleanedPath)); } this.activeProject = project; this.pathManager.setActiveProject(project.path); // Set the project root as the active directory const projectRoot = path.dirname(project.path); this.directoryState.setActiveDirectory(projectRoot); } /** * Start the server */ public async start() { try { console.error("Starting Xcode MCP Server..."); console.error("Node.js version:", process.version); console.error("Current working directory:", process.cwd()); console.error("Projects base directory:", this.config.projectsBaseDir || "Not set"); // Check if we can access the projects directory if (this.config.projectsBaseDir) { try { await fs.access(this.config.projectsBaseDir); console.error("Projects directory exists and is accessible"); } catch (err) { console.error("Warning: Cannot access projects directory:", err instanceof Error ? err.message : String(err)); } } // Initialize transport with error handling console.error("Initializing StdioServerTransport..."); const transport = new StdioServerTransport(); // Connect with detailed logging console.error("Connecting to transport..."); await this.server.connect(transport); console.error("Xcode MCP Server started successfully"); } catch (error) { if (error instanceof Error) { console.error("Failed to start server:", error.message); console.error("Error stack:", error.stack); throw new XcodeServerError(`Server initialization failed: ${error.message}`); } console.error("Unknown error starting server:", error); throw new XcodeServerError(`Server initialization failed: ${String(error)}`); } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/r-huijts/xcode-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server