Skip to main content
Glama

Xcode MCP Server

by r-huijts
index.ts52.9 kB
import { z } from "zod"; import * as fs from "fs/promises"; import * as fsSync from "fs"; import * as path from "path"; import { promisify } from "util"; import { exec, execFile } from "child_process"; import { XcodeServer } from "../../server.js"; import { getProjectInfo } from "../../utils/project.js"; import { ProjectNotFoundError, PathAccessError, FileOperationError, CommandExecutionError } from "../../utils/errors.js"; const execAsync = promisify(exec); /** * Interface for workspace document */ interface WorkspaceDocument { FileRef: string[]; Group: WorkspaceGroup[]; } /** * Interface for workspace group */ interface WorkspaceGroup { name?: string; FileRef?: string[]; Group?: WorkspaceGroup[]; } /** * Interface for project configuration */ interface ProjectConfiguration { configurations: string[]; schemes: string[]; targets: string[]; buildSettings?: Record<string, any>; defaultConfiguration?: string; workspaceProjects?: string[]; // Projects within a workspace } /** * Check if a file or directory exists */ async function fileExists(filePath: string): Promise<boolean> { try { await fs.access(filePath); return true; } catch { return false; } } /** * Check if a path is a Swift Package Manager project */ async function isSPMProject(directoryPath: string): Promise<boolean> { try { const packageSwiftPath = path.join(directoryPath, "Package.swift"); return await fileExists(packageSwiftPath); } catch { return false; } } /** * Find Xcode projects in a directory */ async function findXcodeProjects(directoryPath: string, includeWorkspaces = true): Promise<string[]> { try { const results: string[] = []; const entries = await fs.readdir(directoryPath, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { const fullPath = path.join(directoryPath, entry.name); if (entry.name.endsWith(".xcodeproj")) { results.push(fullPath); } else if (includeWorkspaces && entry.name.endsWith(".xcworkspace")) { results.push(fullPath); } else if (!entry.name.startsWith(".")) { // Recursively search subdirectories, but avoid hidden dirs try { const subResults = await findXcodeProjects(fullPath, includeWorkspaces); results.push(...subResults); } catch { // Ignore errors from subdirectories we can't access } } } } return results; } catch (error) { throw new Error(`Failed to find Xcode projects: ${error instanceof Error ? error.message : String(error)}`); } } /** * Parse a workspace document to find projects */ async function parseWorkspaceDocument(workspacePath: string): Promise<string[]> { try { const contentsPath = path.join(workspacePath, "contents.xcworkspacedata"); const xmlContent = await fs.readFile(contentsPath, "utf-8"); const projects: string[] = []; // Handle different location tag formats: // 1. Standard format: location group:="path/to/project.xcodeproj" // 2. Alternate format: location group="path/to/project.xcodeproj" // 3. Self-closing format: <FileRef location="group:path/to/project.xcodeproj"/> // 4. Container format: <FileRef location="container:path/to/project.xcodeproj"/> // Pattern 1 & 2: location attribute allowing optional spaces around '=' // Examples: // location="group:MyProj.xcodeproj" // location = "container:MyProj.xcodeproj" const locationRegex = /location\s*=\s*"(?:group|container):([^"]+)"/g; let match; while ((match = locationRegex.exec(xmlContent)) !== null) { const relativePath = match[1]; if (relativePath.endsWith(".xcodeproj")) { // Resolve the path relative to the workspace const absolutePath = path.resolve(path.dirname(workspacePath), relativePath); if (!projects.includes(absolutePath)) { projects.push(absolutePath); } } } // Pattern 3 & 4: FileRef with location attribute // Self-closing FileRef element const fileRefRegex = /<FileRef\s+location\s*=\s*"(?:group|container):([^"]+)"\s*\/>/g; while ((match = fileRefRegex.exec(xmlContent)) !== null) { const relativePath = match[1]; if (relativePath.endsWith(".xcodeproj")) { // Resolve the path relative to the workspace const absolutePath = path.resolve(path.dirname(workspacePath), relativePath); if (!projects.includes(absolutePath)) { projects.push(absolutePath); } } } // Pattern 5: Full XML format with FileRef element // FileRef element with separate closing tag const fileRefXmlRegex = /<FileRef\s+location\s*=\s*"(?:group|container):([^"]+)"\s*>[\s\S]*?<\/FileRef>/g; while ((match = fileRefXmlRegex.exec(xmlContent)) !== null) { const relativePath = match[1]; if (relativePath.endsWith(".xcodeproj")) { // Resolve the path relative to the workspace const absolutePath = path.resolve(path.dirname(workspacePath), relativePath); if (!projects.includes(absolutePath)) { projects.push(absolutePath); } } } return projects; } catch (error) { throw new Error(`Failed to parse workspace: ${error instanceof Error ? error.message : String(error)}`); } } /** * Get a project's configuration (schemes, targets, configurations) */ async function getProjectConfiguration(projectPath: string): Promise<ProjectConfiguration> { try { const result: ProjectConfiguration = { configurations: [], schemes: [], targets: [] }; // Get project schemes try { // Determine if this is a workspace or regular project const isWorkspace = projectPath.endsWith('.xcworkspace'); const flag = isWorkspace ? '-workspace' : '-project'; const { stdout: schemesOutput } = await execAsync(`xcodebuild ${flag} "${projectPath}" -list`); // Parse schemes const schemesMatch = schemesOutput.match(/Schemes:\s+((?:.+\s*)+)/); if (schemesMatch && schemesMatch[1]) { result.schemes = schemesMatch[1].trim().split(/\s+/); } // Parse targets const targetsMatch = schemesOutput.match(/Targets:\s+((?:.+\s*)+)/); if (targetsMatch && targetsMatch[1]) { result.targets = targetsMatch[1].trim().split(/\s+/); } // Parse configurations const configsMatch = schemesOutput.match(/Build Configurations:\s+((?:.+\s*)+)/); if (configsMatch && configsMatch[1]) { result.configurations = configsMatch[1].trim().split(/\s+/); } // Get default configuration const defaultConfigMatch = schemesOutput.match(/If no build configuration is specified and -scheme is not passed then "([^"]+)" is used/); if (defaultConfigMatch && defaultConfigMatch[1]) { result.defaultConfiguration = defaultConfigMatch[1]; } } catch (error) { console.error(`Failed to get project schemes: ${error instanceof Error ? error.message : String(error)}`); } return result; } catch (error) { throw new Error(`Failed to get project configuration: ${error instanceof Error ? error.message : String(error)}`); } } /** * Register project management tools */ export function registerProjectTools(server: XcodeServer) { // Register "set_projects_base_dir" server.server.tool( "set_projects_base_dir", "Sets the base directory where your Xcode projects are stored.", { baseDir: z.string().describe("Path to the directory containing your Xcode projects. Supports ~ for home directory and environment variables.") }, async ({ baseDir }, _extra) => { try { // Use our PathManager to expand and validate the path const expandedPath = server.pathManager.expandPath(baseDir); const stats = await fs.stat(expandedPath); if (!stats.isDirectory()) { throw new Error("Provided baseDir is not a directory"); } // Update both the server config and PathManager server.config.projectsBaseDir = expandedPath; server.pathManager.setProjectsBaseDir(expandedPath); await server.detectActiveProject().catch(console.error); return { content: [{ type: "text" as const, text: `Projects base directory set to: ${expandedPath}` }] }; } catch (error) { throw new Error(`Failed to set projects base directory: ${error instanceof Error ? error.message : String(error)}`); } } ); // Register "set_project_path" server.server.tool( "set_project_path", "Sets the active Xcode project by specifying the path to its .xcodeproj directory.", { projectPath: z.string().describe("Path to the .xcodeproj directory for the desired project. Supports ~ for home directory and environment variables."), setActiveDirectory: z.boolean().optional().describe("If true, also set the active directory to the project directory"), openInXcode: z.boolean().optional().describe("If true, also open the project in Xcode (default: false)") }, async ({ projectPath, setActiveDirectory = true, openInXcode = false }, _extra) => { try { // IMPORTANT: Always expand the tilde first, before any other path operations const expandedPath = server.pathManager.expandPath(projectPath); // Then handle relative paths if needed const fullPath = path.isAbsolute(expandedPath) ? expandedPath : server.directoryState.resolvePath(expandedPath); // Now validate the path const validatedPath = server.pathManager.validatePathForReading(fullPath); // Clean up the path if it ends with project.xcworkspace let cleanedPath = validatedPath; if (cleanedPath.endsWith('/project.xcworkspace')) { cleanedPath = cleanedPath.replace('/project.xcworkspace', ''); } // Check if the path exists const stats = await fs.stat(cleanedPath); let isWorkspace = false; let isSPMProject = false; let projectType = "standard"; // Check project type if (cleanedPath.endsWith(".xcworkspace")) { isWorkspace = true; projectType = "workspace"; } else if (cleanedPath.endsWith(".xcodeproj")) { // Standard Xcode project } else { // Check if it's a SPM project with Package.swift const packageSwiftPath = path.join(cleanedPath, "Package.swift"); const isSPM = await fileExists(packageSwiftPath); if (isSPM) { isSPMProject = true; projectType = "spm"; } else { throw new Error("Invalid project path; must be a .xcodeproj directory, .xcworkspace, or a directory with Package.swift"); } } // Create the project object const projectObj = { path: cleanedPath, name: path.basename(cleanedPath, path.extname(cleanedPath)), isWorkspace, isSPMProject, type: projectType as 'standard' | 'workspace' | 'spm' }; // Use our setActiveProject method which updates PathManager server.setActiveProject(projectObj); // Set active directory to project directory if requested if (setActiveDirectory) { const projectDir = path.dirname(cleanedPath); server.directoryState.setActiveDirectory(projectDir); } // Open the project in Xcode if requested let xcodeOpenStatus = ""; if (openInXcode) { try { // Use AppleScript to tell Xcode to open the project const { promisify } = await import('util'); const { execFile } = await import('child_process'); const execFileAsync = promisify(execFile); await execFileAsync('osascript', [ '-e', `tell application "Xcode" to open POSIX file "${cleanedPath}"`, '-e', 'activate' ]); xcodeOpenStatus = " and opened in Xcode"; } catch (openError) { console.error("Failed to open project in Xcode:", openError); xcodeOpenStatus = " (failed to open in Xcode)"; } } return { content: [{ type: "text", text: `Active project set to: ${cleanedPath} (${projectType})${xcodeOpenStatus}` }] }; } catch (error) { console.error("Failed to set project path:", error); // Provide more specific error messages based on the error type if (error instanceof PathAccessError) { throw new Error(`Access denied: The specified project path is outside the allowed directories. ${error.message}`); } else if (error instanceof Error && error.message.includes("Invalid project path")) { throw new Error(`Invalid project path: The path must point to a .xcodeproj directory, .xcworkspace, or a directory with Package.swift. ` + `Please check the path and try again.`); } else if (error instanceof Error && error.message.includes("ENOENT")) { throw new Error(`Project not found: The specified path does not exist. Please check the path and try again.`); } else { throw new Error(`Failed to set project path: ${error instanceof Error ? error.message : String(error)}`); } } } ); // Register "get_active_project" server.server.tool( "get_active_project", "Retrieves detailed information about the currently active Xcode project.", { detailed: z.boolean().optional().describe("If true, include additional detailed project information") }, async ({ detailed = false }) => { try { if (!server.activeProject) { await server.detectActiveProject(); } if (!server.activeProject) { return { content: [{ type: "text" as const, text: "No active Xcode project detected." }] }; } // Get basic project info const projectInfo = await getProjectInfo(server.activeProject.path); // Include current active directory from ProjectDirectoryState const activeDirectory = server.directoryState.getActiveDirectory(); let infoWithActiveDir: any = { ...server.activeProject, ...projectInfo, activeDirectory }; // If detailed is requested, get additional project information if (detailed) { try { // Add configuration, schemes, and targets if (server.activeProject.isWorkspace) { // For workspaces, try to find the main project const projects = await parseWorkspaceDocument(server.activeProject.path); if (projects.length > 0) { const mainProject = projects[0]; // Use the first project as main infoWithActiveDir.projects = projects; infoWithActiveDir.mainProject = mainProject; // Get configurations for the main project const config = await getProjectConfiguration(mainProject); infoWithActiveDir.configurations = config.configurations; infoWithActiveDir.schemes = config.schemes; infoWithActiveDir.targets = config.targets; infoWithActiveDir.defaultConfiguration = config.defaultConfiguration; } } else if (server.activeProject.isSPMProject) { // For SPM projects, we don't have traditional Xcode schemes infoWithActiveDir.configurations = ["debug", "release"]; } else { // For standard Xcode projects const config = await getProjectConfiguration(server.activeProject.path); infoWithActiveDir.configurations = config.configurations; infoWithActiveDir.schemes = config.schemes; infoWithActiveDir.targets = config.targets; infoWithActiveDir.defaultConfiguration = config.defaultConfiguration; } } catch (error) { console.error(`Error getting detailed project info: ${error instanceof Error ? error.message : String(error)}`); infoWithActiveDir.detailedInfoError = String(error); } } return { content: [{ type: "text" as const, text: JSON.stringify(infoWithActiveDir, null, 2) }] }; } catch (error) { throw new Error(`Failed to get active project: ${error instanceof Error ? error.message : String(error)}`); } } ); // Register "find_projects" server.server.tool( "find_projects", "Finds Xcode projects in the specified directory.", { directory: z.string().optional().describe("Directory to search in. Defaults to projects base directory."), includeWorkspaces: z.boolean().optional().describe("If true, include .xcworkspace files"), includeSPM: z.boolean().optional().describe("If true, include Swift Package Manager projects") }, async ({ directory, includeWorkspaces = true, includeSPM = false }) => { try { // Use projects base dir if no directory specified let searchDir: string; if (directory) { // First, expand any tilde in the path const expandedDir = server.pathManager.expandPath(directory); // Then resolve relative to active directory if needed searchDir = server.directoryState.resolvePath(expandedDir); } else { const baseDir = server.pathManager.getProjectsBaseDir(); if (!baseDir) { throw new Error("No projects base directory set"); } searchDir = baseDir; } server.pathManager.validatePathForReading(searchDir); // Find Xcode projects const projects = await findXcodeProjects(searchDir, includeWorkspaces); // Find Swift Package Manager projects if requested let spmProjects: string[] = []; if (includeSPM) { try { // Find directories containing Package.swift const findCmd = `find "${searchDir}" -name "Package.swift" -type f -not -path "*/\\.*/"`; const { stdout: spmOutput } = await execAsync(findCmd); if (spmOutput.trim()) { spmProjects = spmOutput.trim().split('\n') .filter(Boolean) .map(packagePath => path.dirname(packagePath)); } } catch (error) { console.error(`Error finding SPM projects: ${error instanceof Error ? error.message : String(error)}`); } } // Format results const projectInfos = await Promise.all( projects.map(async projectPath => { try { const isWorkspace = projectPath.endsWith(".xcworkspace"); let containedProjects: string[] = []; if (isWorkspace) { try { containedProjects = await parseWorkspaceDocument(projectPath); } catch { // Ignore errors parsing workspace } } return { path: projectPath, name: path.basename(projectPath, path.extname(projectPath)), type: isWorkspace ? "workspace" : "xcodeproj", containedProjects: isWorkspace ? containedProjects : [] }; } catch { // Return minimal info if error return { path: projectPath, name: path.basename(projectPath, path.extname(projectPath)), type: projectPath.endsWith(".xcworkspace") ? "workspace" : "xcodeproj" }; } }) ); // Add SPM projects if any const spmInfos = spmProjects.map(spmPath => ({ path: spmPath, name: path.basename(spmPath), type: "spm" })); const allProjects = [...projectInfos, ...spmInfos]; return { content: [{ type: "text", text: JSON.stringify(allProjects, null, 2) }] }; } catch (error) { throw new Error(`Failed to find projects: ${error instanceof Error ? error.message : String(error)}`); } } ); // Register "change_directory" server.server.tool( "change_directory", "Changes the active directory for relative path operations.", { directoryPath: z.string().describe("Path to the directory to set as active. Supports absolute paths, paths relative to the current active directory, and ~ for home directory.") }, async ({ directoryPath }, _extra) => { try { // Expand tilde first, then resolve the path const expandedPath = server.pathManager.expandPath(directoryPath); const resolvedPath = server.directoryState.resolvePath(expandedPath); // Validate the path is within allowed boundaries server.pathManager.validatePathForReading(resolvedPath); // Validate the directory exists const stats = await fs.stat(resolvedPath); if (!stats.isDirectory()) { throw new Error("Path is not a directory"); } // Set as active directory server.directoryState.setActiveDirectory(resolvedPath); return { content: [{ type: "text" as const, text: `Active directory changed to: ${resolvedPath}` }] }; } catch (error) { if (error instanceof PathAccessError) { throw new Error(`Access denied: ${error.message}`); } else if (error instanceof FileOperationError) { throw new Error(`File operation error: ${error.message}`); } else { throw new Error(`Failed to change directory: ${error instanceof Error ? error.message : String(error)}`); } } } ); // Register "push_directory" server.server.tool( "push_directory", "Pushes the current directory onto a stack and changes to a new directory.", { directoryPath: z.string().describe("Path to the directory to set as active. Supports absolute paths, paths relative to the current active directory, and ~ for home directory.") }, async ({ directoryPath }, _extra) => { try { // Expand tilde first, then resolve the path const expandedPath = server.pathManager.expandPath(directoryPath); const resolvedPath = server.directoryState.resolvePath(expandedPath); // Validate the path is within allowed boundaries server.pathManager.validatePathForReading(resolvedPath); // Validate the directory exists const stats = await fs.stat(resolvedPath); if (!stats.isDirectory()) { throw new Error("Path is not a directory"); } // Push onto stack and change server.directoryState.pushDirectory(resolvedPath); return { content: [{ type: "text" as const, text: `Directory stack pushed, active directory changed to: ${resolvedPath}` }] }; } catch (error) { if (error instanceof PathAccessError) { throw new Error(`Access denied: ${error.message}`); } else if (error instanceof FileOperationError) { throw new Error(`File operation error: ${error.message}`); } else { throw new Error(`Failed to push directory: ${error instanceof Error ? error.message : String(error)}`); } } } ); // Register "pop_directory" server.server.tool( "pop_directory", "Pops a directory from the stack and changes to it.", {}, async () => { try { const previousDir = server.directoryState.popDirectory(); if (!previousDir) { return { content: [{ type: "text" as const, text: "Directory stack is empty, no directory to pop." }] }; } return { content: [{ type: "text" as const, text: `Directory popped, active directory changed to: ${previousDir}` }] }; } catch (error) { throw new Error(`Failed to pop directory: ${error instanceof Error ? error.message : String(error)}`); } } ); // Register "get_current_directory" server.server.tool( "get_current_directory", "Returns the current active directory.", {}, async () => { try { const activeDirectory = server.directoryState.getActiveDirectory(); return { content: [{ type: "text" as const, text: activeDirectory }] }; } catch (error) { throw new Error(`Failed to get current directory: ${error instanceof Error ? error.message : String(error)}`); } } ); // Register "get_project_configuration" server.server.tool( "get_project_configuration", "Retrieves configuration details for the active project, including schemes and targets.", {}, async () => { try { if (!server.activeProject) { throw new ProjectNotFoundError(); } const projectPath = server.activeProject.path; let configuration: ProjectConfiguration; if (server.activeProject.isWorkspace) { // For workspaces, try to find the main project const projects = await parseWorkspaceDocument(projectPath); if (projects.length === 0) { throw new Error("No projects found in workspace"); } const mainProject = projects[0]; // Use the first project as main configuration = await getProjectConfiguration(mainProject); configuration.workspaceProjects = projects; } else if (server.activeProject.isSPMProject) { // For SPM projects, provide basic configuration configuration = { configurations: ["debug", "release"], schemes: ["all"], targets: ["all"], defaultConfiguration: "debug" }; } else { // Standard Xcode project configuration = await getProjectConfiguration(projectPath); } return { content: [{ type: "text", text: JSON.stringify(configuration, null, 2) }] }; } catch (error) { if (error instanceof ProjectNotFoundError) { throw new Error("No active project set."); } else { throw new Error(`Failed to get project configuration: ${error instanceof Error ? error.message : String(error)}`); } } } ); // Register "detect_active_project" server.server.tool( "detect_active_project", "Attempts to automatically detect the active Xcode project.", { forceRedetect: z.boolean().optional().describe("If true, always try to detect the project even if one is already set (default: false)") }, async ({ forceRedetect = false }, _extra) => { try { // If we already have an active project and aren't forcing a redetect, just return it if (server.activeProject && !forceRedetect) { const projectInfo = await getProjectInfo(server.activeProject.path); return { content: [{ type: "text", text: `Using existing active project: ${server.activeProject.path}\n\n${JSON.stringify({ ...server.activeProject, ...projectInfo }, null, 2)}` }] }; } // Otherwise, try to detect the active project await server.detectActiveProject(); if (!server.activeProject) { return { content: [{ type: "text", text: "Could not detect an active Xcode project. Please set one manually with set_project_path." }] }; } const projectInfo = await getProjectInfo(server.activeProject.path); return { content: [{ type: "text", text: `Detected active project: ${server.activeProject.path}\n\n${JSON.stringify({ ...server.activeProject, ...projectInfo }, null, 2)}` }] }; } catch (error) { throw new Error(`Failed to detect active project: ${error instanceof Error ? error.message : String(error)}`); } } ); // Register "add_file_to_project" server.server.tool( "add_file_to_project", "Adds a file to the active Xcode project.", { filePath: z.string().describe("Path to the file to add to the project"), targetName: z.string().optional().describe("Name of the target to add the file to. If not provided, will try to add to the first target."), group: z.string().optional().describe("Group path within the project to add the file to (e.g., 'MyApp/Models'). If not provided, will add to the root group."), createGroups: z.boolean().optional().describe("Whether to create intermediate groups if they don't exist (default: true)") }, async ({ filePath, targetName, group, createGroups = true }) => { try { if (!server.activeProject) { throw new ProjectNotFoundError(); } // Validate and resolve the file path const expandedFilePath = server.pathManager.expandPath(filePath); const resolvedFilePath = server.directoryState.resolvePath(expandedFilePath); server.pathManager.validatePathForReading(resolvedFilePath); // Check if the file exists try { const stats = await fs.stat(resolvedFilePath); if (!stats.isFile()) { throw new Error(`Path is not a file: ${filePath}`); } } catch (error) { throw new Error(`File not found: ${filePath}`); } // Check if the active project is a workspace or a standard project if (server.activeProject.isWorkspace) { throw new Error("Adding files directly to a workspace is not supported. Please set a specific project as active."); } // Check if the active project is an SPM project if (server.activeProject.isSPMProject) { throw new Error("Adding files directly to a Swift Package Manager project is not supported. Please modify the Package.swift file instead."); } // Get the project directory const projectDir = path.dirname(server.activeProject.path); // Make the file path relative to the project directory if it's not already const relativeFilePath = path.isAbsolute(resolvedFilePath) && resolvedFilePath.startsWith(projectDir) ? path.relative(projectDir, resolvedFilePath) : resolvedFilePath; // Build the command to add the file to the project let cmd = `cd "${projectDir}" && xcrun swift package generate-xcodeproj`; // For standard Xcode projects, we need to use a different approach // Since there's no direct CLI for adding files to Xcode projects, we'll use a script // Create a temporary AppleScript file to add the file to the project const tempScriptPath = path.join(projectDir, "temp_add_file.scpt"); // Build the AppleScript content let scriptContent = ` tell application "Xcode" open "${server.activeProject.path}" set mainWindow to window 1 -- Wait for the project to load delay 1 -- Get the project document set projectDocument to document 1 -- Add the file to the project tell projectDocument -- Add the file set theFile to POSIX file "${resolvedFilePath}" `; // Add target specification if provided if (targetName) { scriptContent += ` -- Add to specific target set targetList to targets of projectDocument set foundTarget to false repeat with aTarget in targetList if name of aTarget is "${targetName}" then add files theFile to aTarget set foundTarget to true exit repeat end if end repeat if not foundTarget then error "Target '${targetName}' not found in project" end if `; } else { scriptContent += ` -- Add to first target set targetList to targets of projectDocument if (count of targetList) > 0 then add files theFile to item 1 of targetList end if `; } // Add group specification if provided if (group) { scriptContent += ` -- Add to specific group set groupPath to "${group}" set groupComponents to my splitString(groupPath, "/") set currentGroup to main group of projectDocument repeat with groupName in groupComponents set foundGroup to false set childGroups to groups of currentGroup repeat with childGroup in childGroups if name of childGroup is groupName then set currentGroup to childGroup set foundGroup to true exit repeat end if end repeat if not foundGroup then if ${createGroups} then -- Create the group if it doesn't exist set currentGroup to make new group with properties {name:groupName} at end of groups of currentGroup else error "Group '" & groupName & "' not found in path '" & groupPath & "'" end if end if end repeat -- Move the file to the target group move item 1 of files of main group of projectDocument to end of files of currentGroup `; } // Close the script scriptContent += ` -- Save the project save projectDocument end tell end tell -- Helper function to split a string on splitString(theString, theDelimiter) set oldDelimiters to AppleScript's text item delimiters set AppleScript's text item delimiters to theDelimiter set theArray to every text item of theString set AppleScript's text item delimiters to oldDelimiters return theArray end splitString `; // Write the script to a temporary file await fs.writeFile(tempScriptPath, scriptContent, "utf-8"); try { // Execute the AppleScript const { stdout, stderr } = await execAsync(`osascript "${tempScriptPath}"`); // Clean up the temporary script file await fs.unlink(tempScriptPath).catch(() => {}); return { content: [{ type: "text", text: `Successfully added file to project:\n` + `File: ${resolvedFilePath}\n` + `Project: ${server.activeProject.path}\n` + (targetName ? `Target: ${targetName}\n` : "") + (group ? `Group: ${group}\n` : "") + `\n${stdout}${stderr ? '\nError output:\n' + stderr : ''}` }] }; } catch (error) { // Clean up the temporary script file await fs.unlink(tempScriptPath).catch(() => {}); let stderr = ''; if (error instanceof Error && 'stderr' in error) { stderr = (error as any).stderr; } throw new Error( `Failed to add file to project: ${stderr || (error instanceof Error ? error.message : String(error))}` ); } } catch (error) { if (error instanceof ProjectNotFoundError) { throw new Error("No active project set. Use set_project_path to set an active project first."); } else if (error instanceof PathAccessError) { throw new Error(`Access denied: ${error.message}`); } else { throw new Error(`Failed to add file to project: ${error instanceof Error ? error.message : String(error)}`); } } } ); // Register "create_workspace" server.server.tool( "create_workspace", "Creates a new Xcode workspace and optionally adds existing projects to it.", { name: z.string().describe("Name of the workspace to create"), outputDirectory: z.string().describe("Directory where the workspace will be created"), projects: z.array(z.string()).optional().describe("Optional array of project paths to add to the workspace"), setAsActive: z.boolean().optional().describe("Whether to set the new workspace as the active project (default: true)") }, async ({ name, outputDirectory, projects = [], setAsActive = true }) => { try { // Validate and resolve the output directory const expandedOutputDir = server.pathManager.expandPath(outputDirectory); const resolvedOutputDir = server.directoryState.resolvePath(expandedOutputDir); server.pathManager.validatePathForWriting(resolvedOutputDir); // Create the output directory if it doesn't exist await fs.mkdir(resolvedOutputDir, { recursive: true }); // Create the workspace directory const workspaceName = name.endsWith('.xcworkspace') ? name : `${name}.xcworkspace`; const workspacePath = path.join(resolvedOutputDir, workspaceName); await fs.mkdir(workspacePath, { recursive: true }); // Create the contents.xcworkspacedata file const contentsPath = path.join(workspacePath, 'contents.xcworkspacedata'); // Start with the basic XML structure let contentsXml = '<?xml version="1.0" encoding="UTF-8"?>\n'; contentsXml += '<Workspace version="1.0">\n'; // Add projects if provided for (const projectPath of projects) { // Validate and resolve each project path const expandedProjectPath = server.pathManager.expandPath(projectPath); const resolvedProjectPath = server.directoryState.resolvePath(expandedProjectPath); server.pathManager.validatePathForReading(resolvedProjectPath); // Check if the project exists try { const stats = await fs.stat(resolvedProjectPath); if (!stats.isDirectory()) { console.error(`Warning: Project path is not a directory: ${resolvedProjectPath}`); continue; } } catch (error) { console.error(`Warning: Cannot access project path: ${resolvedProjectPath}`); continue; } // Make the project path relative to the workspace const relativeProjectPath = path.relative(path.dirname(workspacePath), resolvedProjectPath); // Add the project reference to the workspace contentsXml += ` <FileRef location="group:${relativeProjectPath}"></FileRef>\n`; } // Close the XML structure contentsXml += '</Workspace>\n'; // Write the contents file await fs.writeFile(contentsPath, contentsXml, 'utf-8'); // Set as active project if requested if (setAsActive) { const projectObj = { path: workspacePath, name: path.basename(workspacePath, '.xcworkspace'), isWorkspace: true, isSPMProject: false, type: "workspace" as 'standard' | 'workspace' | 'spm' }; server.setActiveProject(projectObj); server.directoryState.setActiveDirectory(resolvedOutputDir); } return { content: [{ type: "text", text: `Created new Xcode workspace at: ${workspacePath}\n` + (projects.length > 0 ? `Added ${projects.length} project(s) to the workspace.\n` : '') }] }; } catch (error) { if (error instanceof PathAccessError) { throw new Error(`Access denied: ${error.message}`); } else { throw new Error(`Failed to create workspace: ${error instanceof Error ? error.message : String(error)}`); } } } ); // Register "add_project_to_workspace" server.server.tool( "add_project_to_workspace", "Adds an existing project to the active workspace.", { projectPath: z.string().describe("Path to the project to add to the workspace"), workspacePath: z.string().optional().describe("Path to the workspace. If not provided, uses the active workspace.") }, async ({ projectPath, workspacePath }) => { try { // Determine which workspace to use let resolvedWorkspacePath: string; if (workspacePath) { // Use the provided workspace path const expandedWorkspacePath = server.pathManager.expandPath(workspacePath); resolvedWorkspacePath = server.directoryState.resolvePath(expandedWorkspacePath); server.pathManager.validatePathForWriting(resolvedWorkspacePath); } else if (server.activeProject && server.activeProject.isWorkspace) { // Use the active workspace resolvedWorkspacePath = server.activeProject.path; } else { throw new Error("No active workspace set. Please provide a workspace path or set an active workspace first."); } // Validate and resolve the project path const expandedProjectPath = server.pathManager.expandPath(projectPath); const resolvedProjectPath = server.directoryState.resolvePath(expandedProjectPath); server.pathManager.validatePathForReading(resolvedProjectPath); // Check if the project exists try { const stats = await fs.stat(resolvedProjectPath); if (!stats.isDirectory()) { throw new Error(`Project path is not a directory: ${resolvedProjectPath}`); } } catch (error) { throw new Error(`Cannot access project path: ${resolvedProjectPath}`); } // Check if the workspace exists const contentsPath = path.join(resolvedWorkspacePath, 'contents.xcworkspacedata'); let contentsXml: string; try { contentsXml = await fs.readFile(contentsPath, 'utf-8'); } catch (error) { throw new Error(`Cannot access workspace contents file: ${contentsPath}`); } // Make the project path relative to the workspace const relativeProjectPath = path.relative(path.dirname(resolvedWorkspacePath), resolvedProjectPath); // Check if the project is already in the workspace const escapedPath = relativeProjectPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const duplicateRegex = new RegExp(`<FileRef\\s+location\\s*=\\s*\"(?:group|container):${escapedPath}\"`); if (duplicateRegex.test(contentsXml)) { return { content: [{ type: "text", text: `Project is already in the workspace: ${resolvedProjectPath}` }] }; } // Add the project reference to the workspace const insertPoint = contentsXml.lastIndexOf('</Workspace>'); if (insertPoint === -1) { throw new Error("Invalid workspace file format"); } const newContentsXml = contentsXml.substring(0, insertPoint) + ` <FileRef location="group:${relativeProjectPath}"></FileRef>\n` + contentsXml.substring(insertPoint); // Write the updated contents file await fs.writeFile(contentsPath, newContentsXml, 'utf-8'); return { content: [{ type: "text", text: `Added project to workspace:\n` + `Project: ${resolvedProjectPath}\n` + `Workspace: ${resolvedWorkspacePath}` }] }; } catch (error) { if (error instanceof PathAccessError) { throw new Error(`Access denied: ${error.message}`); } else { throw new Error(`Failed to add project to workspace: ${error instanceof Error ? error.message : String(error)}`); } } } ); // Register "create_xcode_project" server.server.tool( "create_xcode_project", "Creates a new Xcode project using a template.", { name: z.string().describe("Name of the project to create"), template: z.enum([ "ios-app", "macos-app", "ios-framework", "macos-framework", "watchos-app", "tvos-app", "ios-game", "macos-game", "cross-platform-framework", "cross-platform-library" ]).describe("Template to use for the project"), outputDirectory: z.string().describe("Directory where the project will be created"), organizationName: z.string().optional().describe("Organization name to use in the project"), organizationIdentifier: z.string().optional().describe("Organization identifier (e.g., 'com.example') to use in the project"), language: z.enum(["swift", "objc"]).optional().describe("Programming language to use (default: swift)"), includeTests: z.boolean().optional().describe("Whether to include unit tests (default: true)"), includeUITests: z.boolean().optional().describe("Whether to include UI tests (default: false)"), setAsActive: z.boolean().optional().describe("Whether to set the new project as the active project (default: true)") }, async ({ name, template, outputDirectory, organizationName, organizationIdentifier, language = "swift", includeTests = true, includeUITests = false, setAsActive = true }) => { try { // Validate and resolve the output directory const expandedOutputDir = server.pathManager.expandPath(outputDirectory); const resolvedOutputDir = server.directoryState.resolvePath(expandedOutputDir); server.pathManager.validatePathForWriting(resolvedOutputDir); // Create the output directory if it doesn't exist await fs.mkdir(resolvedOutputDir, { recursive: true }); // Determine the Xcode template to use let xcodeTemplate: string; let platformIdentifier: string; switch (template) { case "ios-app": xcodeTemplate = "Single View App"; platformIdentifier = "com.apple.platform.iphoneos"; break; case "macos-app": xcodeTemplate = "Cocoa App"; platformIdentifier = "com.apple.platform.macosx"; break; case "ios-framework": xcodeTemplate = "Framework"; platformIdentifier = "com.apple.platform.iphoneos"; break; case "macos-framework": xcodeTemplate = "Framework"; platformIdentifier = "com.apple.platform.macosx"; break; case "watchos-app": xcodeTemplate = "Watch App"; platformIdentifier = "com.apple.platform.watchos"; break; case "tvos-app": xcodeTemplate = "TV App"; platformIdentifier = "com.apple.platform.appletvos"; break; case "ios-game": xcodeTemplate = "Game"; platformIdentifier = "com.apple.platform.iphoneos"; break; case "macos-game": xcodeTemplate = "Game"; platformIdentifier = "com.apple.platform.macosx"; break; case "cross-platform-framework": xcodeTemplate = "Cross-platform Framework"; platformIdentifier = ""; break; case "cross-platform-library": xcodeTemplate = "Cross-platform Library"; platformIdentifier = ""; break; default: throw new Error(`Unsupported template: ${template}`); } // Build the command to create the project let cmd = `cd "${resolvedOutputDir}" && xcrun swift package init`; // For Swift Package Manager templates, use swift package init if (template === "cross-platform-framework" || template === "cross-platform-library") { const isLibrary = template === "cross-platform-library"; cmd = `cd "${resolvedOutputDir}" && xcrun swift package init --${isLibrary ? 'type library' : 'type framework'}`; if (!includeTests) { cmd += " --no-tests"; } // Execute the command const { stdout, stderr } = await execAsync(cmd); // Generate Xcode project from the Swift package const generateCmd = `cd "${resolvedOutputDir}" && xcrun swift package generate-xcodeproj`; const { stdout: genStdout, stderr: genStderr } = await execAsync(generateCmd); // Get the path to the generated .xcodeproj const projectPath = path.join(resolvedOutputDir, `${name}.xcodeproj`); // Set as active project if requested if (setAsActive && await fileExists(projectPath)) { const projectObj = { path: projectPath, name: name, isWorkspace: false, isSPMProject: true, type: "spm" as 'standard' | 'workspace' | 'spm' }; server.setActiveProject(projectObj); server.directoryState.setActiveDirectory(resolvedOutputDir); } return { content: [{ type: "text", text: `Created new Swift Package project at: ${resolvedOutputDir}\n` + `Generated Xcode project: ${path.join(resolvedOutputDir, `${name}.xcodeproj`)}\n\n` + `Package initialization output:\n${stdout}\n${stderr ? 'Error output:\n' + stderr : ''}\n\n` + `Xcode project generation output:\n${genStdout}\n${genStderr ? 'Error output:\n' + genStderr : ''}` }] }; } // For Xcode templates, use Xcode's template system // Create a temporary directory for the project const projectDir = path.join(resolvedOutputDir, name); await fs.mkdir(projectDir, { recursive: true }); // Build the Xcode project creation command cmd = `cd "${resolvedOutputDir}" && xcrun swift package generate-xcodeproj --output "${name}.xcodeproj"`; // Add organization name if provided const orgNameArg = organizationName ? `--organization "${organizationName}"` : ""; // Add organization identifier if provided const orgIdArg = organizationIdentifier ? `--identifier "${organizationIdentifier}"` : ""; // Add language const langArg = `--language ${language === "swift" ? "Swift" : "Objective-C"}`; // Add test options const testArgs = includeTests ? "" : "--no-tests"; const uiTestArgs = includeUITests ? "--include-ui-tests" : ""; // Execute the command const { stdout, stderr } = await execAsync(cmd); // Get the path to the generated .xcodeproj const projectPath = path.join(resolvedOutputDir, `${name}.xcodeproj`); // Set as active project if requested if (setAsActive && await fileExists(projectPath)) { const projectObj = { path: projectPath, name: name, isWorkspace: false, isSPMProject: false, type: "standard" as 'standard' | 'workspace' | 'spm' }; server.setActiveProject(projectObj); server.directoryState.setActiveDirectory(resolvedOutputDir); } return { content: [{ type: "text", text: `Created new Xcode project at: ${projectPath}\n\n` + `Project creation output:\n${stdout}\n${stderr ? 'Error output:\n' + stderr : ''}` }] }; } catch (error) { if (error instanceof PathAccessError) { throw new Error(`Access denied: ${error.message}`); } else { let stderr = ''; if (error instanceof Error && 'stderr' in error) { stderr = (error as any).stderr; } throw new Error( `Failed to create Xcode project: ${stderr || (error instanceof Error ? error.message : 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