Skip to main content
Glama

XcodeBuildMCP

ARCHITECTURE.md22.9 kB
# XcodeBuildMCP Architecture ## Table of Contents 1. [Overview](#overview) 2. [Core Architecture](#core-architecture) 3. [Design Principles](#design-principles) 4. [Component Details](#component-details) 5. [Registration System](#registration-system) 6. [Tool Naming Conventions & Glossary](#tool-naming-conventions--glossary) 7. [Testing Architecture](#testing-architecture) 8. [Build and Deployment](#build-and-deployment) 9. [Extension Guidelines](#extension-guidelines) 10. [Performance Considerations](#performance-considerations) 11. [Security Considerations](#security-considerations) ## Overview XcodeBuildMCP is a Model Context Protocol (MCP) server that exposes Xcode operations as tools for AI assistants. The architecture emphasizes modularity, type safety, and selective enablement to support diverse development workflows. ### High-Level Objectives - Expose Xcode-related tools (build, test, deploy, UI automation, etc.) through MCP - Run as a long-lived stdio-based server for LLM agents, CLIs, or editors - Enable fine-grained, opt-in activation of individual tools or tool groups - Support incremental builds via experimental xcodemake with xcodebuild fallback ## Core Architecture ### Runtime Flow 1. **Initialization** - The `xcodebuildmcp` executable, as defined in `package.json`, points to the compiled `build/index.js` which executes the main logic from `src/index.ts`. - Sentry initialized for error tracking (optional) - Version information loaded from `package.json` 2. **Server Creation** - MCP server created with stdio transport - Plugin discovery system initialized 3. **Plugin Discovery (Build-Time)** - A build-time script (`build-plugins/plugin-discovery.ts`) scans the `src/mcp/tools/` and `src/mcp/resources/` directories - It generates `src/core/generated-plugins.ts` and `src/core/generated-resources.ts` with dynamic import maps - This approach improves startup performance by avoiding synchronous file system scans and enables code-splitting - Tool code is only loaded when needed, reducing initial memory footprint 4. **Plugin & Resource Loading (Runtime)** - At runtime, `loadPlugins()` and `loadResources()` use the generated loaders from the previous step - In **Static Mode**, all workflow loaders are executed at startup to register all tools - In **Dynamic Mode**, only the `discover_tools` tool is registered initially - The `enableWorkflows` function in `src/core/dynamic-tools.ts` uses generated loaders to dynamically import and register selected workflow tools on demand 5. **Tool Registration** - Discovered tools automatically registered with server using pre-generated maps - No manual registration or configuration required - Environment variables control dynamic tool discovery behavior 5. **Request Handling** - MCP client calls tool → server routes to tool handler - Zod validates parameters before execution - Tool handler uses shared utilities (build, simctl, etc.) - Returns standardized `ToolResponse` 6. **Response Streaming** - Server streams response back to client - Consistent error handling with `isError` flag ## Design Principles ### 1. **Plugin Autonomy** Tools are self-contained units that export a standardized interface. They don't know about the server implementation, ensuring loose coupling and high testability. ### 2. **Pure Functions vs Stateful Components** - Most utilities are stateless pure functions - Stateful components (e.g., process tracking) isolated in specific tool modules - Clear separation between computation and side effects ### 3. **Single Source of Truth** - Version from `package.json` drives all version references - Tool directory structure is authoritative tool source - Environment variables provide consistent configuration interface ### 4. **Feature Isolation** - Experimental features behind environment flags - Optional dependencies (Sentry, xcodemake) gracefully degrade - Tool directory structure enables workflow-specific organization ### 5. **Type Safety Throughout** - TypeScript strict mode enabled - Zod schemas for runtime validation - Generic type constraints ensure compile-time safety ## Module Organization and Import Strategy ### Focused Facades Pattern XcodeBuildMCP has migrated from a traditional "barrel file" export pattern (`src/utils/index.ts`) to a more structured **focused facades** pattern. Each distinct area of functionality within `src/utils` is exposed through its own `index.ts` file in a dedicated subdirectory. **Example Structure:** ``` src/utils/ ├── execution/ │ └── index.ts # Facade for CommandExecutor, FileSystemExecutor ├── logging/ │ └── index.ts # Facade for the logger ├── responses/ │ └── index.ts # Facade for error types and response creators ├── validation/ │ └── index.ts # Facade for validation utilities ├── axe/ │ └── index.ts # Facade for axe UI automation helpers ├── plugin-registry/ │ └── index.ts # Facade for plugin system utilities ├── xcodemake/ │ └── index.ts # Facade for xcodemake utilities ├── template/ │ └── index.ts # Facade for template management utilities ├── version/ │ └── index.ts # Facade for version information ├── test/ │ └── index.ts # Facade for test utilities ├── log-capture/ │ └── index.ts # Facade for log capture utilities └── index.ts # Deprecated barrel file (legacy/external use only) ``` This approach offers several architectural benefits: - **Clear Dependencies**: It makes the dependency graph explicit. Importing from `utils/execution` clearly indicates a dependency on command execution logic - **Reduced Coupling**: Modules only import the functionality they need, reducing coupling between unrelated utility components - **Prevention of Circular Dependencies**: It's much harder to create circular dependencies, which were a risk with the large barrel file - **Improved Tree-Shaking**: Bundlers can more effectively eliminate unused code - **Performance**: Eliminates loading of unused modules, reducing startup time and memory usage ### ESLint Enforcement To maintain this architecture, an ESLint rule in `eslint.config.js` explicitly forbids importing from the deprecated barrel file within the `src/` directory. **ESLint Rule Snippet** (`eslint.config.js`): ```javascript 'no-restricted-imports': ['error', { patterns: [{ group: ['**/utils/index.js', '../utils/index.js', '../../utils/index.js', '../../../utils/index.js'], message: 'Barrel imports from utils/index.js are prohibited. Use focused facade imports instead (e.g., utils/logging/index.js, utils/execution/index.js).' }] }], ``` This rule prevents regression to the previous barrel import pattern and ensures all new code follows the focused facade architecture. ## Component Details ### Entry Points #### `src/index.ts` Main server entry point responsible for: - Sentry initialization (if enabled) - xcodemake availability check - Server creation and startup - Process lifecycle management (SIGTERM, SIGINT) - Error handling and logging #### `src/doctor-cli.ts` Standalone doctor tool for: - Environment validation - Dependency checking - Configuration verification - Troubleshooting assistance ### Server Layer #### `src/server/server.ts` MCP server wrapper providing: - Server instance creation - stdio transport configuration - Request/response handling - Error boundary implementation ### Tool Discovery System #### `src/core/plugin-registry.ts` Runtime plugin loading system that leverages build-time generated code: - Uses `WORKFLOW_LOADERS` and `WORKFLOW_METADATA` maps from the generated `src/core/generated-plugins.ts` file - `loadWorkflowGroups()` iterates through the loaders, dynamically importing each workflow module using `await loader()` - Validates that each imported module contains the required `workflow` metadata export - Aggregates all tools from the loaded workflows into a single map - This system eliminates runtime file system scanning, providing significant startup performance boost #### `src/core/plugin-types.ts` Plugin type definitions: - `PluginMeta` interface for plugin structure - `WorkflowMeta` interface for workflow metadata - `WorkflowGroup` interface for directory organization ### Tool Implementation Each tool is implemented in TypeScript and follows a standardized pattern that separates the core business logic from the MCP handler boilerplate. This is achieved using the `createTypedTool` factory, which provides compile-time and runtime type safety. **Standard Tool Pattern** (`src/mcp/tools/some-workflow/some_tool.ts`): ```typescript import { z } from 'zod'; import { createTypedTool } from '../../../utils/typed-tool-factory.js'; import type { CommandExecutor } from '../../../utils/execution/index.js'; import { getDefaultCommandExecutor } from '../../../utils/execution/index.js'; import { log } from '../../../utils/logging/index.js'; import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.js'; // 1. Define the Zod schema for parameters const someToolSchema = z.object({ requiredParam: z.string().describe('Description for AI'), optionalParam: z.boolean().optional().describe('Optional parameter'), }); // 2. Infer the parameter type from the schema type SomeToolParams = z.infer<typeof someToolSchema>; // 3. Implement the core logic in a separate, testable function // This function receives strongly-typed parameters and an injected executor. export async function someToolLogic( params: SomeToolParams, executor: CommandExecutor, ): Promise<ToolResponse> { log('info', `Executing some_tool with param: ${params.requiredParam}`); try { const result = await executor(['some', 'command'], 'Some Tool Operation'); if (!result.success) { return createErrorResponse('Operation failed', result.error); } return createTextResponse(`✅ Success: ${result.output}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return createErrorResponse('Tool execution failed', errorMessage); } } // 4. Export the tool definition for auto-discovery export default { name: 'some_tool', description: 'Tool description for AI agents. Example: some_tool({ requiredParam: "value" })', schema: someToolSchema.shape, // Expose shape for MCP SDK // 5. Create the handler using the type-safe factory handler: createTypedTool( someToolSchema, someToolLogic, getDefaultCommandExecutor, ), }; ``` This pattern ensures that: - The `someToolLogic` function is highly testable via dependency injection - Zod handles all runtime parameter validation automatically - The handler is type-safe, preventing unsafe access to parameters - Import paths use focused facades for clear dependency management ``` ### MCP Resources System XcodeBuildMCP provides dual interfaces: traditional MCP tools and efficient MCP resources for supported clients. Resources are located in `src/mcp/resources/` and are automatically discovered **at build time**. The build process generates `src/core/generated-resources.ts`, which contains dynamic loaders for each resource, improving startup performance. For more details on creating resources, see the [Plugin Development Guide](docs/PLUGIN_DEVELOPMENT.md). #### Resource Architecture ``` src/mcp/resources/ ├── simulators.ts # Simulator data resource └── __tests__/ # Resource-specific tests ``` #### Client Capability Detection The system automatically detects client MCP capabilities: ```typescript // src/core/resources.ts export function supportsResources(server?: unknown): boolean { // Detects client capabilities via getClientCapabilities() // Conservative fallback: assumes resource support } ``` #### Resource Implementation Pattern Resources can reuse existing tool logic for consistency: ```typescript // src/mcp/resources/some_resource.ts import { log } from '../../utils/logging/index.js'; import { getDefaultCommandExecutor, CommandExecutor } from '../../utils/execution/index.js'; import { getSomeResourceLogic } from '../tools/some-workflow/get_some_resource.js'; // Testable resource logic separated from MCP handler export async function someResourceResourceLogic( executor: CommandExecutor = getDefaultCommandExecutor(), ): Promise<{ contents: Array<{ text: string }> }> { try { log('info', 'Processing some resource request'); const result = await getSomeResourceLogic({}, executor); if (result.isError) { const errorText = result.content[0]?.text; throw new Error( typeof errorText === 'string' ? errorText : 'Failed to retrieve some resource data', ); } return { contents: [ { text: typeof result.content[0]?.text === 'string' ? result.content[0].text : 'No data for that resource is available', }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); log('error', `Error in some_resource resource handler: ${errorMessage}`); return { contents: [ { text: `Error retrieving resource data: ${errorMessage}`, }, ], }; } } export default { uri: 'xcodebuildmcp://some_resource', name: 'some_resource', description: 'Returns some resource information', mimeType: 'text/plain', async handler(_uri: URL): Promise<{ contents: Array<{ text: string }> }> { return someResourceResourceLogic(); }, }; ``` ## Registration System XcodeBuildMCP supports two primary operating modes for tool registration, controlled by the `XCODEBUILDMCP_DYNAMIC_TOOLS` environment variable. ### Static Mode (Default) - **Environment**: `XCODEBUILDMCP_DYNAMIC_TOOLS` is `false` or not set. - **Behavior**: All available tools are loaded and registered with the MCP server at startup. - **Use Case**: This mode is ideal for environments where the full suite of tools is desired immediately, providing a comprehensive and predictable toolset for the AI assistant. ### Dynamic Mode (AI-Powered Workflow Selection) - **Environment**: `XCODEBUILDMCP_DYNAMIC_TOOLS=true` - **Behavior**: At startup, only the `discover_tools` tool is registered. This tool is designed to analyze a natural language task description from the user. - **Workflow**: 1. The client sends a task description (e.g., "I want to build and test my iOS app") to the `discover_tools` tool. 2. The tool uses the client's LLM via an MCP sampling request to determine the most relevant workflow group (e.g., `simulator-workspace`). 3. The server then dynamically loads and registers all tools from the selected workflow group. 4. The client is notified of the newly available tools. - **Use Case**: This mode is beneficial for conserving the LLM's context window by only loading a relevant subset of tools, leading to more focused and efficient interactions. ## Tool Naming Conventions & Glossary Tools follow a consistent naming pattern to ensure predictability and clarity. Understanding this convention is crucial for both using and developing tools. ### Naming Pattern The standard naming convention for tools is: `{action}_{target}_{specifier}_{projectType}` - **action**: The primary verb describing the tool's function (e.g., `build`, `test`, `get`, `list`). - **target**: The main subject of the action (e.g., `sim` for simulator, `dev` for device, `mac` for macOS). - **specifier**: A variant that specifies *how* the target is identified (e.g., `id` for UUID, `name` for by-name). - **projectType**: The type of Xcode project the tool operates on (e.g., `ws` for workspace, `proj` for project). Not all parts are required for every tool. For example, `swift_package_build` has an action and a target, but no specifier or project type. ### Examples - `build_sim_id_ws`: **Build** for a **simulator** identified by its **ID (UUID)** from a **workspace**. - `test_dev_proj`: **Test** on a **device** from a **project**. - `get_mac_app_path_ws`: **Get** the app path for a **macOS** application from a **workspace**. - `list_sims`: **List** all **simulators**. ### Glossary | Term/Abbreviation | Meaning | Description | |---|---|---| | `ws` | Workspace | Refers to an `.xcworkspace` file. Used for projects with multiple `.xcodeproj` files or dependencies managed by CocoaPods or SPM. | | `proj` | Project | Refers to an `.xcodeproj` file. Used for single-project setups. | | `sim` | Simulator | Refers to the iOS, watchOS, tvOS, or visionOS simulator. | | `dev` | Device | Refers to a physical Apple device (iPhone, iPad, etc.). | | `mac` | macOS | Refers to a native macOS application target. | | `id` | Identifier | Refers to the unique identifier (UUID/UDID) of a simulator or device. | | `name` | Name | Refers to the human-readable name of a simulator (e.g., "iPhone 15 Pro"). | | `cap` | Capture | Used in logging tools, e.g., `start_sim_log_cap`. | ## Testing Architecture ### Framework and Configuration - **Test Runner**: Vitest 3.x - **Environment**: Node.js - **Configuration**: `vitest.config.ts` - **Test Pattern**: `*.test.ts` files alongside implementation ### Testing Principles XcodeBuildMCP uses a strict **Dependency Injection (DI)** pattern for testing, which completely bans the use of traditional mocking libraries like Vitest's `vi.mock` or `vi.fn`. This ensures that tests are robust, maintainable, and verify the actual integration between components. For detailed guidelines, see the [Testing Guide](docs/TESTING.md). ### Test Structure Example Tests inject mock "executors" for external interactions like command-line execution or file system access. This allows for deterministic testing of tool logic without mocking the implementation itself. The project provides helper functions like `createMockExecutor` and `createMockFileSystemExecutor` in `src/test-utils/mock-executors.ts` to facilitate this pattern. ```typescript import { describe, it, expect } from 'vitest'; import { someToolLogic } from '../tool-file.js'; // Import the logic function import { createMockExecutor } from '../../../test-utils/mock-executors.js'; describe('Tool Name', () => { it('should execute successfully', async () => { // 1. Create a mock executor to simulate command-line results const mockExecutor = createMockExecutor({ success: true, output: 'Command output' }); // 2. Call the tool's logic function, injecting the mock executor const result = await someToolLogic({ requiredParam: 'value' }, mockExecutor); // 3. Assert the final result expect(result).toEqual({ content: [{ type: 'text', text: 'Expected output' }], isError: false }); }); }); ``` ## Build and Deployment ### Build Process 1. **Version Generation** ```bash npm run build ``` - Reads version from `package.json` - Generates `src/version.ts` 2. **Plugin & Resource Loader Generation** - The `build-plugins/plugin-discovery.ts` script is executed - It scans `src/mcp/tools/` and `src/mcp/resources/` to find all workflows and resources - It generates `src/core/generated-plugins.ts` and `src/core/generated-resources.ts` with dynamic import maps - This eliminates runtime file system scanning and enables code-splitting 3. **TypeScript Compilation** - `tsup` compiles the TypeScript source, including the newly generated files, into JavaScript - Compiles TypeScript with tsup 4. **Build Configuration** (`tsup.config.ts`) - Entry points: `index.ts`, `doctor-cli.ts` - Output format: ESM - Target: Node 18+ - Source maps enabled 5. **Distribution Structure** ``` build/ ├── index.js # Main server executable ├── doctor-cli.js # Doctor tool └── *.js.map # Source maps ``` ### npm Package - **Name**: `xcodebuildmcp` - **Executables**: - `xcodebuildmcp` → Main server - `xcodebuildmcp-doctor` → Doctor tool - **Dependencies**: Minimal runtime dependencies - **Platform**: macOS only (due to Xcode requirement) ### Bundled Resources ``` bundled/ ├── axe # UI automation binary └── Frameworks/ # Facebook device frameworks ├── FBControlCore.framework ├── FBDeviceControl.framework └── FBSimulatorControl.framework ``` ## Extension Guidelines This project is designed to be extensible. For comprehensive instructions on creating new tools, workflow groups, and resources, please refer to the dedicated [**Plugin Development Guide**](docs/PLUGIN_DEVELOPMENT.md). The guide covers: - The auto-discovery system architecture. - The dependency injection pattern required for all new tools. - How to organize tools into workflow groups. - Testing guidelines and patterns. ## Performance Considerations ### Startup Performance - **Build-Time Plugin Discovery**: The server avoids expensive and slow file system scans at startup by using pre-generated loader maps. This is the single most significant performance optimization - **Code-Splitting**: In Dynamic Mode, tool code is only loaded into memory when its workflow is enabled, reducing the initial memory footprint and parse time - **Focused Facades**: Using targeted imports instead of a large barrel file improves module resolution speed for the Node.js runtime - **Lazy Loading**: Tools only initialized when registered - **Selective Registration**: Fewer tools = faster startup - **Minimal Dependencies**: Fast module resolution ### Runtime Performance - **Stateless Operations**: Most tools complete quickly - **Process Management**: Long-running processes tracked separately - **Incremental Builds**: xcodemake provides significant speedup - **Parallel Execution**: Tools can run concurrently ### Memory Management - **Process Cleanup**: Proper process termination handling - **Log Rotation**: Captured logs have size limits - **Resource Disposal**: Explicit cleanup in lifecycle hooks ### Optimization Strategies 1. **Use Tool Groups**: Enable only needed workflows 2. **Enable Incremental Builds**: Set `INCREMENTAL_BUILDS_ENABLED=true` 3. **Limit Log Capture**: Use structured logging when possible ## Security Considerations ### Input Validation - All tool inputs validated with Zod schemas - Command injection prevented via proper escaping - Path traversal protection in file operations ### Process Isolation - Tools run with user permissions - No privilege escalation - Sandboxed execution environment ### Error Handling - Sensitive information scrubbed from errors - Stack traces limited to application code - Sentry integration respects privacy settings

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/cameroncooke/XcodeBuildMCP'

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