- Overview
- Core Architecture
- Design Principles
- Component Details
- Registration System
- Tool Naming Conventions & Glossary
- Testing Architecture
- Build and Deployment
- Extension Guidelines
- Performance Considerations
- Security Considerations
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.
- 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
-
Initialization
- The
xcodebuildmcpexecutable, as defined inpackage.json, points to the compiledbuild/cli.js(CLI entrypoint fromsrc/cli.ts); the MCP server starts via themcpsubcommand which invokessrc/index.ts. - Sentry initialized for error tracking (optional)
- Version information loaded from
package.json
- The
-
Server Creation
- MCP server created with stdio transport
- Plugin discovery system initialized
-
Plugin Discovery (Build-Time)
- A build-time script (
build-plugins/plugin-discovery.ts) scans thesrc/mcp/tools/andsrc/mcp/resources/directories - It generates
src/core/generated-plugins.tsandsrc/core/generated-resources.tswith 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
- A build-time script (
-
Plugin & Resource Loading (Runtime)
- At runtime,
loadPlugins()andloadResources()use the generated loaders from the previous step - All workflow loaders are executed at startup to register tools
- At runtime,
- If
XCODEBUILDMCP_ENABLED_WORKFLOWSis set, only those workflows (plussession-management) are registered;workflow-discoveryis only auto-included whenXCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY=true
-
Tool Registration
- Discovered tools automatically registered with server using pre-generated maps
- No manual registration or configuration required
- Environment variables control workflow selection behavior
-
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
-
Response Streaming
- Server streams response back to client
- Consistent error handling with
isErrorflag
Tools are self-contained units that export a standardized interface. They don't know about the server implementation, ensuring loose coupling and high testability.
- Most utilities are stateless pure functions
- Stateful components (e.g., process tracking) isolated in specific tool modules
- Clear separation between computation and side effects
- Version from
package.jsondrives all version references - Tool directory structure is authoritative tool source
- Environment variables provide consistent configuration interface
- Experimental features behind environment flags
- Optional dependencies (Sentry, xcodemake) gracefully degrade
- Tool directory structure enables workflow-specific organization
- TypeScript strict mode enabled
- Zod schemas for runtime validation
- Generic type constraints ensure compile-time safety
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/executionclearly 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
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):
'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.
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
Standalone doctor tool for:
- Environment validation
- Dependency checking
- Configuration verification
- Troubleshooting assistance
MCP server wrapper providing:
- Server instance creation
- stdio transport configuration
- Request/response handling
- Error boundary implementation
Runtime plugin loading system that leverages build-time generated code:
- Uses
WORKFLOW_LOADERSandWORKFLOW_METADATAmaps from the generatedsrc/core/generated-plugins.tsfile loadWorkflowGroups()iterates through the loaders, dynamically importing each workflow module usingawait loader()- Validates that each imported module contains the required
workflowmetadata export - Aggregates all tools from the loaded workflows into a single map
- This system eliminates runtime file system scanning, providing significant startup performance boost
Plugin type definitions:
PluginMetainterface for plugin structureWorkflowMetainterface for workflow metadataWorkflowGroupinterface for directory organization
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):
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
someToolLogicfunction 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
### Debugger Subsystem
The debugging workflow relies on a long-lived, interactive LLDB subprocess. A `DebuggerManager` owns the session lifecycle and routes tool calls to a backend implementation. The default backend is the LLDB CLI (`xcrun lldb --no-lldbinit`) and configures a unique prompt sentinel to safely read command results. A stub DAP backend exists for future expansion.
Key elements:
- **Interactive execution**: Uses a dedicated interactive spawner with `stdin: 'pipe'` so LLDB commands can be streamed across multiple tool calls.
- **Session manager**: Tracks debug session metadata (session id, simulator id, pid, timestamps) and maintains a “current” session.
- **Backend abstraction**: `DebuggerBackend` keeps the tool contract stable while allowing future DAP support.
### 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](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
}
Resources can reuse existing tool logic for consistency:
// 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();
},
};XcodeBuildMCP registers tools at startup using the generated workflow loaders. Tool selection can be narrowed using the XCODEBUILDMCP_ENABLED_WORKFLOWS environment variable.
- Environment:
XCODEBUILDMCP_ENABLED_WORKFLOWSis not set. - Behavior: All available tools are loaded and registered with the MCP server at startup.
- Use Case: Use this mode when you want the full suite of tools immediately available.
- Environment:
XCODEBUILDMCP_ENABLED_WORKFLOWS=simulator,device,project-discovery(comma-separated) - Behavior: Only tools from the selected workflows are registered, plus the required
session-managementworkflow. - Use Case: Use this mode to reduce tool surface area for focused workflows.
Tools follow a consistent naming pattern to ensure predictability and clarity. Understanding this convention is crucial for both using and developing tools.
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.,
simfor simulator,devfor device,macfor macOS). - specifier: A variant that specifies how the target is identified (e.g.,
idfor UUID,namefor by-name). - projectType: The type of Xcode project the tool operates on (e.g.,
wsfor workspace,projfor 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.
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.
| 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. |
- Test Runner: Vitest 3.x
- Environment: Node.js
- Configuration:
vitest.config.ts - Test Pattern:
*.test.tsfiles alongside implementation
XcodeBuildMCP uses a Dependency Injection (DI) pattern for testing external boundaries (command execution, filesystem, and other side effects). Vitest mocking libraries (vi.mock, vi.fn, etc.) are acceptable for internal collaborators when needed. This keeps tests robust while preserving deterministic behavior at external boundaries.
For detailed guidelines, see the Testing Guide.
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.
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
});
});
});-
Version Generation
npm run build
- Reads version from
package.json - Generates
src/version.ts
- Reads version from
-
Plugin & Resource Loader Generation
- The
build-plugins/plugin-discovery.tsscript is executed - It scans
src/mcp/tools/andsrc/mcp/resources/to find all workflows and resources - It generates
src/core/generated-plugins.tsandsrc/core/generated-resources.tswith dynamic import maps - This eliminates runtime file system scanning and enables code-splitting
- The
-
TypeScript Compilation
tsupcompiles the TypeScript source, including the newly generated files, into JavaScript- Compiles TypeScript with tsup
-
Build Configuration (
tsup.config.ts)- Entry points:
index.ts,doctor-cli.ts - Output format: ESM
- Target: Node 18+
- Source maps enabled
- Entry points:
-
Distribution Structure
build/ ├── index.js # Main server executable ├── doctor-cli.js # Doctor tool └── *.js.map # Source maps
- Name:
xcodebuildmcp - Executables:
xcodebuildmcp→ Main serverxcodebuildmcp-doctor→ Doctor tool
- Dependencies: Minimal runtime dependencies
- Platform: macOS only (due to Xcode requirement)
bundled/
├── axe # UI automation binary
└── Frameworks/ # Facebook device frameworks
├── FBControlCore.framework
├── FBDeviceControl.framework
└── FBSimulatorControl.framework
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.
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.
- 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: Workflow modules are loaded via dynamic imports when registration occurs, 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
- 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
- Process Cleanup: Proper process termination handling
- Log Rotation: Captured logs have size limits
- Resource Disposal: Explicit cleanup in lifecycle hooks
- Use Tool Groups: Enable only needed workflows
- Enable Incremental Builds: Set
INCREMENTAL_BUILDS_ENABLED=true - Limit Log Capture: Use structured logging when possible
- All tool inputs validated with Zod schemas
- Command injection prevented via proper escaping
- Path traversal protection in file operations
- Tools run with user permissions
- No privilege escalation
- Sandboxed execution environment
- Sensitive information scrubbed from errors
- Stack traces limited to application code
- Sentry capture is explicit (
{ sentry: true }) and limited to internal runtime failures - MCP wrapper auto-instrumentation is enabled for MCP observability, with tool input/output capture disabled (
recordInputs: false,recordOutputs: false) - Request/user context and user home paths are scrubbed before telemetry is sent