├── .cursor └── environment.json ├── .cursorrules ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml └── workflows │ ├── ci.yml │ └── sentry.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .vscode ├── launch.json └── mcp.json ├── .windsurf └── workflows │ └── build-app.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── TOOL_OPTIONS.md ├── banner.png ├── eslint.config.js ├── example_projects ├── iOS │ ├── .cursor │ │ └── rules │ │ │ └── errors.mdc │ ├── MCPTest.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── MCPTest.xcscheme │ ├── MCPTest │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── ContentView.swift │ │ ├── MCPTestApp.swift │ │ └── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ └── Makefile ├── macOS │ ├── MCPTest.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── MCPTest.xcscheme │ └── MCPTest │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── ContentView.swift │ │ ├── MCPTest.entitlements │ │ ├── MCPTestApp.swift │ │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json └── spm │ ├── .gitignore │ ├── Package.resolved │ ├── Package.swift │ ├── Sources │ ├── TestLib │ │ └── TaskManager.swift │ ├── long-server │ │ └── main.swift │ ├── quick-task │ │ └── main.swift │ └── spm │ │ └── main.swift │ └── Tests │ └── TestLibTests │ └── SimpleTests.swift ├── mcp-install-dark.png ├── package-lock.json ├── package.json ├── release.sh ├── src ├── diagnostic-cli.ts ├── index.ts ├── server │ └── server.ts ├── tools │ ├── app_path.ts │ ├── axe.ts │ ├── build-swift-package.ts │ ├── build_ios_device.ts │ ├── build_ios_simulator.ts │ ├── build_macos.ts │ ├── build_settings.ts │ ├── bundleId.ts │ ├── clean.ts │ ├── common.ts │ ├── diagnostic.ts │ ├── discover_projects.ts │ ├── launch.ts │ ├── log.ts │ ├── run-swift-package.ts │ ├── scaffold.ts │ ├── screenshot.ts │ ├── simulator.ts │ └── test-swift-package.ts ├── types │ └── common.ts └── utils │ ├── axe-setup.ts │ ├── build-utils.ts │ ├── command.ts │ ├── errors.ts │ ├── log_capture.ts │ ├── logger.ts │ ├── register-tools.ts │ ├── sentry.ts │ ├── template-manager.ts │ ├── tool-groups.ts │ ├── validation.ts │ ├── xcode.ts │ └── xcodemake.ts ├── tsconfig.json └── tsup.config.ts /.cursor/environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "agentCanUpdateSnapshot": true 3 | } -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- 1 | # XcodeBuildMCP Rules 2 | 3 | You are an AI assistant helping with the XcodeBuildMCP project - a Model Context Protocol (MCP) server that provides Xcode-related tools for AI assistants and MCP clients. 4 | 5 | ## Project Overview 6 | 7 | - **Project**: XcodeBuildMCP - MCP server for Xcode operations 8 | - **Tech Stack**: TypeScript, Node.js (ES modules), MCP SDK 9 | - **Purpose**: Standardized interface for AI agents to interact with Xcode projects 10 | - **Target Platforms**: macOS, iOS Simulator, iOS Device 11 | 12 | ## Key Technologies & Dependencies 13 | 14 | - **Runtime**: Node.js 18+ with ES modules (`type: "module"`) 15 | - **Language**: TypeScript with strict configuration 16 | - **MCP SDK**: `@modelcontextprotocol/sdk` for protocol implementation 17 | - **Error Monitoring**: Sentry for production error tracking 18 | - **External Tools**: xcodebuild, AXe (UI automation), iOS Simulator 19 | - **Package Management**: npm, supports mise for environment management 20 | 21 | ## Build Commands 22 | 23 | - Build: `npm run build` 24 | - Lint: `npm run lint` 25 | - Fix lint issues: `npm run lint:fix` 26 | - Format code: `npm run format` 27 | - Check formatting: `npm run format:check` 28 | - Run diagnostics: `npm run diagnostic` 29 | - Watch mode for development: `npm run build:watch` 30 | - Launch MCP Inspector for testing: `npm run inspect` 31 | 32 | ## Code Style & Standards 33 | 34 | ### TypeScript Configuration 35 | - Target: ES2022 with Node16 module resolution 36 | - Strict type checking enabled 37 | - Source maps enabled for debugging 38 | - Output directory: `./build` 39 | - Explicit function return types required 40 | - No console.log (console.error allowed for MCP logging) 41 | - Unused variables prefixed with underscore 42 | 43 | ### Code Style Guidelines 44 | - TypeScript with strict typing enabled 45 | - ES Modules (`import`/`export`) syntax 46 | - Prefer explicit imports over wildcard imports 47 | - Group imports: Node.js modules → third-party → local modules 48 | - Name files in kebab-case, variables/functions in camelCase, classes in PascalCase 49 | - Prefer async/await over Promises 50 | - Comment complex logic but avoid redundant comments 51 | - Error handling should use typed errors from utils/errors.ts 52 | 53 | ### Formatting & Linting 54 | - **ESLint**: Use `npm run lint` and `npm run lint:fix` 55 | - **Prettier**: Use `npm run format` and `npm run format:check` 56 | - **Pre-commit**: Always run linting and formatting checks 57 | - Follow existing code patterns and naming conventions 58 | 59 | ## Architecture Patterns 60 | 61 | ### Tool Structure 62 | - Each tool is a separate module in `src/tools/` 63 | - Tools follow MCP tool schema with proper parameter validation 64 | - Use Zod for runtime parameter validation 65 | - Implement proper error handling with descriptive messages 66 | 67 | ### Tool Registration 68 | - Tools are registered in `src/utils/register-tools.ts` 69 | - Use the `registerTool` helper from `src/tools/common.ts` 70 | - Tool registration is managed by the `ToolsetManager` in `src/utils/tool-groups.ts` 71 | - Tool enablement is controlled by `--toolsets` and `--dynamic-toolsets` command-line flags 72 | - When adding a new tool, define its registration function, add it to the `allToolRegistrations` list in `register-tools.ts`, and assign it to relevant `ToolGroup`(s) and a unique `toolKey`. Mark it with `isWriteTool: true` if it modifies state or files 73 | 74 | ### Error Handling 75 | - Use structured error responses with context 76 | - Provide actionable error messages for users 77 | - Log errors appropriately (Sentry integration available) 78 | - Handle both synchronous and asynchronous errors 79 | 80 | ### Async Operations 81 | - All xcodebuild operations are asynchronous 82 | - Use proper async/await patterns 83 | - Handle process spawning and cleanup correctly 84 | - Implement timeouts for long-running operations 85 | 86 | ## Development Guidelines 87 | 88 | ### Adding New Tools 89 | 1. Create tool file in `src/tools/` directory 90 | 2. Define Zod schema for parameters 91 | 3. Implement tool logic with proper error handling 92 | 4. Add tool to tool registry in main server file 93 | 5. Update TOOL_OPTIONS.md if adding configuration options 94 | 6. Test with automated tool calls (preferred) or MCP Inspector if human testing is required 95 | 96 | ### Testing Approach 97 | - Build project: `npm run build` 98 | - Prefer automated testing via direct tool calls over MCP Inspector 99 | - Use MCP Inspector (`npm run inspect`) only when developer/user testing is needed on behalf of the agent 100 | - Verify across different MCP clients (VS Code, Claude Desktop, Cursor) 101 | - Test error scenarios and edge cases 102 | 103 | ### Code Quality 104 | - **Type Safety**: Leverage TypeScript's strict mode fully 105 | - **Parameter Validation**: Always validate tool parameters with Zod 106 | - **Error Messages**: Provide clear, actionable error messages 107 | - **Logging**: Use appropriate log levels and structured logging 108 | 109 | ## Build & Development 110 | 111 | ### Scripts 112 | - `npm run build`: Build with tsup (fast bundler) and set executable permissions 113 | - `npm run build:watch`: Watch mode for development 114 | - `npm run lint`: Check code style 115 | - `npm run format`: Format code 116 | - `npm run inspect`: Launch MCP Inspector for testing 117 | 118 | ### Local Development 119 | - Use VS Code with MCP extension for best development experience 120 | - Configure local server in `.vscode/mcp.json` 121 | - Use debugger with `F5` for step-through debugging 122 | - Enable debug mode with `XCODEBUILDMCP_DEBUG=true` 123 | 124 | ## Platform-Specific Considerations 125 | 126 | ### macOS Integration 127 | - Respect macOS security permissions and sandboxing 128 | - Handle different Xcode installation paths 129 | - Support both Intel and Apple Silicon architectures 130 | 131 | ### iOS Simulator 132 | - Properly manage simulator lifecycle (boot, install, launch) 133 | - Handle simulator state changes gracefully 134 | - Support multiple iOS versions and device types 135 | 136 | ### Xcode Projects 137 | - Support both `.xcodeproj` and `.xcworkspace` files 138 | - Handle complex build configurations 139 | - Respect incremental build capabilities 140 | 141 | ## Documentation 142 | 143 | ### Code Documentation 144 | - Document complex algorithms and business logic 145 | - Include JSDoc comments for public APIs 146 | - Explain non-obvious parameter requirements 147 | - Document error conditions and recovery strategies 148 | 149 | ### User-Facing Documentation 150 | - Keep README.md updated with new features 151 | - Update TOOL_OPTIONS.md for configuration changes 152 | - Include examples for complex usage patterns 153 | 154 | ## Security & Privacy 155 | 156 | ### Data Handling 157 | - Never log sensitive information (API keys, credentials) 158 | - Respect user privacy preferences (Sentry opt-out) 159 | - Handle file paths securely 160 | - Validate all external inputs 161 | 162 | ### Error Reporting 163 | - Sentry integration for production error monitoring 164 | - Allow users to opt-out with `SENTRY_DISABLED=true` 165 | - Sanitize error messages before reporting 166 | 167 | ## Performance Considerations 168 | 169 | ### Resource Management 170 | - Properly clean up spawned processes 171 | - Handle memory usage for large projects 172 | - Implement appropriate timeouts 173 | - Manage temporary files and directories 174 | 175 | ## Testing & Validation 176 | 177 | ### Development Testing Workflow 178 | The XcodeBuildMCP server is available within this development environment as "XcodeBuildMCP" or "xcode-mcp-server-dev" (or similar variations) and exposes all tools for testing. 179 | 180 | **Testing Process for Changes:** 181 | 1. `npm run lint` - Check for linting issues 182 | 2. `npm run format` - Format code consistently 183 | 3. `npm run build` - Build with tsup and update tools 184 | 4. Tools automatically become updated and available for immediate testing 185 | 186 | ### Required Testing 187 | - Build and run the project locally 188 | - Automated testing via tool calling is preferred over MCP Inspector 189 | - Use MCP Inspector (`npm run inspect`) only when end-user testing is needed (requires human intervention) 190 | - Test error scenarios and edge cases 191 | 192 | ### Example Projects for Testing 193 | - Use the example iOS and macOS projects in this workspace (`example_projects/`) to test different tool calls 194 | - These projects provide real Xcode projects for validating build, simulator, and app deployment functionality 195 | - Example projects can be updated with new options/screens/configurations to aid specific testing scenarios 196 | - Test both `.xcodeproj` and `.xcworkspace` project types 197 | 198 | ### CI/CD Considerations 199 | - All changes must pass linting and formatting checks 200 | - No need to maintain backward compatibility unless tool requires significant breaking changes 201 | 202 | ## Common Tasks 203 | 204 | ### Adding a New Tool 205 | 1. Define tool schema with Zod validation 206 | 2. Implement tool logic with error handling 207 | 3. Add to tool registry 208 | 4. Test with MCP Inspector 209 | 5. Update documentation 210 | 6. Add to CHANGELOG.md 211 | 212 | ### Updating Dependencies 213 | 1. Update package.json 214 | 2. Run `npm install` 215 | 3. Test build and functionality 216 | 4. Update any breaking changes 217 | 5. Document changes in CHANGELOG.md 218 | 219 | ### Release Process 220 | 1. Update version in package.json 221 | 2. Update CHANGELOG.md 222 | 3. Run full test suite 223 | 4. Build and verify package 224 | 5. Create release with proper tags 225 | 226 | ## Environment Variables 227 | 228 | ### Development 229 | - `XCODEBUILDMCP_DEBUG=true`: Enable debug mode 230 | - `INCREMENTAL_BUILDS_ENABLED=true`: Enable experimental incremental builds 231 | - `SENTRY_DISABLED=true`: Disable error reporting 232 | 233 | ### Tool Selection 234 | - Use `XCODEBUILDMCP_TOOL_*` and `XCODEBUILDMCP_GROUP_*` for selective tool registration 235 | - See TOOL_OPTIONS.md for complete list 236 | 237 | Remember: Always prioritize user experience, maintain backward compatibility (where applicable), and follow the existing code patterns and conventions in the project. -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: cameroncooke 3 | buy_me_a_coffee: cameroncooke 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a bug or issue with XcodeBuildMCP 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to report an issue with XcodeBuildMCP! 10 | 11 | - type: textarea 12 | id: description 13 | attributes: 14 | label: Bug Description 15 | description: A description of the bug or issue you're experiencing. 16 | placeholder: When trying to build my iOS app using the AI assistant... 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: debug 22 | attributes: 23 | label: Debug Output 24 | description: Output from the diagnostic tool, run `mise x npm:xcodebuildmcp@1.3.5 -- xcodebuildmcp-diagnostic` in your terminal (replacing the @ version with the version number you're using) and paste the output here 25 | placeholder: | 26 | ``` 27 | Running XcodeBuildMCP Diagnostic Tool (v1.3.5)... 28 | Collecting system information and checking dependencies... 29 | 30 | [2025-05-07T10:06:22.737Z] [INFO] [Diagnostic]: Running diagnostic tool 31 | # XcodeBuildMCP Diagnostic Report 32 | 33 | ... 34 | ``` 35 | validations: 36 | required: true 37 | 38 | - type: input 39 | id: editor-client 40 | attributes: 41 | label: Editor/Client 42 | description: The editor or MCP client you're using 43 | placeholder: Cursor 0.49.1 44 | validations: 45 | required: true 46 | 47 | - type: input 48 | id: mcp-server-version 49 | attributes: 50 | label: MCP Server Version 51 | description: The version of XcodeBuildMCP you're using 52 | placeholder: 1.2.2 53 | validations: 54 | required: true 55 | 56 | - type: input 57 | id: llm 58 | attributes: 59 | label: LLM 60 | description: The AI model you're using 61 | placeholder: Claude 3.5 Sonnet 62 | validations: 63 | required: true 64 | 65 | - type: textarea 66 | id: mcp-config 67 | attributes: 68 | label: MCP Configuration 69 | description: Your MCP configuration file (if applicable) 70 | placeholder: | 71 | ```json 72 | { 73 | "mcpServers": { 74 | "XcodeBuildMCP": {...} 75 | } 76 | } 77 | ``` 78 | render: json 79 | 80 | - type: textarea 81 | id: steps 82 | attributes: 83 | label: Steps to Reproduce 84 | description: Steps to reproduce the behavior 85 | placeholder: | 86 | 1. What you asked the AI agent to do 87 | 2. What the AI agent attempted to do 88 | 3. What failed or didn't work as expected 89 | validations: 90 | required: true 91 | 92 | - type: textarea 93 | id: expected 94 | attributes: 95 | label: Expected Behavior 96 | description: What you expected to happen 97 | placeholder: The AI should have been able to... 98 | validations: 99 | required: true 100 | 101 | - type: textarea 102 | id: actual 103 | attributes: 104 | label: Actual Behavior 105 | description: What actually happened 106 | placeholder: Instead, the AI... 107 | validations: 108 | required: true 109 | 110 | - type: textarea 111 | id: error 112 | attributes: 113 | label: Error Messages 114 | description: Any error messages or unexpected output 115 | placeholder: Error message or output from the AI 116 | render: shell 117 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a new feature for XcodeBuildMCP 3 | title: "[Feature]: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for suggesting a new feature for XcodeBuildMCP! 10 | 11 | - type: textarea 12 | id: feature-description 13 | attributes: 14 | label: Feature Description 15 | description: Describe the new capability you'd like to add to XcodeBuildMCP 16 | placeholder: I would like the AI assistant to be able to... 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: use-cases 22 | attributes: 23 | label: Use Cases 24 | description: Describe specific scenarios where this feature would be useful 25 | placeholder: | 26 | - Building and testing iOS apps with custom schemes 27 | - Managing multiple simulator configurations 28 | - Automating complex Xcode workflows 29 | validations: 30 | required: false 31 | 32 | - type: textarea 33 | id: example-interactions 34 | attributes: 35 | label: Example Interactions 36 | description: Provide examples of how you envision using this feature 37 | placeholder: | 38 | You: [Example request to the AI] 39 | AI: [Desired response/action] 40 | validations: 41 | required: false 42 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build-and-test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | cache: 'npm' 25 | 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | - name: Build 30 | run: npm run build 31 | 32 | - name: Lint 33 | run: npm run lint 34 | 35 | - name: Check formatting 36 | run: npm run format:check 37 | -------------------------------------------------------------------------------- /.github/workflows/sentry.yml: -------------------------------------------------------------------------------- 1 | name: Sentry Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - name: Install dependencies 15 | run: npm ci 16 | 17 | - name: Build project 18 | run: npm run build 19 | 20 | - name: Extract version from build/version.js 21 | id: get_version 22 | run: echo "MCP_VERSION=$(grep -oE "'[0-9]+\.[0-9]+\.[0-9]+'" build/version.js | tr -d "'")" >> $GITHUB_OUTPUT 23 | 24 | - name: Create Sentry release 25 | uses: getsentry/action-release@v3 26 | env: 27 | SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} 28 | SENTRY_ORG: ${{ secrets.SENTRY_ORG }} 29 | SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} 30 | with: 31 | environment: production 32 | sourcemaps: "./build" 33 | version: ${{ steps.get_version.outputs.MCP_VERSION }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # TypeScript build output 8 | dist/ 9 | build/ 10 | *.tsbuildinfo 11 | 12 | # Auto-generated files 13 | src/version.ts 14 | 15 | # IDE and editor files 16 | .idea/ 17 | .vscode/ 18 | .vscode/* 19 | !.vscode/mcp.json 20 | !.vscode/launch.json 21 | *.swp 22 | *.swo 23 | .DS_Store 24 | .env 25 | .env.local 26 | .env.*.local 27 | 28 | # Logs 29 | logs/ 30 | *.log 31 | 32 | # Test coverage 33 | coverage/ 34 | 35 | # macOS specific 36 | .DS_Store 37 | .AppleDouble 38 | .LSOverride 39 | Icon 40 | ._* 41 | .DocumentRevisions-V100 42 | .fseventsd 43 | .Spotlight-V100 44 | .TemporaryItems 45 | .Trashes 46 | .VolumeIcon.icns 47 | .com.apple.timemachine.donotpresent 48 | 49 | # Xcode 50 | *.xcodeproj/* 51 | !*.xcodeproj/project.pbxproj 52 | !*.xcodeproj/xcshareddata/ 53 | !*.xcworkspace/contents.xcworkspacedata 54 | **/xcshareddata/WorkspaceSettings.xcsettings 55 | *.xcuserstate 56 | project.xcworkspace/ 57 | xcuserdata/ 58 | 59 | # Debug files 60 | .nyc_output/ 61 | *.map 62 | 63 | # Optional npm cache directory 64 | .npm 65 | 66 | # Optional eslint cache 67 | .eslintcache 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variable files 79 | .env 80 | .env.test 81 | .env.production 82 | 83 | # parcel-bundler cache 84 | .cache 85 | .parcel-cache 86 | 87 | # Windsurf 88 | .windsurfrules 89 | 90 | # Sentry Config File 91 | .sentryclirc 92 | 93 | # Claude Config File 94 | **/.claude/settings.local.json 95 | CLAUDE.md 96 | 97 | # incremental builds 98 | Makefile 99 | buildServer.json 100 | 101 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | coverage 5 | *.json 6 | *.md 7 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 100, 6 | tabWidth: 2, 7 | endOfLine: 'auto', 8 | }; 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "attach", 7 | "name": "Attach to MCP Server Dev", 8 | "port": 9999, 9 | "restart": true, 10 | "skipFiles": [ 11 | "/**" 12 | ], 13 | "sourceMaps": true, 14 | "outFiles": [ 15 | "${workspaceFolder}/build/**/*.js" 16 | ], 17 | "cwd": "${workspaceFolder}", 18 | "sourceMapPathOverrides": { 19 | "/*": "${workspaceFolder}/src/*" 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "servers": { 3 | "xcodebuildmcp-dev": { 4 | "type": "stdio", 5 | "command": "node", 6 | "args": [ 7 | "--inspect=9999", 8 | "${workspaceFolder}/build/index.js" 9 | ], 10 | "env": { 11 | "XCODEBUILDMCP_DEBUG": "true" 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /.windsurf/workflows/build-app.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Build and run the app on the iOS simulator 3 | --- 4 | 5 | 1. Build the iOS app 6 | 2. Run the app on the simulator 7 | 3. Only use tools exposed by "xcode-mcp-server-dev" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v1.7.0] - 2025-06-04 4 | - Added support for Swift Package Manager (SPM) 5 | - New tools for Swift Package Manager: 6 | - `swift_package_build` 7 | - `swift_package_clean` 8 | - `swift_package_test` 9 | - `swift_package_run` 10 | - `swift_package_list` 11 | - `swift_package_stop` 12 | 13 | ## [v1.6.1] - 2025-06-03 14 | - Improve UI tool hints 15 | 16 | ## [v1.6.0] - 2025-06-03 17 | - Moved project templates to external GitHub repositories for independent versioning 18 | - Added support for downloading templates from GitHub releases 19 | - Added local template override support via environment variables 20 | - Added `scaffold_ios_project` and `scaffold_macos_project` tools for creating new projects 21 | - Centralized template version management in package.json for easier updates 22 | 23 | ## [v1.5.0] - 2025-06-01 24 | - UI automation is no longer in beta! 25 | - Added support for AXe UI automation 26 | - Revised default installation instructions to prefer npx instead of mise 27 | 28 | ## [v1.4.0] - 2025-05-11 29 | - Merge the incremental build beta branch into main 30 | - Add preferXcodebuild argument to build tools with improved error handling allowing the agent to force the use of xcodebuild over xcodemake for complex projects. It also adds a hint when incremental builds fail due to non-compiler errors, enabling the agent to automatically switch to xcodebuild for a recovery build attempt, improving reliability. 31 | 32 | ## [v1.3.7] - 2025-05-08 33 | - Fix Claude Code issue due to long tool names 34 | 35 | ## [v1.4.0-beta.3] - 2025-05-07 36 | - Fixed issue where incremental builds would only work for "Debug" build configurations 37 | - 38 | ## [v1.4.0-beta.2] - 2025-05-07 39 | - Same as beta 1 but has the latest features from the main release channel 40 | 41 | ## [v1.4.0-beta.1] - 2025-05-05 42 | - Added experimental support for incremental builds (requires opt-in) 43 | 44 | ## [v1.3.6] - 2025-05-07 45 | - Added support for enabling/disabling tools via environment variables 46 | 47 | ## [v1.3.5] - 2025-05-05 48 | - Fixed the text input UI automation tool 49 | - Improve the UI automation tool hints to reduce agent tool call errors 50 | - Improved the project discovery tool to reduce agent tool call errors 51 | - Added instructions for installing idb client manually 52 | 53 | ## [v1.3.4] - 2025-05-04 54 | - Improved Sentry integration 55 | 56 | ## [v1.3.3] - 2025-05-04 57 | - Added Sentry opt-out functionality 58 | 59 | ## [v1.3.1] - 2025-05-03 60 | - Added Sentry integration for error reporting 61 | 62 | ## [v1.3.0] - 2025-04-28 63 | 64 | - Added support for interacting with the simulator (tap, swipe etc.) 65 | - Added support for capturing simulator screenshots 66 | 67 | Please note that the UI automation features are an early preview and currently in beta your mileage may vary. 68 | 69 | ## [v1.2.4] - 2025-04-24 70 | - Improved xcodebuild reporting of warnings and errors in tool response 71 | - Refactor build utils and remove redundant code 72 | 73 | ## [v1.2.3] - 2025-04-23 74 | - Added support for skipping macro validation 75 | 76 | ## [v1.2.2] - 2025-04-23 77 | - Improved log readability with version information for easier debugging 78 | - Enhanced overall stability and performance 79 | 80 | ## [v1.2.1] - 2025-04-23 81 | - General stability improvements and bug fixes 82 | 83 | ## [v1.2.0] - 2025-04-14 84 | ### Added 85 | - New simulator log capture feature: Easily view and debug your app's logs while running in the simulator 86 | - Automatic project discovery: XcodeBuildMCP now finds your Xcode projects and workspaces automatically 87 | - Support for both Intel and Apple Silicon Macs in macOS builds 88 | 89 | ### Improved 90 | - Cleaner, more readable build output with better error messages 91 | - Faster build times and more reliable build process 92 | - Enhanced documentation with clearer usage examples 93 | 94 | ## [v1.1.0] - 2025-04-05 95 | ### Added 96 | - Real-time build progress reporting 97 | - Separate tools for iOS and macOS builds 98 | - Better workspace and project support 99 | 100 | ### Improved 101 | - Simplified build commands with better parameter handling 102 | - More reliable clean operations for both projects and workspaces 103 | 104 | ## [v1.0.2] - 2025-04-02 105 | - Improved documentation with better examples and clearer instructions 106 | - Easier version tracking for compatibility checks 107 | 108 | ## [v1.0.1] - 2025-04-02 109 | - Initial release of XcodeBuildMCP 110 | - Basic support for building iOS and macOS applications 111 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | xcodebuildmcp@cameroncooke.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome! Here's how you can help improve XcodeBuildMCP. 4 | 5 | ## Local development setup 6 | 7 | ### Prerequisites 8 | 9 | In addition to the prerequisites mentioned in the [Getting started](README.md/#getting-started) section of the README, you will also need: 10 | 11 | - Node.js (v18 or later) 12 | - npm 13 | 14 | #### Optional: Enabling UI Automation 15 | 16 | When running locally, you'll need to install AXe for UI automation: 17 | 18 | ```bash 19 | # Install axe (required for UI automation) 20 | brew tap cameroncooke/axe 21 | brew install axe 22 | ``` 23 | 24 | ### Installation 25 | 26 | 1. Clone the repository 27 | 2. Install dependencies: 28 | ``` 29 | npm install 30 | ``` 31 | 3. Build the project: 32 | ``` 33 | npm run build 34 | ``` 35 | 4. Start the server: 36 | ``` 37 | node build/index.js 38 | ``` 39 | 40 | ### Configure your MCP client 41 | 42 | To configure your MCP client to use your local XcodeBuildMCP server you can use the following configuration: 43 | 44 | ```json 45 | { 46 | "mcpServers": { 47 | "XcodeBuildMCP": { 48 | "command": "node", 49 | "args": [ 50 | "/path_to/XcodeBuildMCP/build/index.js" 51 | ] 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | ### Developing using VS Code 58 | 59 | VS Code is especially good for developing XcodeBuildMCP as it has a built-in way to view MCP client/server logs as well as the ability to configure MCP servers at a project level. It probably has the most comprehensive support for MCP development. 60 | 61 | To make your development workflow in VS Code more efficient: 62 | 63 | 1. **Start the MCP Server**: Open the `.vscode/mcp.json` file. You can start the `xcodebuildmcp-dev` server either by clicking the `Start` CodeLens that appears above the server definition, or by opening the Command Palette (`Cmd+Shift+P` or `Ctrl+Shift+P`), running `Mcp: List Servers`, selecting `xcodebuildmcp-dev`, and starting the server. 64 | 2. **Launch the Debugger**: Press `F5` to attach the Node.js debugger. 65 | 66 | Once these steps are completed, you can utilize the tools from the MCP server you are developing within this repository in agent mode. 67 | For more details on how to work with MCP servers in VS Code see: https://code.visualstudio.com/docs/copilot/chat/mcp-servers 68 | 69 | ### Debugging 70 | 71 | You can use MCP Inspector via: 72 | 73 | ```bash 74 | npm run inspect 75 | ``` 76 | 77 | or if you prefer the explicit command: 78 | 79 | ```bash 80 | npx @modelcontextprotocol/inspector node build/index.js 81 | ``` 82 | 83 | #### Using the diagnostic tool 84 | 85 | Running the XcodeBuildMCP server with the environmental variable `XCODEBUILDMCP_DEBUG=true` will expose a new diagnostic tool which you can run using MCP Inspector: 86 | 87 | 88 | ```bash 89 | XCODEBUILDMCP_DEBUG=true npm run inspect 90 | ``` 91 | 92 | Alternatively, you can run the diagnostic tool directly: 93 | 94 | ```bash 95 | node build/diagnostic-cli.js 96 | ``` 97 | 98 | ## Making changes 99 | 100 | 1. Fork the repository and create a new branch 101 | 2. Follow the TypeScript best practices and existing code style 102 | 3. Add proper parameter validation and error handling 103 | 104 | ### Working with Project Templates 105 | 106 | XcodeBuildMCP uses external template repositories for the iOS and macOS project scaffolding features. These templates are maintained separately to allow independent versioning and updates. 107 | 108 | #### Template Repositories 109 | 110 | - **iOS Template**: [XcodeBuildMCP-iOS-Template](https://github.com/cameroncooke/XcodeBuildMCP-iOS-Template) 111 | - **macOS Template**: [XcodeBuildMCP-macOS-Template](https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template) 112 | 113 | #### Local Template Development 114 | 115 | When developing or testing changes to the templates: 116 | 117 | 1. Clone the template repository you want to work on: 118 | ```bash 119 | git clone https://github.com/cameroncooke/XcodeBuildMCP-iOS-Template.git 120 | git clone https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template.git 121 | ``` 122 | 123 | 2. Set the appropriate environment variable to use your local template: 124 | ```bash 125 | # For iOS template development 126 | export XCODEBUILD_MCP_IOS_TEMPLATE_PATH=/path/to/XcodeBuildMCP-iOS-Template 127 | 128 | # For macOS template development 129 | export XCODEBUILD_MCP_MACOS_TEMPLATE_PATH=/path/to/XcodeBuildMCP-macOS-Template 130 | ``` 131 | 132 | 3. When using MCP clients, add these environment variables to your MCP configuration: 133 | ```json 134 | { 135 | "mcpServers": { 136 | "XcodeBuildMCP": { 137 | "command": "node", 138 | "args": ["/path_to/XcodeBuildMCP/build/index.js"], 139 | "env": { 140 | "XCODEBUILD_MCP_IOS_TEMPLATE_PATH": "/path/to/XcodeBuildMCP-iOS-Template", 141 | "XCODEBUILD_MCP_MACOS_TEMPLATE_PATH": "/path/to/XcodeBuildMCP-macOS-Template" 142 | } 143 | } 144 | } 145 | } 146 | ``` 147 | 148 | 4. The scaffold tools will use your local templates instead of downloading from GitHub releases. 149 | 150 | #### Template Versioning 151 | 152 | - Templates are versioned independently from XcodeBuildMCP 153 | - The default template version is specified in `package.json` under `templateVersion` 154 | - You can override the template version with `XCODEBUILD_MCP_TEMPLATE_VERSION` environment variable 155 | - To update the default template version: 156 | 1. Update `templateVersion` in `package.json` 157 | 2. Run `npm run build` to regenerate version.ts 158 | 3. Create a new XcodeBuildMCP release 159 | 160 | #### Testing Template Changes 161 | 162 | 1. Make changes to your local template 163 | 2. Test scaffolding with your changes using the local override 164 | 3. Verify the scaffolded project builds and runs correctly 165 | 4. Once satisfied, create a PR in the template repository 166 | 5. After merging, create a new release in the template repository using the release script 167 | 168 | ## Testing 169 | 170 | 1. Build the project with `npm run build` 171 | 2. Test your changes with MCP Inspector 172 | 3. Verify tools work correctly with different MCP clients 173 | 174 | ## Submitting 175 | 176 | 1. Run `npm run lint` to check for linting issues (use `npm run lint:fix` to auto-fix) 177 | 2. Run `npm run format:check` to verify formatting (use `npm run format` to fix) 178 | 3. Update documentation if you've added or modified features 179 | 4. Add your changes to the CHANGELOG.md file 180 | 5. Push your changes and create a pull request with a clear description 181 | 6. Link any related issues 182 | 183 | For major changes or new features, please open an issue first to discuss your proposed changes. 184 | 185 | ## Code of Conduct 186 | 187 | Please follow our [Code of Conduct](CODE_OF_CONDUCT.md) and community guidelines. 188 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Cameron Cooke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /TOOL_OPTIONS.md: -------------------------------------------------------------------------------- 1 | # XcodeBuildMCP Tool Options 2 | 3 | This document explains how to configure tool registration in XcodeBuildMCP to optimise for different workflows and reduce the number of tools presented to LLM clients. 4 | 5 | ## Overview 6 | 7 | XcodeBuildMCP supports selective tool registration based on environment variables. This allows you to: 8 | 9 | 1. **Opt-in to individual tools** - Enable only specific tools you need 10 | 2. **Enable tool groups** - Enable logical groups of tools for specific workflows 11 | 3. **Default "all tools enabled"** - Without any configuration, all tools are enabled (default behaviour) 12 | 13 | ## Why Use Selective Tool Registration? 14 | 15 | - **Reduced context window usage for LLMs** - Only register tools needed for a specific workflow 16 | - **Optimised for different use cases** - Configure for iOS development, macOS development, UI testing, etc. 17 | 18 | ## Available Tool Groups and Environment Variables 19 | 20 | XcodeBuildMCP provides workflow-based tool groups that organise tools logically based on common developer workflows. 21 | 22 | ### Workflow-based Groups 23 | 24 | These groups organise tools based on common developer workflows, making it easier to enable just the tools needed for specific tasks: 25 | 26 | - **XCODEBUILDMCP_GROUP_PROJECT_DISCOVERY=true** - Project/target discovery and analysis tools 27 | - _e.g., Discover projects, list schemes, show build settings._ 28 | - **XCODEBUILDMCP_GROUP_IOS_SIMULATOR_WORKFLOW=true** - Complete iOS simulator development workflow tools 29 | - _e.g., Building, running, debugging on simulators._ 30 | - **XCODEBUILDMCP_GROUP_IOS_DEVICE_WORKFLOW=true** - iOS physical device development workflow tools 31 | - _e.g., Building and deploying to physical iOS devices._ 32 | - **XCODEBUILDMCP_GROUP_MACOS_WORKFLOW=true** - macOS application development workflow tools 33 | - _e.g., Building, running, debugging macOS applications._ 34 | - **XCODEBUILDMCP_GROUP_SWIFT_PACKAGE_WORKFLOW=true** - Swift Package Manager development workflow tools 35 | - _e.g., Building, testing, and running Swift packages._ 36 | - **XCODEBUILDMCP_GROUP_SIMULATOR_MANAGEMENT=true** - Simulator device management tools 37 | - _e.g., Managing simulator lifecycle (boot, open, set appearance)._ 38 | - **XCODEBUILDMCP_GROUP_APP_DEPLOYMENT=true** - Application deployment tools 39 | - _e.g., Installing and launching apps across platforms._ 40 | - **XCODEBUILDMCP_GROUP_DIAGNOSTICS=true** - Logging and diagnostics tools 41 | - _e.g., Log capture, debugging information._ 42 | - **XCODEBUILDMCP_GROUP_UI_TESTING=true** - UI testing and automation tools 43 | - _e.g., Tools for interacting with UI elements via AXe._ 44 | 45 | ## Enabling Individual Tools 46 | 47 | To enable specific tools rather than entire groups, use the following environment variables. Each tool is enabled by setting its corresponding variable to `true`. 48 | 49 | ### Project Discovery & Information 50 | - **XCODEBUILDMCP_TOOL_DISCOVER_PROJECTS=true** - Discover Xcode projects and workspaces. 51 | - **XCODEBUILDMCP_TOOL_LIST_SCHEMES_WORKSPACE=true** - List schemes in an Xcode workspace. 52 | - **XCODEBUILDMCP_TOOL_LIST_SCHEMES_PROJECT=true** - List schemes in an Xcode project. 53 | - **XCODEBUILDMCP_TOOL_LIST_SIMULATORS=true** - List available iOS/tvOS/watchOS simulators. 54 | - **XCODEBUILDMCP_TOOL_SHOW_BUILD_SETTINGS_WORKSPACE=true** - Show build settings for an Xcode workspace. 55 | - **XCODEBUILDMCP_TOOL_SHOW_BUILD_SETTINGS_PROJECT=true** - Show build settings for an Xcode project. 56 | 57 | ### Build, Clean & Run Tools 58 | 59 | #### Clean 60 | - **XCODEBUILDMCP_TOOL_CLEAN_WORKSPACE=true** - Clean build products for an Xcode workspace. 61 | - **XCODEBUILDMCP_TOOL_CLEAN_PROJECT=true** - Clean build products for an Xcode project. 62 | 63 | #### Swift Package Tools 64 | - **XCODEBUILDMCP_TOOL_SWIFT_PACKAGE_BUILD=true** - Build a Swift package using `swift build`. 65 | - **XCODEBUILDMCP_TOOL_SWIFT_PACKAGE_TEST=true** - Run tests for a Swift package using `swift test`. 66 | - **XCODEBUILDMCP_TOOL_SWIFT_PACKAGE_RUN=true** - Run an executable target from a Swift package using `swift run`. 67 | - **XCODEBUILDMCP_TOOL_SWIFT_PACKAGE_STOP=true** - Stop a running Swift package executable by PID. 68 | - **XCODEBUILDMCP_TOOL_SWIFT_PACKAGE_LIST=true** - List currently running Swift package processes. 69 | - **XCODEBUILDMCP_TOOL_SWIFT_PACKAGE_CLEAN=true** - Clean Swift package build artifacts and derived data. 70 | 71 | #### macOS Build & Run 72 | - **XCODEBUILDMCP_TOOL_MACOS_BUILD_WORKSPACE=true** - Build a macOS application from a workspace. 73 | - **XCODEBUILDMCP_TOOL_MACOS_BUILD_PROJECT=true** - Build a macOS application from a project. 74 | - **XCODEBUILDMCP_TOOL_MACOS_BUILD_AND_RUN_WORKSPACE=true** - Build and run a macOS application from a workspace. 75 | - **XCODEBUILDMCP_TOOL_MACOS_BUILD_AND_RUN_PROJECT=true** - Build and run a macOS application from a project. 76 | 77 | #### iOS Simulator Build & Run 78 | - **XCODEBUILDMCP_TOOL_IOS_SIMULATOR_BUILD_BY_NAME_WORKSPACE=true** - Build for iOS Simulator by name from a workspace. 79 | - **XCODEBUILDMCP_TOOL_IOS_SIMULATOR_BUILD_BY_NAME_PROJECT=true** - Build for iOS Simulator by name from a project. 80 | - **XCODEBUILDMCP_TOOL_IOS_SIMULATOR_BUILD_BY_ID_WORKSPACE=true** - Build for iOS Simulator by UDID from a workspace. 81 | - **XCODEBUILDMCP_TOOL_IOS_SIMULATOR_BUILD_BY_ID_PROJECT=true** - Build for iOS Simulator by UDID from a project. 82 | - **XCODEBUILDMCP_TOOL_IOS_SIMULATOR_BUILD_AND_RUN_BY_NAME_WORKSPACE=true** - Build and run on iOS Simulator by name (workspace). 83 | - **XCODEBUILDMCP_TOOL_IOS_SIMULATOR_BUILD_AND_RUN_BY_NAME_PROJECT=true** - Build and run on iOS Simulator by name (project). 84 | - **XCODEBUILDMCP_TOOL_IOS_SIMULATOR_BUILD_AND_RUN_BY_ID_WORKSPACE=true** - Build and run on iOS Simulator by UDID (workspace). 85 | - **XCODEBUILDMCP_TOOL_IOS_SIMULATOR_BUILD_AND_RUN_BY_ID_PROJECT=true** - Build and run on iOS Simulator by UDID (project). 86 | 87 | #### iOS Device Build 88 | - **XCODEBUILDMCP_TOOL_IOS_DEVICE_BUILD_TOOLS=true** - Build iOS apps for physical devices (collection of tools). 89 | 90 | ### App Path & Bundle ID Retrieval 91 | 92 | #### App Path 93 | - **XCODEBUILDMCP_TOOL_GET_MACOS_APP_PATH_WORKSPACE=true** - Get path to a built macOS app (workspace). 94 | - **XCODEBUILDMCP_TOOL_GET_MACOS_APP_PATH_PROJECT=true** - Get path to a built macOS app (project). 95 | - **XCODEBUILDMCP_TOOL_GET_IOS_DEVICE_APP_PATH_WORKSPACE=true** - Get path to a built iOS device app (workspace). 96 | - **XCODEBUILDMCP_TOOL_GET_IOS_DEVICE_APP_PATH_PROJECT=true** - Get path to a built iOS device app (project). 97 | - **XCODEBUILDMCP_TOOL_GET_SIMULATOR_APP_PATH_BY_NAME_WORKSPACE=true** - Get path to a built simulator app by name (workspace). 98 | - **XCODEBUILDMCP_TOOL_GET_SIMULATOR_APP_PATH_BY_NAME_PROJECT=true** - Get path to a built simulator app by name (project). 99 | - **XCODEBUILDMCP_TOOL_GET_SIMULATOR_APP_PATH_BY_ID_WORKSPACE=true** - Get path to a built simulator app by UDID (workspace). 100 | - **XCODEBUILDMCP_TOOL_GET_SIMULATOR_APP_PATH_BY_ID_PROJECT=true** - Get path to a built simulator app by UDID (project). 101 | 102 | #### Bundle ID 103 | - **XCODEBUILDMCP_TOOL_GET_MACOS_BUNDLE_ID=true** - Get the bundle ID of a macOS app. 104 | - **XCODEBUILDMCP_TOOL_GET_IOS_BUNDLE_ID=true** - Get the bundle ID of an iOS app. 105 | 106 | ### Simulator Management & App Lifecycle 107 | 108 | #### Management 109 | - **XCODEBUILDMCP_TOOL_BOOT_SIMULATOR=true** - Boot an iOS/tvOS/watchOS simulator. 110 | - **XCODEBUILDMCP_TOOL_OPEN_SIMULATOR=true** - Open the Simulator application. 111 | - **XCODEBUILDMCP_TOOL_SET_SIMULATOR_APPEARANCE=true** - Set simulator appearance (dark/light mode). 112 | 113 | #### App Installation & Launch 114 | - **XCODEBUILDMCP_TOOL_INSTALL_APP_IN_SIMULATOR=true** - Install an app in a simulator. 115 | - **XCODEBUILDMCP_TOOL_LAUNCH_APP_IN_SIMULATOR=true** - Launch an app in a simulator. 116 | - **XCODEBUILDMCP_TOOL_LAUNCH_APP_WITH_LOGS_IN_SIMULATOR=true** - Launch an app in simulator and capture logs. 117 | - **XCODEBUILDMCP_TOOL_LAUNCH_MACOS_APP=true** - Launch a macOS application. 118 | 119 | ### Logging & Diagnostics 120 | 121 | #### Log Capture 122 | - **XCODEBUILDMCP_TOOL_START_SIMULATOR_LOG_CAPTURE=true** - Start capturing logs from a simulator. 123 | - **XCODEBUILDMCP_TOOL_STOP_AND_GET_SIMULATOR_LOG=true** - Stop capturing logs and retrieve them. 124 | 125 | #### UI Automation (AXe) 126 | - **XCODEBUILDMCP_TOOL_UI_AUTOMATION_TOOLS=true** - Enable UI automation tools (e.g., tap, swipe - requires AXe). 127 | - **XCODEBUILDMCP_TOOL_SCREENSHOT=true** - Capture screenshots from iOS simulators. 128 | 129 | #### Project Scaffolding 130 | - **XCODEBUILDMCP_TOOL_SCAFFOLD_PROJECT=true** - Scaffold new iOS/macOS projects from templates. 131 | 132 | #### Diagnostics 133 | - **XCODEBUILDMCP_DEBUG=true** - Enable diagnostic tool for XcodeBuildMCP server. 134 | 135 | ## Recommended Tool Combinations for Common Use Cases 136 | 137 | Workflow-based groups make it easier to enable just the right tools for specific development tasks. Here are some recommended combinations: 138 | 139 | ### iOS Simulator Developer 140 | 141 | For developers focussed on iOS simulator development: 142 | 143 | ```json 144 | { 145 | // Rest of your MCP configuration 146 | "env": { 147 | "XCODEBUILDMCP_GROUP_PROJECT_DISCOVERY": "true", 148 | "XCODEBUILDMCP_GROUP_IOS_SIMULATOR_WORKFLOW": "true" 149 | } 150 | // Rest of your MCP configuration 151 | } 152 | ``` 153 | 154 | This provides all tools needed to: 155 | 1. Discover and analyse projects 156 | 2. Build for iOS simulators 157 | 3. Install and launch on simulators 158 | 4. Capture logs 159 | 160 | ### macOS Application Developer 161 | 162 | For developers focussed on macOS application development: 163 | 164 | ```json 165 | { 166 | // Rest of your MCP configuration 167 | "env": { 168 | "XCODEBUILDMCP_GROUP_PROJECT_DISCOVERY": "true", 169 | "XCODEBUILDMCP_GROUP_MACOS_WORKFLOW": "true" 170 | } 171 | // Rest of your MCP configuration 172 | } 173 | ``` 174 | 175 | This provides all tools needed to: 176 | 1. Discover and analyse projects 177 | 2. Build for macOS 178 | 3. Launch macOS applications 179 | 180 | ### UI Automation Testing 181 | 182 | For developers focussed on UI automation testing: 183 | 184 | ```json 185 | { 186 | // Rest of your MCP configuration 187 | "env": { 188 | "XCODEBUILDMCP_GROUP_UI_TESTING": "true", 189 | "XCODEBUILDMCP_GROUP_SIMULATOR_MANAGEMENT": "true", 190 | "XCODEBUILDMCP_GROUP_APP_DEPLOYMENT": "true" 191 | } 192 | // Rest of your MCP configuration 193 | } 194 | ``` 195 | 196 | This provides tools for: 197 | 1. Managing simulators 198 | 2. Installing and launching apps 199 | 3. Running UI automation tests 200 | 201 | ## Example Cursor/Windsurf Configuration 202 | 203 | Here is a fully worked example of how to configure Cursor/Windsurf to use specific tool groups: 204 | 205 | ```json 206 | { 207 | "mcpServers": { 208 | "XcodeBuildMCP": { 209 | "command": "npx", 210 | "args": [ 211 | "-y", 212 | "xcodebuildmcp@latest" 213 | ], 214 | "env": { 215 | "XCODEBUILDMCP_GROUP_PROJECT_DISCOVERY": "true", 216 | "XCODEBUILDMCP_GROUP_IOS_SIMULATOR_WORKFLOW": "true" 217 | } 218 | } 219 | } 220 | } 221 | ``` 222 | 223 | This example configures the MCP client to only enable tools related to iOS simulator development. -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cameroncooke/XcodeBuildMCP/21f7c1eb8530e56095fb0e3bfbe3468c5d7d3b54/banner.png -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import tseslint from 'typescript-eslint'; 3 | import prettierPlugin from 'eslint-plugin-prettier'; 4 | 5 | export default [ 6 | eslint.configs.recommended, 7 | ...tseslint.configs.recommended, 8 | { 9 | files: ['**/*.{js,ts}'], 10 | ignores: ['node_modules/**', 'build/**', 'dist/**', 'coverage/**'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | sourceType: 'module', 14 | parser: tseslint.parser, 15 | parserOptions: { 16 | project: './tsconfig.json', 17 | }, 18 | }, 19 | plugins: { 20 | '@typescript-eslint': tseslint.plugin, 21 | 'prettier': prettierPlugin, 22 | }, 23 | rules: { 24 | 'prettier/prettier': 'error', 25 | '@typescript-eslint/explicit-function-return-type': 'warn', 26 | '@typescript-eslint/no-explicit-any': 'warn', 27 | '@typescript-eslint/no-unused-vars': ['error', { 28 | argsIgnorePattern: '^_', 29 | varsIgnorePattern: '^_' 30 | }], 31 | 'no-console': ['warn', { allow: ['error'] }], 32 | }, 33 | }, 34 | ]; 35 | -------------------------------------------------------------------------------- /example_projects/iOS/.cursor/rules/errors.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | Please don't fix any code errors unless reported by XcodeBuildMCP server tool responses. -------------------------------------------------------------------------------- /example_projects/iOS/MCPTest.xcodeproj/xcshareddata/xcschemes/MCPTest.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /example_projects/iOS/MCPTest/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example_projects/iOS/MCPTest/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | } 30 | ], 31 | "info" : { 32 | "author" : "xcode", 33 | "version" : 1 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /example_projects/iOS/MCPTest/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example_projects/iOS/MCPTest/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // MCPTest 4 | // 5 | // Created by Cameron on 16/02/2025. 6 | // 7 | 8 | import SwiftUI 9 | import OSLog 10 | 11 | struct ContentView: View { 12 | @State private var text: String = "" 13 | 14 | var body: some View { 15 | VStack { 16 | Image(systemName: "globe") 17 | .imageScale(.large) 18 | .foregroundStyle(.tint) 19 | TextField("Enter text", text: $text) 20 | .textFieldStyle(RoundedBorderTextFieldStyle()) 21 | .padding(.horizontal) 22 | Text(text) 23 | 24 | Button("Log something") { 25 | let message = ProcessInfo.processInfo.environment.map { "\($0.key): \($0.value)" }.joined(separator: "\n") 26 | Logger.myApp.debug("Environment: \(message)") 27 | debugPrint("Button was pressed.") 28 | 29 | text = "You just pressed the button!" 30 | } 31 | } 32 | .padding() 33 | } 34 | } 35 | 36 | #Preview { 37 | ContentView() 38 | } 39 | 40 | // OS Log Extension 41 | extension Logger { 42 | static let myApp = Logger( 43 | subsystem: "com.cameroncooke.MCPTest", 44 | category: "default" 45 | ) 46 | } 47 | 48 | -------------------------------------------------------------------------------- /example_projects/iOS/MCPTest/MCPTestApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MCPTestApp.swift 3 | // MCPTest 4 | // 5 | // Created by Cameron on 16/02/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct MCPTestApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example_projects/iOS/MCPTest/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example_projects/macOS/MCPTest.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXFileReference section */ 10 | 8BA9F8202D62A17D00C22D5D /* MCPTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MCPTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; 11 | /* End PBXFileReference section */ 12 | 13 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 14 | 8BA9F8222D62A17D00C22D5D /* MCPTest */ = { 15 | isa = PBXFileSystemSynchronizedRootGroup; 16 | path = MCPTest; 17 | sourceTree = ""; 18 | }; 19 | /* End PBXFileSystemSynchronizedRootGroup section */ 20 | 21 | /* Begin PBXFrameworksBuildPhase section */ 22 | 8BA9F81D2D62A17D00C22D5D /* Frameworks */ = { 23 | isa = PBXFrameworksBuildPhase; 24 | buildActionMask = 2147483647; 25 | files = ( 26 | ); 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXFrameworksBuildPhase section */ 30 | 31 | /* Begin PBXGroup section */ 32 | 8BA9F8172D62A17D00C22D5D = { 33 | isa = PBXGroup; 34 | children = ( 35 | 8BA9F8222D62A17D00C22D5D /* MCPTest */, 36 | 8BA9F8212D62A17D00C22D5D /* Products */, 37 | ); 38 | sourceTree = ""; 39 | }; 40 | 8BA9F8212D62A17D00C22D5D /* Products */ = { 41 | isa = PBXGroup; 42 | children = ( 43 | 8BA9F8202D62A17D00C22D5D /* MCPTest.app */, 44 | ); 45 | name = Products; 46 | sourceTree = ""; 47 | }; 48 | /* End PBXGroup section */ 49 | 50 | /* Begin PBXNativeTarget section */ 51 | 8BA9F81F2D62A17D00C22D5D /* MCPTest */ = { 52 | isa = PBXNativeTarget; 53 | buildConfigurationList = 8BA9F8452D62A18100C22D5D /* Build configuration list for PBXNativeTarget "MCPTest" */; 54 | buildPhases = ( 55 | 8BA9F81C2D62A17D00C22D5D /* Sources */, 56 | 8BA9F81D2D62A17D00C22D5D /* Frameworks */, 57 | 8BA9F81E2D62A17D00C22D5D /* Resources */, 58 | ); 59 | buildRules = ( 60 | ); 61 | dependencies = ( 62 | ); 63 | fileSystemSynchronizedGroups = ( 64 | 8BA9F8222D62A17D00C22D5D /* MCPTest */, 65 | ); 66 | name = MCPTest; 67 | packageProductDependencies = ( 68 | ); 69 | productName = MCPTest; 70 | productReference = 8BA9F8202D62A17D00C22D5D /* MCPTest.app */; 71 | productType = "com.apple.product-type.application"; 72 | }; 73 | /* End PBXNativeTarget section */ 74 | 75 | /* Begin PBXProject section */ 76 | 8BA9F8182D62A17D00C22D5D /* Project object */ = { 77 | isa = PBXProject; 78 | attributes = { 79 | BuildIndependentTargetsInParallel = 1; 80 | LastSwiftUpdateCheck = 1620; 81 | LastUpgradeCheck = 1620; 82 | TargetAttributes = { 83 | 8BA9F81F2D62A17D00C22D5D = { 84 | CreatedOnToolsVersion = 16.2; 85 | }; 86 | }; 87 | }; 88 | buildConfigurationList = 8BA9F81B2D62A17D00C22D5D /* Build configuration list for PBXProject "MCPTest" */; 89 | developmentRegion = en; 90 | hasScannedForEncodings = 0; 91 | knownRegions = ( 92 | en, 93 | Base, 94 | ); 95 | mainGroup = 8BA9F8172D62A17D00C22D5D; 96 | minimizedProjectReferenceProxies = 1; 97 | preferredProjectObjectVersion = 77; 98 | productRefGroup = 8BA9F8212D62A17D00C22D5D /* Products */; 99 | projectDirPath = ""; 100 | projectRoot = ""; 101 | targets = ( 102 | 8BA9F81F2D62A17D00C22D5D /* MCPTest */, 103 | ); 104 | }; 105 | /* End PBXProject section */ 106 | 107 | /* Begin PBXResourcesBuildPhase section */ 108 | 8BA9F81E2D62A17D00C22D5D /* Resources */ = { 109 | isa = PBXResourcesBuildPhase; 110 | buildActionMask = 2147483647; 111 | files = ( 112 | ); 113 | runOnlyForDeploymentPostprocessing = 0; 114 | }; 115 | /* End PBXResourcesBuildPhase section */ 116 | 117 | /* Begin PBXSourcesBuildPhase section */ 118 | 8BA9F81C2D62A17D00C22D5D /* Sources */ = { 119 | isa = PBXSourcesBuildPhase; 120 | buildActionMask = 2147483647; 121 | files = ( 122 | ); 123 | runOnlyForDeploymentPostprocessing = 0; 124 | }; 125 | /* End PBXSourcesBuildPhase section */ 126 | 127 | /* Begin XCBuildConfiguration section */ 128 | 8BA9F8432D62A18100C22D5D /* Debug */ = { 129 | isa = XCBuildConfiguration; 130 | buildSettings = { 131 | ALWAYS_SEARCH_USER_PATHS = NO; 132 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 133 | CLANG_ANALYZER_NONNULL = YES; 134 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 135 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 136 | CLANG_ENABLE_MODULES = YES; 137 | CLANG_ENABLE_OBJC_ARC = YES; 138 | CLANG_ENABLE_OBJC_WEAK = YES; 139 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 140 | CLANG_WARN_BOOL_CONVERSION = YES; 141 | CLANG_WARN_COMMA = YES; 142 | CLANG_WARN_CONSTANT_CONVERSION = YES; 143 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 144 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 145 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 146 | CLANG_WARN_EMPTY_BODY = YES; 147 | CLANG_WARN_ENUM_CONVERSION = YES; 148 | CLANG_WARN_INFINITE_RECURSION = YES; 149 | CLANG_WARN_INT_CONVERSION = YES; 150 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 151 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 152 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 153 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 154 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 155 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 156 | CLANG_WARN_STRICT_PROTOTYPES = YES; 157 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 158 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 159 | CLANG_WARN_UNREACHABLE_CODE = YES; 160 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 161 | COPY_PHASE_STRIP = NO; 162 | DEBUG_INFORMATION_FORMAT = dwarf; 163 | ENABLE_STRICT_OBJC_MSGSEND = YES; 164 | ENABLE_TESTABILITY = YES; 165 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 166 | GCC_C_LANGUAGE_STANDARD = gnu17; 167 | GCC_DYNAMIC_NO_PIC = NO; 168 | GCC_NO_COMMON_BLOCKS = YES; 169 | GCC_OPTIMIZATION_LEVEL = 0; 170 | GCC_PREPROCESSOR_DEFINITIONS = ( 171 | "DEBUG=1", 172 | "$(inherited)", 173 | ); 174 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 175 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 176 | GCC_WARN_UNDECLARED_SELECTOR = YES; 177 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 178 | GCC_WARN_UNUSED_FUNCTION = YES; 179 | GCC_WARN_UNUSED_VARIABLE = YES; 180 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 181 | MACOSX_DEPLOYMENT_TARGET = 15.2; 182 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 183 | MTL_FAST_MATH = YES; 184 | ONLY_ACTIVE_ARCH = YES; 185 | SDKROOT = macosx; 186 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 187 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 188 | }; 189 | name = Debug; 190 | }; 191 | 8BA9F8442D62A18100C22D5D /* Release */ = { 192 | isa = XCBuildConfiguration; 193 | buildSettings = { 194 | ALWAYS_SEARCH_USER_PATHS = NO; 195 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 196 | CLANG_ANALYZER_NONNULL = YES; 197 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 198 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 199 | CLANG_ENABLE_MODULES = YES; 200 | CLANG_ENABLE_OBJC_ARC = YES; 201 | CLANG_ENABLE_OBJC_WEAK = YES; 202 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 203 | CLANG_WARN_BOOL_CONVERSION = YES; 204 | CLANG_WARN_COMMA = YES; 205 | CLANG_WARN_CONSTANT_CONVERSION = YES; 206 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 207 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 208 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 209 | CLANG_WARN_EMPTY_BODY = YES; 210 | CLANG_WARN_ENUM_CONVERSION = YES; 211 | CLANG_WARN_INFINITE_RECURSION = YES; 212 | CLANG_WARN_INT_CONVERSION = YES; 213 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 214 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 215 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 216 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 217 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 218 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 219 | CLANG_WARN_STRICT_PROTOTYPES = YES; 220 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 221 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 222 | CLANG_WARN_UNREACHABLE_CODE = YES; 223 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 224 | COPY_PHASE_STRIP = NO; 225 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 226 | ENABLE_NS_ASSERTIONS = NO; 227 | ENABLE_STRICT_OBJC_MSGSEND = YES; 228 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 229 | GCC_C_LANGUAGE_STANDARD = gnu17; 230 | GCC_NO_COMMON_BLOCKS = YES; 231 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 232 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 233 | GCC_WARN_UNDECLARED_SELECTOR = YES; 234 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 235 | GCC_WARN_UNUSED_FUNCTION = YES; 236 | GCC_WARN_UNUSED_VARIABLE = YES; 237 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 238 | MACOSX_DEPLOYMENT_TARGET = 15.2; 239 | MTL_ENABLE_DEBUG_INFO = NO; 240 | MTL_FAST_MATH = YES; 241 | SDKROOT = macosx; 242 | SWIFT_COMPILATION_MODE = wholemodule; 243 | }; 244 | name = Release; 245 | }; 246 | 8BA9F8462D62A18100C22D5D /* Debug */ = { 247 | isa = XCBuildConfiguration; 248 | buildSettings = { 249 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 250 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 251 | CODE_SIGN_ENTITLEMENTS = MCPTest/MCPTest.entitlements; 252 | CODE_SIGN_STYLE = Automatic; 253 | COMBINE_HIDPI_IMAGES = YES; 254 | CURRENT_PROJECT_VERSION = 1; 255 | DEVELOPMENT_ASSET_PATHS = "\"MCPTest/Preview Content\""; 256 | DEVELOPMENT_TEAM = ""; 257 | ENABLE_HARDENED_RUNTIME = YES; 258 | ENABLE_PREVIEWS = YES; 259 | GENERATE_INFOPLIST_FILE = YES; 260 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 261 | LD_RUNPATH_SEARCH_PATHS = ( 262 | "$(inherited)", 263 | "@executable_path/../Frameworks", 264 | ); 265 | MARKETING_VERSION = 1.0; 266 | PRODUCT_BUNDLE_IDENTIFIER = com.cameroncooke.MCPTest; 267 | PRODUCT_NAME = "$(TARGET_NAME)"; 268 | SWIFT_EMIT_LOC_STRINGS = YES; 269 | SWIFT_VERSION = 5.0; 270 | }; 271 | name = Debug; 272 | }; 273 | 8BA9F8472D62A18100C22D5D /* Release */ = { 274 | isa = XCBuildConfiguration; 275 | buildSettings = { 276 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 277 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 278 | CODE_SIGN_ENTITLEMENTS = MCPTest/MCPTest.entitlements; 279 | CODE_SIGN_STYLE = Automatic; 280 | COMBINE_HIDPI_IMAGES = YES; 281 | CURRENT_PROJECT_VERSION = 1; 282 | DEVELOPMENT_ASSET_PATHS = "\"MCPTest/Preview Content\""; 283 | DEVELOPMENT_TEAM = ""; 284 | ENABLE_HARDENED_RUNTIME = YES; 285 | ENABLE_PREVIEWS = YES; 286 | GENERATE_INFOPLIST_FILE = YES; 287 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 288 | LD_RUNPATH_SEARCH_PATHS = ( 289 | "$(inherited)", 290 | "@executable_path/../Frameworks", 291 | ); 292 | MARKETING_VERSION = 1.0; 293 | PRODUCT_BUNDLE_IDENTIFIER = com.cameroncooke.MCPTest; 294 | PRODUCT_NAME = "$(TARGET_NAME)"; 295 | SWIFT_EMIT_LOC_STRINGS = YES; 296 | SWIFT_VERSION = 5.0; 297 | }; 298 | name = Release; 299 | }; 300 | /* End XCBuildConfiguration section */ 301 | 302 | /* Begin XCConfigurationList section */ 303 | 8BA9F81B2D62A17D00C22D5D /* Build configuration list for PBXProject "MCPTest" */ = { 304 | isa = XCConfigurationList; 305 | buildConfigurations = ( 306 | 8BA9F8432D62A18100C22D5D /* Debug */, 307 | 8BA9F8442D62A18100C22D5D /* Release */, 308 | ); 309 | defaultConfigurationIsVisible = 0; 310 | defaultConfigurationName = Release; 311 | }; 312 | 8BA9F8452D62A18100C22D5D /* Build configuration list for PBXNativeTarget "MCPTest" */ = { 313 | isa = XCConfigurationList; 314 | buildConfigurations = ( 315 | 8BA9F8462D62A18100C22D5D /* Debug */, 316 | 8BA9F8472D62A18100C22D5D /* Release */, 317 | ); 318 | defaultConfigurationIsVisible = 0; 319 | defaultConfigurationName = Release; 320 | }; 321 | /* End XCConfigurationList section */ 322 | }; 323 | rootObject = 8BA9F8182D62A17D00C22D5D /* Project object */; 324 | } 325 | -------------------------------------------------------------------------------- /example_projects/macOS/MCPTest.xcodeproj/xcshareddata/xcschemes/MCPTest.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /example_projects/macOS/MCPTest/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example_projects/macOS/MCPTest/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /example_projects/macOS/MCPTest/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example_projects/macOS/MCPTest/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // MCPTest 4 | // 5 | // Created by Cameron on 16/02/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | var body: some View { 12 | VStack { 13 | Image(systemName: "globe") 14 | .imageScale(.large) 15 | .foregroundStyle(.tint) 16 | Text("Hello, world!") 17 | } 18 | .padding() 19 | } 20 | } 21 | 22 | #Preview { 23 | ContentView() 24 | } 25 | 26 | -------------------------------------------------------------------------------- /example_projects/macOS/MCPTest/MCPTest.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example_projects/macOS/MCPTest/MCPTestApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MCPTestApp.swift 3 | // MCPTest 4 | // 5 | // Created by Cameron on 16/02/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct MCPTestApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example_projects/macOS/MCPTest/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example_projects/spm/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /example_projects/spm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "7cf18911c918103f9311fb24b72f425fd83fa1521b50c6eacd4a0f8ee0c18743", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-argument-parser", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/apple/swift-argument-parser.git", 8 | "state" : { 9 | "revision" : "011f0c765fb46d9cac61bca19be0527e99c98c8b", 10 | "version" : "1.5.1" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /example_projects/spm/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "spm", 8 | platforms: [ 9 | .macOS(.v15), 10 | ], 11 | dependencies: [ 12 | .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.1"), 13 | ], 14 | targets: [ 15 | .executableTarget( 16 | name: "spm" 17 | ), 18 | .executableTarget( 19 | name: "quick-task", 20 | dependencies: [ 21 | "TestLib", 22 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 23 | ] 24 | ), 25 | .executableTarget( 26 | name: "long-server", 27 | dependencies: [ 28 | "TestLib", 29 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 30 | ] 31 | ), 32 | .target( 33 | name: "TestLib" 34 | ), 35 | .testTarget( 36 | name: "TestLibTests", 37 | dependencies: ["TestLib"] 38 | ), 39 | ] 40 | ) 41 | -------------------------------------------------------------------------------- /example_projects/spm/Sources/TestLib/TaskManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public class TaskManager { 4 | private var isServerRunning = false 5 | 6 | public init() {} 7 | 8 | public func executeQuickTask(name: String, duration: Int, verbose: Bool) async { 9 | if verbose { 10 | print("📝 Task '\(name)' started at \(Date())") 11 | } 12 | 13 | // Simulate work with periodic output using Swift Concurrency 14 | for i in 1...duration { 15 | if verbose { 16 | print("⚙️ Working... step \(i)/\(duration)") 17 | } 18 | try? await Task.sleep(for: .seconds(1)) 19 | } 20 | 21 | if verbose { 22 | print("🎉 Task '\(name)' completed at \(Date())") 23 | } else { 24 | print("Task '\(name)' completed in \(duration)s") 25 | } 26 | } 27 | 28 | public func startLongRunningServer(port: Int, verbose: Bool, autoShutdown: Int) async { 29 | if verbose { 30 | print("🔧 Initializing server on port \(port)...") 31 | } 32 | 33 | var secondsRunning = 0 34 | let startTime = Date() 35 | isServerRunning = true 36 | 37 | // Simulate server startup 38 | try? await Task.sleep(for: .milliseconds(500)) 39 | print("✅ Server running on port \(port)") 40 | 41 | // Main server loop using Swift Concurrency 42 | while isServerRunning { 43 | try? await Task.sleep(for: .seconds(1)) 44 | secondsRunning += 1 45 | 46 | if verbose && secondsRunning % 5 == 0 { 47 | print("📊 Server heartbeat: \(secondsRunning)s uptime") 48 | } 49 | 50 | // Handle auto-shutdown 51 | if autoShutdown > 0 && secondsRunning >= autoShutdown { 52 | if verbose { 53 | print("⏰ Auto-shutdown triggered after \(autoShutdown)s") 54 | } 55 | break 56 | } 57 | } 58 | 59 | let uptime = Date().timeIntervalSince(startTime) 60 | print("🛑 Server stopped after \(String(format: "%.1f", uptime))s uptime") 61 | isServerRunning = false 62 | } 63 | 64 | public func stopServer() { 65 | isServerRunning = false 66 | } 67 | 68 | public func calculateSum(_ a: Int, _ b: Int) -> Int { 69 | return a + b 70 | } 71 | 72 | public func validateInput(_ input: String) -> Bool { 73 | return !input.isEmpty && input.count <= 100 74 | } 75 | } -------------------------------------------------------------------------------- /example_projects/spm/Sources/long-server/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import TestLib 3 | import ArgumentParser 4 | 5 | @main 6 | struct LongServer: AsyncParsableCommand { 7 | static let configuration = CommandConfiguration( 8 | commandName: "long-server", 9 | abstract: "A long-running server that runs indefinitely until stopped" 10 | ) 11 | 12 | @Option(name: .shortAndLong, help: "Port to listen on (default: 8080)") 13 | var port: Int = 8080 14 | 15 | @Flag(name: .shortAndLong, help: "Enable verbose logging") 16 | var verbose: Bool = false 17 | 18 | @Option(name: .shortAndLong, help: "Auto-shutdown after N seconds (0 = run forever)") 19 | var autoShutdown: Int = 0 20 | 21 | func run() async throws { 22 | let taskManager = TaskManager() 23 | 24 | if verbose { 25 | print("🚀 Starting long-running server...") 26 | print("🌐 Port: \(port)") 27 | if autoShutdown > 0 { 28 | print("⏰ Auto-shutdown: \(autoShutdown) seconds") 29 | } else { 30 | print("♾️ Running indefinitely (use SIGTERM to stop)") 31 | } 32 | } 33 | 34 | // Set up signal handling for graceful shutdown 35 | let signalSource = DispatchSource.makeSignalSource(signal: SIGTERM, queue: .main) 36 | signalSource.setEventHandler { 37 | if verbose { 38 | print("\n🛑 Received SIGTERM, shutting down gracefully...") 39 | } 40 | taskManager.stopServer() 41 | } 42 | signalSource.resume() 43 | signal(SIGTERM, SIG_IGN) 44 | 45 | await taskManager.startLongRunningServer( 46 | port: port, 47 | verbose: verbose, 48 | autoShutdown: autoShutdown 49 | ) 50 | } 51 | } -------------------------------------------------------------------------------- /example_projects/spm/Sources/quick-task/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import TestLib 3 | import ArgumentParser 4 | 5 | @main 6 | struct QuickTask: AsyncParsableCommand { 7 | static let configuration = CommandConfiguration( 8 | commandName: "quick-task", 9 | abstract: "A quick task that finishes within 5 seconds", 10 | version: "1.0.0" 11 | ) 12 | 13 | @Option(name: .shortAndLong, help: "Number of seconds to work (default: 3)") 14 | var duration: Int = 3 15 | 16 | @Flag(name: .shortAndLong, help: "Enable verbose output") 17 | var verbose: Bool = false 18 | 19 | @Option(name: .shortAndLong, help: "Task name to display") 20 | var taskName: String = "DefaultTask" 21 | 22 | func run() async throws { 23 | let taskManager = TaskManager() 24 | 25 | if verbose { 26 | print("🚀 Starting quick task: \(taskName)") 27 | print("⏱️ Duration: \(duration) seconds") 28 | } 29 | 30 | await taskManager.executeQuickTask(name: taskName, duration: duration, verbose: verbose) 31 | 32 | if verbose { 33 | print("✅ Quick task completed successfully!") 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /example_projects/spm/Sources/spm/main.swift: -------------------------------------------------------------------------------- 1 | print("Hello, world!") 2 | -------------------------------------------------------------------------------- /example_projects/spm/Tests/TestLibTests/SimpleTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | 3 | @Test("Basic truth assertions") 4 | func basicTruthTest() { 5 | #expect(true == true) 6 | #expect(false == false) 7 | #expect(true != false) 8 | } 9 | 10 | @Test("Basic math operations") 11 | func basicMathTest() { 12 | #expect(2 + 2 == 4) 13 | #expect(5 - 3 == 2) 14 | #expect(3 * 4 == 12) 15 | #expect(10 / 2 == 5) 16 | } 17 | 18 | @Test("String operations") 19 | func stringTest() { 20 | let greeting = "Hello" 21 | let world = "World" 22 | #expect(greeting + " " + world == "Hello World") 23 | #expect(greeting.count == 5) 24 | #expect(world.isEmpty == false) 25 | } 26 | 27 | @Test("Array operations") 28 | func arrayTest() { 29 | let numbers = [1, 2, 3, 4, 5] 30 | #expect(numbers.count == 5) 31 | #expect(numbers.first == 1) 32 | #expect(numbers.last == 5) 33 | #expect(numbers.contains(3) == true) 34 | } 35 | 36 | @Test("Optional handling") 37 | func optionalTest() { 38 | let someValue: Int? = 42 39 | let nilValue: Int? = nil 40 | 41 | #expect(someValue != nil) 42 | #expect(nilValue == nil) 43 | #expect(someValue! == 42) 44 | } 45 | -------------------------------------------------------------------------------- /mcp-install-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cameroncooke/XcodeBuildMCP/21f7c1eb8530e56095fb0e3bfbe3468c5d7d3b54/mcp-install-dark.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xcodebuildmcp", 3 | "version": "1.7.0", 4 | "templateVersion": "v1.0.2", 5 | "main": "build/index.js", 6 | "type": "module", 7 | "bin": { 8 | "xcodebuildmcp": "./build/index.js", 9 | "xcodebuildmcp-diagnostic": "./build/diagnostic-cli.js" 10 | }, 11 | "scripts": { 12 | "prebuild": "node -e \"const fs = require('fs'); const pkg = require('./package.json'); fs.writeFileSync('src/version.ts', \\`export const version = '\\${pkg.version}';\\nexport const templateVersion = '\\${pkg.templateVersion}';\\n\\`)\"", 13 | "build": "npm run prebuild && tsup", 14 | "build:watch": "npm run prebuild && tsup --watch", 15 | "lint": "eslint 'src/**/*.{js,ts}'", 16 | "lint:fix": "eslint 'src/**/*.{js,ts}' --fix", 17 | "format": "prettier --write 'src/**/*.{js,ts}'", 18 | "format:check": "prettier --check 'src/**/*.{js,ts}'", 19 | "inspect": "npx @modelcontextprotocol/inspector node build/index.js", 20 | "diagnostic": "node build/diagnostic-cli.js" 21 | }, 22 | "files": [ 23 | "build" 24 | ], 25 | "keywords": [ 26 | "xcodebuild", 27 | "mcp", 28 | "modelcontextprotocol", 29 | "xcode", 30 | "ios", 31 | "macos", 32 | "simulator" 33 | ], 34 | "author": "Cameron Cooke", 35 | "license": "MIT", 36 | "description": "XcodeBuildMCP is a ModelContextProtocol server that provides tools for Xcode project management, simulator management, and app utilities.", 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/cameroncooke/XcodeBuildMCP.git" 40 | }, 41 | "homepage": "https://www.async-let.com/blog/xcodebuild-mcp/", 42 | "bugs": { 43 | "url": "https://github.com/cameroncooke/XcodeBuildMCP/issues" 44 | }, 45 | "dependencies": { 46 | "@modelcontextprotocol/sdk": "^1.6.1", 47 | "@sentry/cli": "^2.43.1", 48 | "@sentry/node": "^9.15.0", 49 | "uuid": "^11.1.0", 50 | "zod": "^3.24.2" 51 | }, 52 | "devDependencies": { 53 | "@bacons/xcode": "^1.0.0-alpha.24", 54 | "@eslint/eslintrc": "^3.3.1", 55 | "@eslint/js": "^9.23.0", 56 | "@types/node": "^22.13.6", 57 | "@typescript-eslint/eslint-plugin": "^8.28.0", 58 | "@typescript-eslint/parser": "^8.28.0", 59 | "eslint": "^9.23.0", 60 | "eslint-config-prettier": "^10.1.1", 61 | "eslint-plugin-prettier": "^5.2.5", 62 | "prettier": "^3.5.3", 63 | "ts-node": "^10.9.2", 64 | "tsup": "^8.5.0", 65 | "typescript": "^5.8.2", 66 | "typescript-eslint": "^8.28.0", 67 | "xcode": "^3.0.1" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Usage: ./release.sh [npm-tag] [--dry-run] 5 | VERSION=$1 6 | DRY_RUN=false 7 | NPM_TAG_SPECIFIED=false 8 | NPM_TAG="latest" 9 | 10 | # Validate version format 11 | if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+\.[0-9]+)?$ ]]; then 12 | echo "❌ Invalid version format: $VERSION" 13 | echo "Version must be in format: x.y.z or x.y.z-tag.n (e.g., 1.4.0 or 1.4.0-beta.3)" 14 | exit 1 15 | fi 16 | 17 | # Set default tag based on version format 18 | if [[ "$VERSION" =~ -beta ]]; then 19 | NPM_TAG="beta" 20 | elif [[ "$VERSION" =~ -alpha ]]; then 21 | NPM_TAG="alpha" 22 | elif [[ "$VERSION" =~ -rc ]]; then 23 | NPM_TAG="rc" 24 | elif [[ "$VERSION" =~ -experimental ]]; then 25 | NPM_TAG="experimental" 26 | fi 27 | 28 | # Check for arguments and set flags 29 | for arg in "$@"; do 30 | if [[ "$arg" == "--dry-run" ]]; then 31 | DRY_RUN=true 32 | elif [[ "$arg" != "$VERSION" && "$arg" != "--dry-run" ]]; then 33 | # If argument is not the version and not --dry-run, treat it as the npm tag 34 | NPM_TAG="$arg" 35 | NPM_TAG_SPECIFIED=true 36 | fi 37 | done 38 | 39 | if [ -z "$VERSION" ]; then 40 | echo "Usage: $0 [npm-tag] [--dry-run]" 41 | exit 1 42 | fi 43 | 44 | # Detect current branch 45 | BRANCH=$(git rev-parse --abbrev-ref HEAD) 46 | 47 | # Enforce branch/tag policy (customize as needed) 48 | if [[ "$BRANCH" == "main" && "$NPM_TAG" != "latest" && "$NPM_TAG_SPECIFIED" == false ]]; then 49 | echo "⚠️ Warning: Publishing a non-latest tag from main branch." 50 | echo "Continue? (y/n)" 51 | read -r CONTINUE 52 | if [[ "$CONTINUE" != "y" ]]; then 53 | echo "❌ Release cancelled." 54 | exit 1 55 | fi 56 | fi 57 | 58 | if [[ "$BRANCH" != "main" && "$NPM_TAG" == "latest" ]]; then 59 | echo "⚠️ Warning: Publishing with tag '$NPM_TAG' from non-main branch." 60 | echo "Continue? (y/n)" 61 | read -r CONTINUE 62 | if [[ "$CONTINUE" != "y" ]]; then 63 | echo "❌ Release cancelled." 64 | exit 1 65 | fi 66 | fi 67 | 68 | run() { 69 | if $DRY_RUN; then 70 | echo "[dry-run] $*" 71 | else 72 | eval "$@" 73 | fi 74 | } 75 | 76 | # Version update 77 | echo "" 78 | echo "🔧 Setting version to $VERSION..." 79 | run "npm version \"$VERSION\" --no-git-tag-version" 80 | 81 | # README update 82 | echo "" 83 | echo "📝 Updating version in README.md..." 84 | # Update version references in code examples using extended regex for precise semver matching 85 | run "sed -i '' -E 's/@[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+\.[0-9]+)?(-[a-zA-Z0-9]+\.[0-9]+)*(-[a-zA-Z0-9]+)?/@'"$VERSION"'/g' README.md" 86 | 87 | # Update URL-encoded version references in shield links 88 | echo "📝 Updating version in README.md shield links..." 89 | run "sed -i '' -E 's/npm%3Axcodebuildmcp%40[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+\.[0-9]+)?(-[a-zA-Z0-9]+\.[0-9]+)*(-[a-zA-Z0-9]+)?/npm%3Axcodebuildmcp%40'"$VERSION"'/g' README.md" 90 | 91 | echo "" 92 | echo "📝 Updating version in TOOL_OPTIONS.md..." 93 | run "sed -i '' -E 's/@[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+\.[0-9]+)?(-[a-zA-Z0-9]+\.[0-9]+)*(-[a-zA-Z0-9]+)?/@'"$VERSION"'/g' TOOL_OPTIONS.md" 94 | 95 | # Build 96 | echo "" 97 | echo "🛠 Running build..." 98 | run "npm run build" 99 | 100 | # Git operations 101 | echo "" 102 | echo "📦 Committing changes..." 103 | run "git add ." 104 | run "git commit -m \"Release v$VERSION\"" 105 | run "git tag \"v$VERSION\"" 106 | 107 | echo "" 108 | echo "🚀 Pushing to origin..." 109 | run "git push origin $BRANCH --tags" 110 | 111 | echo "📦 Creating GitHub release..." 112 | if [[ "$NPM_TAG" == "beta" || "$NPM_TAG" == "alpha" || "$NPM_TAG" == "rc" || "$NPM_TAG" == "experimental" ]]; then 113 | run "gh release create "v$VERSION" --generate-notes -t \"Release v$VERSION\" --prerelease" 114 | else 115 | run "gh release create "v$VERSION" --generate-notes -t \"Release v$VERSION\"" 116 | fi 117 | 118 | # npm publish 119 | echo "" 120 | echo "📤 Publishing to npm with tag '$NPM_TAG'..." 121 | run "npm publish --tag $NPM_TAG" 122 | 123 | # Completion message 124 | echo "" 125 | echo "✅ Release v$VERSION complete!" 126 | echo "📝 Don't forget to update the changelog" -------------------------------------------------------------------------------- /src/diagnostic-cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * XcodeBuildMCP Diagnostic CLI 5 | * 6 | * This standalone script runs the diagnostic tool and outputs the results 7 | * to the console. It's designed to be run directly via npx or mise. 8 | */ 9 | 10 | import { version } from './version.js'; 11 | 12 | // Set the debug environment variable 13 | process.env.XCODEBUILDMCP_DEBUG = 'true'; 14 | 15 | async function runDiagnostic(): Promise { 16 | try { 17 | // Using console.error to avoid linting issues as it's allowed by the project's linting rules 18 | console.error(`Running XcodeBuildMCP Diagnostic Tool (v${version})...`); 19 | console.error('Collecting system information and checking dependencies...\n'); 20 | 21 | const { runDiagnosticTool } = await import('./tools/diagnostic.js'); 22 | 23 | // Run the diagnostic tool 24 | const result = await runDiagnosticTool(); 25 | 26 | // Output the diagnostic information 27 | if (result.content && result.content.length > 0) { 28 | const textContent = result.content.find((item: { type: string }) => item.type === 'text'); 29 | if (textContent && 'text' in textContent) { 30 | console.error(textContent.text); 31 | } else { 32 | console.error('Error: Unexpected diagnostic result format'); 33 | } 34 | } else { 35 | console.error('Error: No diagnostic information returned'); 36 | } 37 | 38 | console.error('\nDiagnostic complete. Please include this output when reporting issues.'); 39 | } catch (error) { 40 | console.error('Error running diagnostic:', error); 41 | process.exit(1); 42 | } 43 | } 44 | 45 | // Run the diagnostic 46 | runDiagnostic().catch((error) => { 47 | console.error('Unhandled exception:', error); 48 | process.exit(1); 49 | }); 50 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * XcodeBuildMCP - Main entry point 5 | * 6 | * This file serves as the entry point for the XcodeBuildMCP server, importing and registering 7 | * all tool modules with the MCP server. It follows the platform-specific approach for Xcode tools. 8 | * 9 | * Responsibilities: 10 | * - Creating and starting the MCP server 11 | * - Registering all platform-specific tool modules 12 | * - Configuring server options and logging 13 | * - Handling server lifecycle events 14 | */ 15 | 16 | // Import Sentry instrumentation 17 | import './utils/sentry.js'; 18 | 19 | // Import server components 20 | import { createServer, startServer } from './server/server.js'; 21 | 22 | // Import utilities 23 | import { log } from './utils/logger.js'; 24 | 25 | // Import version 26 | import { version } from './version.js'; 27 | import { registerTools } from './utils/register-tools.js'; 28 | 29 | // Import xcodemake utilities 30 | import { isXcodemakeEnabled, isXcodemakeAvailable } from './utils/xcodemake.js'; 31 | 32 | /** 33 | * Main function to start the server 34 | */ 35 | async function main(): Promise { 36 | try { 37 | // Check if xcodemake is enabled and available 38 | if (isXcodemakeEnabled()) { 39 | log('info', 'xcodemake is enabled, checking if available...'); 40 | const available = await isXcodemakeAvailable(); 41 | if (available) { 42 | log('info', 'xcodemake is available and will be used for builds'); 43 | } else { 44 | log( 45 | 'warn', 46 | 'xcodemake is enabled but could not be made available, falling back to xcodebuild', 47 | ); 48 | } 49 | } else { 50 | log('debug', 'xcodemake is disabled, using standard xcodebuild'); 51 | } 52 | 53 | // Create the server 54 | const server = createServer(); 55 | 56 | // Register tools 57 | registerTools(server); 58 | 59 | // Start the server 60 | await startServer(server); 61 | 62 | // Clean up on exit 63 | process.on('SIGTERM', async () => { 64 | await server.close(); 65 | process.exit(0); 66 | }); 67 | 68 | process.on('SIGINT', async () => { 69 | await server.close(); 70 | process.exit(0); 71 | }); 72 | 73 | // Log successful startup 74 | log('info', `XcodeBuildMCP server (version ${version}) started successfully`); 75 | } catch (error) { 76 | console.error('Fatal error in main():', error); 77 | process.exit(1); 78 | } 79 | } 80 | 81 | // Start the server 82 | main().catch((error) => { 83 | console.error('Unhandled exception:', error); 84 | // Give Sentry a moment to send the error before exiting 85 | setTimeout(() => process.exit(1), 1000); 86 | }); 87 | -------------------------------------------------------------------------------- /src/server/server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Server Configuration - MCP Server setup and lifecycle management 3 | * 4 | * This module handles the creation, configuration, and lifecycle management of the 5 | * Model Context Protocol (MCP) server. It provides the foundation for all tool 6 | * registrations and server capabilities. 7 | * 8 | * Responsibilities: 9 | * - Creating and configuring the MCP server instance 10 | * - Setting up server capabilities and options 11 | * - Managing server lifecycle (start/stop) 12 | * - Handling transport configuration (stdio) 13 | */ 14 | 15 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 16 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 17 | import { log } from '../utils/logger.js'; 18 | import { version } from '../version.js'; 19 | 20 | /** 21 | * Create and configure the MCP server 22 | * @returns Configured MCP server instance 23 | */ 24 | export function createServer(): McpServer { 25 | // Create server instance 26 | const server = new McpServer( 27 | { 28 | name: 'xcodebuildmcp', 29 | version, 30 | }, 31 | { 32 | capabilities: { 33 | tools: { 34 | listChanged: true, 35 | }, 36 | logging: {}, 37 | }, 38 | }, 39 | ); 40 | 41 | // Log server initialization 42 | log('info', `Server initialized (version ${version})`); 43 | 44 | return server; 45 | } 46 | 47 | /** 48 | * Start the MCP server with stdio transport 49 | * @param server The MCP server instance to start 50 | */ 51 | export async function startServer(server: McpServer): Promise { 52 | const transport = new StdioServerTransport(); 53 | await server.connect(transport); 54 | log('info', 'XcodeBuildMCP Server running on stdio'); 55 | } 56 | -------------------------------------------------------------------------------- /src/tools/build-swift-package.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 3 | import path from 'node:path'; 4 | import { 5 | registerTool, 6 | swiftConfigurationSchema, 7 | swiftArchitecturesSchema, 8 | parseAsLibrarySchema, 9 | } from './common.js'; 10 | import { executeCommand } from '../utils/command.js'; 11 | import { validateRequiredParam } from '../utils/validation.js'; 12 | import { ToolResponse } from '../types/common.js'; 13 | import { createErrorResponse } from '../utils/errors.js'; 14 | import { log } from '../utils/logger.js'; 15 | 16 | export function registerBuildSwiftPackageTool(server: McpServer): void { 17 | registerTool( 18 | server, 19 | 'swift_package_build', 20 | 'Builds a Swift Package with swift build', 21 | { 22 | packagePath: z.string().describe('Path to the Swift package root (Required)'), 23 | targetName: z.string().optional().describe('Optional target to build'), 24 | configuration: swiftConfigurationSchema, 25 | architectures: swiftArchitecturesSchema, 26 | parseAsLibrary: parseAsLibrarySchema, 27 | }, 28 | async (params: { 29 | packagePath: string; 30 | targetName?: string; 31 | configuration?: 'debug' | 'release'; 32 | architectures?: ('arm64' | 'x86_64')[]; 33 | parseAsLibrary?: boolean; 34 | }): Promise => { 35 | const pkgValidation = validateRequiredParam('packagePath', params.packagePath); 36 | if (!pkgValidation.isValid) return pkgValidation.errorResponse!; 37 | 38 | const resolvedPath = path.resolve(params.packagePath); 39 | const args: string[] = ['build', '--package-path', resolvedPath]; 40 | 41 | if (params.configuration && params.configuration.toLowerCase() === 'release') { 42 | args.push('-c', 'release'); 43 | } 44 | 45 | if (params.targetName) { 46 | args.push('--target', params.targetName); 47 | } 48 | 49 | if (params.architectures) { 50 | for (const arch of params.architectures) { 51 | args.push('--arch', arch); 52 | } 53 | } 54 | 55 | if (params.parseAsLibrary) { 56 | args.push('-Xswiftc', '-parse-as-library'); 57 | } 58 | 59 | log('info', `Running swift ${args.join(' ')}`); 60 | try { 61 | const result = await executeCommand(['swift', ...args], 'Swift Package Build'); 62 | if (!result.success) { 63 | const errorMessage = result.error || result.output || 'Unknown error'; 64 | return createErrorResponse('Swift package build failed', errorMessage, 'BuildError'); 65 | } 66 | 67 | return { 68 | content: [ 69 | { type: 'text', text: '✅ Swift package build succeeded.' }, 70 | { 71 | type: 'text', 72 | text: '💡 Next: Run tests with swift_package_test or execute with swift_package_run', 73 | }, 74 | { type: 'text', text: result.output }, 75 | ], 76 | }; 77 | } catch (error) { 78 | const message = error instanceof Error ? error.message : String(error); 79 | log('error', `Swift package build failed: ${message}`); 80 | return createErrorResponse('Failed to execute swift build', message, 'SystemError'); 81 | } 82 | }, 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /src/tools/build_ios_device.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * iOS Device Build Tools - Tools for building iOS applications for physical devices 3 | * 4 | * This module provides specialized tools for building iOS applications targeting physical 5 | * devices using xcodebuild. It supports both workspace and project-based builds. 6 | * 7 | * Responsibilities: 8 | * - Building iOS applications for physical devices from project files 9 | * - Building iOS applications for physical devices from workspaces 10 | * - Handling build configuration and derived data paths 11 | * - Providing platform-specific destination parameters 12 | */ 13 | 14 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 15 | import { XcodePlatform } from '../utils/xcode.js'; 16 | import { validateRequiredParam } from '../utils/validation.js'; 17 | import { executeXcodeBuildCommand } from '../utils/build-utils.js'; 18 | import { 19 | registerTool, 20 | workspacePathSchema, 21 | projectPathSchema, 22 | schemeSchema, 23 | configurationSchema, 24 | derivedDataPathSchema, 25 | extraArgsSchema, 26 | BaseWorkspaceParams, 27 | BaseProjectParams, 28 | preferXcodebuildSchema, 29 | } from './common.js'; 30 | 31 | // --- Tool Registration Functions --- 32 | 33 | /** 34 | * Registers the build_ios_dev_ws tool. 35 | */ 36 | export function registerIOSDeviceBuildWorkspaceTool(server: McpServer): void { 37 | type Params = BaseWorkspaceParams; 38 | registerTool( 39 | server, 40 | 'build_ios_dev_ws', 41 | "Builds an iOS app from a workspace for a physical device. IMPORTANT: Requires workspacePath and scheme. Example: build_ios_dev_ws({ workspacePath: '/path/to/MyProject.xcworkspace', scheme: 'MyScheme' })", 42 | { 43 | workspacePath: workspacePathSchema, 44 | scheme: schemeSchema, 45 | configuration: configurationSchema, 46 | derivedDataPath: derivedDataPathSchema, 47 | extraArgs: extraArgsSchema, 48 | preferXcodebuild: preferXcodebuildSchema, 49 | }, 50 | async (params: Params) => { 51 | const workspaceValidation = validateRequiredParam('workspacePath', params.workspacePath); 52 | if (!workspaceValidation.isValid) return workspaceValidation.errorResponse!; 53 | 54 | const schemeValidation = validateRequiredParam('scheme', params.scheme); 55 | if (!schemeValidation.isValid) return schemeValidation.errorResponse!; 56 | 57 | return executeXcodeBuildCommand( 58 | { 59 | ...params, 60 | configuration: params.configuration ?? 'Debug', // Default config 61 | }, 62 | { 63 | platform: XcodePlatform.iOS, 64 | logPrefix: 'iOS Device Build', 65 | }, 66 | params.preferXcodebuild, 67 | 'build', 68 | ); 69 | }, 70 | ); 71 | } 72 | 73 | /** 74 | * Registers the build_ios_dev_proj tool. 75 | */ 76 | export function registerIOSDeviceBuildProjectTool(server: McpServer): void { 77 | type Params = BaseProjectParams; 78 | registerTool( 79 | server, 80 | 'build_ios_dev_proj', 81 | "Builds an iOS app from a project file for a physical device. IMPORTANT: Requires projectPath and scheme. Example: build_ios_dev_proj({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme' })", 82 | { 83 | projectPath: projectPathSchema, 84 | scheme: schemeSchema, 85 | configuration: configurationSchema, 86 | derivedDataPath: derivedDataPathSchema, 87 | extraArgs: extraArgsSchema, 88 | preferXcodebuild: preferXcodebuildSchema, 89 | }, 90 | async (params: Params) => { 91 | const projectValidation = validateRequiredParam('projectPath', params.projectPath); 92 | if (!projectValidation.isValid) return projectValidation.errorResponse!; 93 | 94 | const schemeValidation = validateRequiredParam('scheme', params.scheme); 95 | if (!schemeValidation.isValid) return schemeValidation.errorResponse!; 96 | 97 | return executeXcodeBuildCommand( 98 | { 99 | ...params, 100 | configuration: params.configuration ?? 'Debug', // Default config 101 | }, 102 | { 103 | platform: XcodePlatform.iOS, 104 | logPrefix: 'iOS Device Build', 105 | }, 106 | params.preferXcodebuild, 107 | 'build', 108 | ); 109 | }, 110 | ); 111 | } 112 | 113 | // Register both iOS device build tools 114 | export function registerIOSDeviceBuildTools(server: McpServer): void { 115 | registerIOSDeviceBuildWorkspaceTool(server); 116 | registerIOSDeviceBuildProjectTool(server); 117 | } 118 | -------------------------------------------------------------------------------- /src/tools/build_macos.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * macOS Build Tools - Tools for building and running macOS applications 3 | * 4 | * This module provides specialized tools for building and running macOS applications 5 | * using xcodebuild. It supports both workspace and project-based builds with architecture 6 | * specification (arm64 or x86_64). 7 | * 8 | * Responsibilities: 9 | * - Building macOS applications from project files and workspaces 10 | * - Running macOS applications after building 11 | * - Supporting architecture-specific builds (arm64, x86_64) 12 | * - Handling build configuration and derived data paths 13 | */ 14 | 15 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 16 | import { exec } from 'child_process'; 17 | import { promisify } from 'util'; 18 | import { log } from '../utils/logger.js'; 19 | import { XcodePlatform } from '../utils/xcode.js'; 20 | import { executeCommand } from '../utils/command.js'; 21 | import { createTextResponse } from '../utils/validation.js'; 22 | import { ToolResponse } from '../types/common.js'; 23 | import { executeXcodeBuildCommand } from '../utils/build-utils.js'; 24 | import { z } from 'zod'; 25 | import { 26 | registerTool, 27 | workspacePathSchema, 28 | projectPathSchema, 29 | schemeSchema, 30 | configurationSchema, 31 | derivedDataPathSchema, 32 | extraArgsSchema, 33 | preferXcodebuildSchema, 34 | } from './common.js'; 35 | 36 | // Schema for architecture parameter 37 | const archSchema = z 38 | .enum(['arm64', 'x86_64']) 39 | .optional() 40 | .describe('Architecture to build for (arm64 or x86_64). For macOS only.'); 41 | 42 | // --- Private Helper Functions --- 43 | 44 | /** 45 | * Internal logic for building macOS apps. 46 | */ 47 | async function _handleMacOSBuildLogic(params: { 48 | workspacePath?: string; 49 | projectPath?: string; 50 | scheme: string; 51 | configuration: string; 52 | derivedDataPath?: string; 53 | arch?: string; 54 | extraArgs?: string[]; 55 | preferXcodebuild?: boolean; 56 | }): Promise { 57 | log('info', `Starting macOS build for scheme ${params.scheme} (internal)`); 58 | 59 | return executeXcodeBuildCommand( 60 | { 61 | ...params, 62 | }, 63 | { 64 | platform: XcodePlatform.macOS, 65 | arch: params.arch, 66 | logPrefix: 'macOS Build', 67 | }, 68 | params.preferXcodebuild, 69 | 'build', 70 | ); 71 | } 72 | 73 | async function _getAppPathFromBuildSettings(params: { 74 | workspacePath?: string; 75 | projectPath?: string; 76 | scheme: string; 77 | configuration: string; 78 | derivedDataPath?: string; 79 | arch?: string; 80 | extraArgs?: string[]; 81 | }): Promise<{ success: boolean; appPath?: string; error?: string }> { 82 | try { 83 | // Create the command array for xcodebuild 84 | const command = ['xcodebuild', '-showBuildSettings']; 85 | 86 | // Add the workspace or project 87 | if (params.workspacePath) { 88 | command.push('-workspace', params.workspacePath); 89 | } else if (params.projectPath) { 90 | command.push('-project', params.projectPath); 91 | } 92 | 93 | // Add the scheme and configuration 94 | command.push('-scheme', params.scheme); 95 | command.push('-configuration', params.configuration); 96 | 97 | // Add derived data path if provided 98 | if (params.derivedDataPath) { 99 | command.push('-derivedDataPath', params.derivedDataPath); 100 | } 101 | 102 | // Add extra args if provided 103 | if (params.extraArgs && params.extraArgs.length > 0) { 104 | command.push(...params.extraArgs); 105 | } 106 | 107 | // Execute the command directly 108 | const result = await executeCommand(command, 'Get Build Settings for Launch'); 109 | 110 | if (!result.success) { 111 | return { 112 | success: false, 113 | error: result.error || 'Failed to get build settings', 114 | }; 115 | } 116 | 117 | // Parse the output to extract the app path 118 | const buildSettingsOutput = result.output; 119 | const builtProductsDirMatch = buildSettingsOutput.match(/BUILT_PRODUCTS_DIR = (.+)$/m); 120 | const fullProductNameMatch = buildSettingsOutput.match(/FULL_PRODUCT_NAME = (.+)$/m); 121 | 122 | if (!builtProductsDirMatch || !fullProductNameMatch) { 123 | return { success: false, error: 'Could not extract app path from build settings' }; 124 | } 125 | 126 | const appPath = `${builtProductsDirMatch[1].trim()}/${fullProductNameMatch[1].trim()}`; 127 | return { success: true, appPath }; 128 | } catch (error) { 129 | const errorMessage = error instanceof Error ? error.message : String(error); 130 | return { success: false, error: errorMessage }; 131 | } 132 | } 133 | 134 | /** 135 | * Internal logic for building and running macOS apps. 136 | */ 137 | async function _handleMacOSBuildAndRunLogic(params: { 138 | workspacePath?: string; 139 | projectPath?: string; 140 | scheme: string; 141 | configuration: string; 142 | derivedDataPath?: string; 143 | arch?: string; 144 | extraArgs?: string[]; 145 | preferXcodebuild?: boolean; 146 | }): Promise { 147 | log('info', 'Handling macOS build & run logic...'); 148 | const _warningMessages: { type: 'text'; text: string }[] = []; 149 | const _warningRegex = /\[warning\]: (.*)/g; 150 | 151 | try { 152 | // First, build the app 153 | const buildResult = await _handleMacOSBuildLogic(params); 154 | 155 | // 1. Check if the build itself failed 156 | if (buildResult.isError) { 157 | return buildResult; // Return build failure directly 158 | } 159 | const buildWarningMessages = buildResult.content?.filter((c) => c.type === 'text') ?? []; 160 | 161 | // 2. Build succeeded, now get the app path using the helper 162 | const appPathResult = await _getAppPathFromBuildSettings(params); 163 | 164 | // 3. Check if getting the app path failed 165 | if (!appPathResult.success) { 166 | log('error', 'Build succeeded, but failed to get app path to launch.'); 167 | const response = createTextResponse( 168 | `✅ Build succeeded, but failed to get app path to launch: ${appPathResult.error}`, 169 | false, // Build succeeded, so not a full error 170 | ); 171 | if (response.content) { 172 | response.content.unshift(...buildWarningMessages); 173 | } 174 | return response; 175 | } 176 | 177 | const appPath = appPathResult.appPath; // We know this is a valid string now 178 | log('info', `App path determined as: ${appPath}`); 179 | 180 | // 4. Launch the app using the verified path 181 | // Launch the app 182 | try { 183 | await promisify(exec)(`open "${appPath}"`); 184 | log('info', `✅ macOS app launched successfully: ${appPath}`); 185 | const successResponse: ToolResponse = { 186 | content: [ 187 | ...buildWarningMessages, 188 | { 189 | type: 'text', 190 | text: `✅ macOS build and run succeeded for scheme ${params.scheme}. App launched: ${appPath}`, 191 | }, 192 | ], 193 | }; 194 | return successResponse; 195 | } catch (launchError) { 196 | const errorMessage = launchError instanceof Error ? launchError.message : String(launchError); 197 | log('error', `Build succeeded, but failed to launch app ${appPath}: ${errorMessage}`); 198 | const errorResponse = createTextResponse( 199 | `✅ Build succeeded, but failed to launch app ${appPath}. Error: ${errorMessage}`, 200 | false, // Build succeeded 201 | ); 202 | if (errorResponse.content) { 203 | errorResponse.content.unshift(...buildWarningMessages); 204 | } 205 | return errorResponse; 206 | } 207 | } catch (error) { 208 | const errorMessage = error instanceof Error ? error.message : String(error); 209 | log('error', `Error during macOS build & run logic: ${errorMessage}`); 210 | const errorResponse = createTextResponse( 211 | `Error during macOS build and run: ${errorMessage}`, 212 | true, 213 | ); 214 | return errorResponse; 215 | } 216 | } 217 | 218 | // --- Public Tool Definitions --- 219 | 220 | // Register macOS build workspace tool 221 | export function registerMacOSBuildWorkspaceTool(server: McpServer): void { 222 | type WorkspaceParams = { 223 | workspacePath: string; 224 | scheme: string; 225 | configuration?: string; 226 | derivedDataPath?: string; 227 | arch?: string; 228 | extraArgs?: string[]; 229 | preferXcodebuild?: boolean; 230 | }; 231 | 232 | registerTool( 233 | server, 234 | 'build_mac_ws', 235 | 'Builds a macOS app using xcodebuild from a workspace.', 236 | { 237 | workspacePath: workspacePathSchema, 238 | scheme: schemeSchema, 239 | configuration: configurationSchema, 240 | derivedDataPath: derivedDataPathSchema, 241 | arch: archSchema, 242 | extraArgs: extraArgsSchema, 243 | preferXcodebuild: preferXcodebuildSchema, 244 | }, 245 | async (params) => 246 | _handleMacOSBuildLogic({ 247 | ...params, 248 | configuration: params.configuration ?? 'Debug', 249 | preferXcodebuild: params.preferXcodebuild ?? false, 250 | }), 251 | ); 252 | } 253 | 254 | // Register macOS build project tool 255 | export function registerMacOSBuildProjectTool(server: McpServer): void { 256 | type ProjectParams = { 257 | projectPath: string; 258 | scheme: string; 259 | configuration?: string; 260 | derivedDataPath?: string; 261 | arch?: string; 262 | extraArgs?: string[]; 263 | preferXcodebuild?: boolean; 264 | }; 265 | 266 | registerTool( 267 | server, 268 | 'build_mac_proj', 269 | 'Builds a macOS app using xcodebuild from a project file.', 270 | { 271 | projectPath: projectPathSchema, 272 | scheme: schemeSchema, 273 | configuration: configurationSchema, 274 | derivedDataPath: derivedDataPathSchema, 275 | arch: archSchema, 276 | extraArgs: extraArgsSchema, 277 | preferXcodebuild: preferXcodebuildSchema, 278 | }, 279 | async (params) => 280 | _handleMacOSBuildLogic({ 281 | ...params, 282 | configuration: params.configuration ?? 'Debug', 283 | preferXcodebuild: params.preferXcodebuild ?? false, 284 | }), 285 | ); 286 | } 287 | 288 | // Register macOS build and run workspace tool 289 | export function registerMacOSBuildAndRunWorkspaceTool(server: McpServer): void { 290 | type WorkspaceParams = { 291 | workspacePath: string; 292 | scheme: string; 293 | configuration?: string; 294 | derivedDataPath?: string; 295 | arch?: string; 296 | extraArgs?: string[]; 297 | preferXcodebuild?: boolean; 298 | }; 299 | 300 | registerTool( 301 | server, 302 | 'build_run_mac_ws', 303 | 'Builds and runs a macOS app from a workspace in one step.', 304 | { 305 | workspacePath: workspacePathSchema, 306 | scheme: schemeSchema, 307 | configuration: configurationSchema, 308 | derivedDataPath: derivedDataPathSchema, 309 | extraArgs: extraArgsSchema, 310 | preferXcodebuild: preferXcodebuildSchema, 311 | }, 312 | async (params) => 313 | _handleMacOSBuildAndRunLogic({ 314 | ...params, 315 | configuration: params.configuration ?? 'Debug', 316 | preferXcodebuild: params.preferXcodebuild ?? false, 317 | }), 318 | ); 319 | } 320 | 321 | // Register macOS build and run project tool 322 | export function registerMacOSBuildAndRunProjectTool(server: McpServer): void { 323 | type ProjectParams = { 324 | projectPath: string; 325 | scheme: string; 326 | configuration?: string; 327 | derivedDataPath?: string; 328 | arch?: string; 329 | extraArgs?: string[]; 330 | preferXcodebuild?: boolean; 331 | }; 332 | 333 | registerTool( 334 | server, 335 | 'build_run_mac_proj', 336 | 'Builds and runs a macOS app from a project file in one step.', 337 | { 338 | projectPath: projectPathSchema, 339 | scheme: schemeSchema, 340 | configuration: configurationSchema, 341 | derivedDataPath: derivedDataPathSchema, 342 | extraArgs: extraArgsSchema, 343 | preferXcodebuild: preferXcodebuildSchema, 344 | }, 345 | async (params) => 346 | _handleMacOSBuildAndRunLogic({ 347 | ...params, 348 | configuration: params.configuration ?? 'Debug', 349 | preferXcodebuild: params.preferXcodebuild ?? false, 350 | }), 351 | ); 352 | } 353 | -------------------------------------------------------------------------------- /src/tools/build_settings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Build Settings and Scheme Tools - Tools for viewing build settings and listing schemes 3 | * 4 | * This module provides tools for retrieving build settings and listing available schemes 5 | * from Xcode projects and workspaces. These tools are useful for debugging and exploring 6 | * project configuration. 7 | * 8 | * Responsibilities: 9 | * - Listing available schemes in Xcode projects and workspaces 10 | * - Retrieving detailed build settings for specific schemes 11 | * - Providing formatted output for build settings 12 | * - Supporting both project and workspace-based operations 13 | */ 14 | 15 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 16 | import { log } from '../utils/logger.js'; 17 | import { executeCommand } from '../utils/command.js'; 18 | import { validateRequiredParam, createTextResponse } from '../utils/validation.js'; 19 | import { ToolResponse } from '../types/common.js'; 20 | import { 21 | registerTool, 22 | workspacePathSchema, 23 | projectPathSchema, 24 | schemeSchema, 25 | BaseWorkspaceParams, 26 | BaseProjectParams, 27 | } from './common.js'; 28 | 29 | // --- Private Helper Functions --- 30 | 31 | /** 32 | * Internal logic for showing build settings. 33 | */ 34 | async function _handleShowBuildSettingsLogic(params: { 35 | workspacePath?: string; 36 | projectPath?: string; 37 | scheme: string; 38 | }): Promise { 39 | log('info', `Showing build settings for scheme ${params.scheme}`); 40 | 41 | try { 42 | // Create the command array for xcodebuild 43 | const command = ['xcodebuild', '-showBuildSettings']; // -showBuildSettings as an option, not an action 44 | 45 | // Add the workspace or project 46 | if (params.workspacePath) { 47 | command.push('-workspace', params.workspacePath); 48 | } else if (params.projectPath) { 49 | command.push('-project', params.projectPath); 50 | } 51 | 52 | // Add the scheme 53 | command.push('-scheme', params.scheme); 54 | 55 | // Execute the command directly 56 | const result = await executeCommand(command, 'Show Build Settings'); 57 | 58 | if (!result.success) { 59 | return createTextResponse(`Failed to show build settings: ${result.error}`, true); 60 | } 61 | 62 | return { 63 | content: [ 64 | { 65 | type: 'text', 66 | text: `✅ Build settings for scheme ${params.scheme}:`, 67 | }, 68 | { 69 | type: 'text', 70 | text: result.output || 'Build settings retrieved successfully.', 71 | }, 72 | ], 73 | }; 74 | } catch (error) { 75 | const errorMessage = error instanceof Error ? error.message : String(error); 76 | log('error', `Error showing build settings: ${errorMessage}`); 77 | return createTextResponse(`Error showing build settings: ${errorMessage}`, true); 78 | } 79 | } 80 | 81 | /** 82 | * Internal logic for listing schemes. 83 | */ 84 | async function _handleListSchemesLogic(params: { 85 | workspacePath?: string; 86 | projectPath?: string; 87 | }): Promise { 88 | log('info', 'Listing schemes'); 89 | 90 | try { 91 | // For listing schemes, we can't use executeXcodeBuild directly since it's not a standard action 92 | // We need to create a custom command with -list flag 93 | const command = ['xcodebuild', '-list']; 94 | 95 | if (params.workspacePath) { 96 | command.push('-workspace', params.workspacePath); 97 | } else if (params.projectPath) { 98 | command.push('-project', params.projectPath); 99 | } // No else needed, one path is guaranteed by callers 100 | 101 | const result = await executeCommand(command, 'List Schemes'); 102 | 103 | if (!result.success) { 104 | return createTextResponse(`Failed to list schemes: ${result.error}`, true); 105 | } 106 | 107 | // Extract schemes from the output 108 | const schemesMatch = result.output.match(/Schemes:([\s\S]*?)(?=\n\n|$)/); 109 | 110 | if (!schemesMatch) { 111 | return createTextResponse('No schemes found in the output', true); 112 | } 113 | 114 | const schemeLines = schemesMatch[1].trim().split('\n'); 115 | const schemes = schemeLines.map((line) => line.trim()).filter((line) => line); 116 | 117 | // Prepare next steps with the first scheme if available 118 | let nextStepsText = ''; 119 | if (schemes.length > 0) { 120 | const firstScheme = schemes[0]; 121 | const projectOrWorkspace = params.workspacePath ? 'workspace' : 'project'; 122 | const path = params.workspacePath || params.projectPath; 123 | 124 | nextStepsText = `Next Steps: 125 | 1. Build the app: ${projectOrWorkspace === 'workspace' ? 'macos_build_workspace' : 'macos_build_project'}({ ${projectOrWorkspace}Path: "${path}", scheme: "${firstScheme}" }) 126 | or for iOS: ${projectOrWorkspace === 'workspace' ? 'ios_simulator_build_by_name_workspace' : 'ios_simulator_build_by_name_project'}({ ${projectOrWorkspace}Path: "${path}", scheme: "${firstScheme}", simulatorName: "iPhone 16" }) 127 | 2. Show build settings: ${projectOrWorkspace === 'workspace' ? 'show_build_set_ws' : 'show_build_set_proj'}({ ${projectOrWorkspace}Path: "${path}", scheme: "${firstScheme}" })`; 128 | } 129 | 130 | return { 131 | content: [ 132 | { 133 | type: 'text', 134 | text: `✅ Available schemes:`, 135 | }, 136 | { 137 | type: 'text', 138 | text: schemes.join('\n'), 139 | }, 140 | { 141 | type: 'text', 142 | text: nextStepsText, 143 | }, 144 | ], 145 | }; 146 | } catch (error) { 147 | const errorMessage = error instanceof Error ? error.message : String(error); 148 | log('error', `Error listing schemes: ${errorMessage}`); 149 | return createTextResponse(`Error listing schemes: ${errorMessage}`, true); 150 | } 151 | } 152 | 153 | // --- Public Tool Definitions --- 154 | 155 | /** 156 | * Registers the show build settings workspace tool 157 | */ 158 | export function registerShowBuildSettingsWorkspaceTool(server: McpServer): void { 159 | registerTool( 160 | server, 161 | 'show_build_set_ws', 162 | "Shows build settings from a workspace using xcodebuild. IMPORTANT: Requires workspacePath and scheme. Example: show_build_set_ws({ workspacePath: '/path/to/MyProject.xcworkspace', scheme: 'MyScheme' })", 163 | { 164 | workspacePath: workspacePathSchema, 165 | scheme: schemeSchema, 166 | }, 167 | async (params: BaseWorkspaceParams) => { 168 | // Validate required parameters 169 | const workspaceValidation = validateRequiredParam('workspacePath', params.workspacePath); 170 | if (!workspaceValidation.isValid) return workspaceValidation.errorResponse!; 171 | 172 | const schemeValidation = validateRequiredParam('scheme', params.scheme); 173 | if (!schemeValidation.isValid) return schemeValidation.errorResponse!; 174 | 175 | return _handleShowBuildSettingsLogic(params); 176 | }, 177 | ); 178 | } 179 | 180 | /** 181 | * Registers the show build settings project tool 182 | */ 183 | export function registerShowBuildSettingsProjectTool(server: McpServer): void { 184 | registerTool( 185 | server, 186 | 'show_build_set_proj', 187 | "Shows build settings from a project file using xcodebuild. IMPORTANT: Requires projectPath and scheme. Example: show_build_set_proj({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme' })", 188 | { 189 | projectPath: projectPathSchema, 190 | scheme: schemeSchema, 191 | }, 192 | async (params: BaseProjectParams) => { 193 | // Validate required parameters 194 | const projectValidation = validateRequiredParam('projectPath', params.projectPath); 195 | if (!projectValidation.isValid) return projectValidation.errorResponse!; 196 | 197 | const schemeValidation = validateRequiredParam('scheme', params.scheme); 198 | if (!schemeValidation.isValid) return schemeValidation.errorResponse!; 199 | 200 | return _handleShowBuildSettingsLogic(params); 201 | }, 202 | ); 203 | } 204 | 205 | /** 206 | * Registers the list schemes workspace tool 207 | */ 208 | export function registerListSchemesWorkspaceTool(server: McpServer): void { 209 | registerTool( 210 | server, 211 | 'list_schems_ws', 212 | "Lists available schemes in the workspace. IMPORTANT: Requires workspacePath. Example: list_schems_ws({ workspacePath: '/path/to/MyProject.xcworkspace' })", 213 | { 214 | workspacePath: workspacePathSchema, 215 | }, 216 | async (params: BaseWorkspaceParams) => { 217 | // Validate required parameters 218 | const workspaceValidation = validateRequiredParam('workspacePath', params.workspacePath); 219 | if (!workspaceValidation.isValid) return workspaceValidation.errorResponse!; 220 | 221 | return _handleListSchemesLogic(params); 222 | }, 223 | ); 224 | } 225 | 226 | /** 227 | * Registers the list schemes project tool 228 | */ 229 | export function registerListSchemesProjectTool(server: McpServer): void { 230 | registerTool( 231 | server, 232 | 'list_schems_proj', 233 | "Lists available schemes in the project file. IMPORTANT: Requires projectPath. Example: list_schems_proj({ projectPath: '/path/to/MyProject.xcodeproj' })", 234 | { 235 | projectPath: projectPathSchema, 236 | }, 237 | async (params: BaseProjectParams) => { 238 | // Validate required parameters 239 | const projectValidation = validateRequiredParam('projectPath', params.projectPath); 240 | if (!projectValidation.isValid) return projectValidation.errorResponse!; 241 | 242 | return _handleListSchemesLogic(params); 243 | }, 244 | ); 245 | } 246 | -------------------------------------------------------------------------------- /src/tools/bundleId.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Bundle ID Tools - Extract bundle identifiers from app bundles 3 | * 4 | * This module provides tools for extracting bundle identifiers from iOS and macOS 5 | * application bundles (.app directories). Bundle IDs are required for launching 6 | * and installing applications. 7 | * 8 | * Responsibilities: 9 | * - Extracting bundle IDs from macOS app bundles 10 | * - Extracting bundle IDs from iOS app bundles 11 | * - Validating app bundle paths 12 | * - Providing formatted responses with next steps 13 | */ 14 | 15 | import { z } from 'zod'; 16 | import { log } from '../utils/logger.js'; 17 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 18 | import { validateRequiredParam, validateFileExists } from '../utils/validation.js'; 19 | import { ToolResponse } from '../types/common.js'; 20 | import { execSync } from 'child_process'; 21 | 22 | /** 23 | * Extracts the bundle identifier from a macOS app bundle (.app). IMPORTANT: You MUST provide the appPath parameter. Example: get_mac_bundle_id({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_get_macos_bundle_id. 24 | */ 25 | export function registerGetMacOSBundleIdTool(server: McpServer): void { 26 | server.tool( 27 | 'get_mac_bundle_id', 28 | "Extracts the bundle identifier from a macOS app bundle (.app). IMPORTANT: You MUST provide the appPath parameter. Example: get_mac_bundle_id({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_get_macos_bundle_id.", 29 | { 30 | appPath: z 31 | .string() 32 | .describe( 33 | 'Path to the macOS .app bundle to extract bundle ID from (full path to the .app directory)', 34 | ), 35 | }, 36 | async (params): Promise => { 37 | const appPathValidation = validateRequiredParam('appPath', params.appPath); 38 | if (!appPathValidation.isValid) { 39 | return appPathValidation.errorResponse!; 40 | } 41 | 42 | const appPathExistsValidation = validateFileExists(params.appPath); 43 | if (!appPathExistsValidation.isValid) { 44 | return appPathExistsValidation.errorResponse!; 45 | } 46 | 47 | log('info', `Starting bundle ID extraction for macOS app: ${params.appPath}`); 48 | 49 | try { 50 | let bundleId; 51 | 52 | try { 53 | bundleId = execSync(`defaults read "${params.appPath}/Contents/Info" CFBundleIdentifier`) 54 | .toString() 55 | .trim(); 56 | } catch { 57 | try { 58 | bundleId = execSync( 59 | `/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${params.appPath}/Contents/Info.plist"`, 60 | ) 61 | .toString() 62 | .trim(); 63 | } catch (innerError: unknown) { 64 | throw new Error( 65 | `Could not extract bundle ID from Info.plist: ${innerError instanceof Error ? innerError.message : String(innerError)}`, 66 | ); 67 | } 68 | } 69 | 70 | log('info', `Extracted macOS bundle ID: ${bundleId}`); 71 | 72 | return { 73 | content: [ 74 | { 75 | type: 'text', 76 | text: ` Bundle ID for macOS app: ${bundleId}`, 77 | }, 78 | { 79 | type: 'text', 80 | text: `Next Steps: 81 | - Launch the app: launch_macos_app({ appPath: "${params.appPath}" })`, 82 | }, 83 | ], 84 | }; 85 | } catch (error) { 86 | const errorMessage = error instanceof Error ? error.message : String(error); 87 | log('error', `Error extracting macOS bundle ID: ${errorMessage}`); 88 | 89 | return { 90 | content: [ 91 | { 92 | type: 'text', 93 | text: `Error extracting iOS bundle ID: ${errorMessage}`, 94 | }, 95 | { 96 | type: 'text', 97 | text: `Make sure the path points to a valid macOS app bundle (.app directory).`, 98 | }, 99 | ], 100 | }; 101 | } 102 | }, 103 | ); 104 | } 105 | 106 | /** 107 | * Extracts the bundle identifier from an iOS app bundle (.app). IMPORTANT: You MUST provide the appPath parameter. Example: get_ios_bundle_id({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_get_ios_bundle_id. 108 | */ 109 | export function registerGetiOSBundleIdTool(server: McpServer): void { 110 | server.tool( 111 | 'get_ios_bundle_id', 112 | "Extracts the bundle identifier from an iOS app bundle (.app). IMPORTANT: You MUST provide the appPath parameter. Example: get_ios_bundle_id({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_get_ios_bundle_id.", 113 | { 114 | appPath: z 115 | .string() 116 | .describe( 117 | 'Path to the iOS .app bundle to extract bundle ID from (full path to the .app directory)', 118 | ), 119 | }, 120 | async (params): Promise => { 121 | const appPathValidation = validateRequiredParam('appPath', params.appPath); 122 | if (!appPathValidation.isValid) { 123 | return appPathValidation.errorResponse!; 124 | } 125 | 126 | const appPathExistsValidation = validateFileExists(params.appPath); 127 | if (!appPathExistsValidation.isValid) { 128 | return appPathExistsValidation.errorResponse!; 129 | } 130 | 131 | log('info', `Starting bundle ID extraction for iOS app: ${params.appPath}`); 132 | 133 | try { 134 | let bundleId; 135 | 136 | try { 137 | bundleId = execSync(`defaults read "${params.appPath}/Info" CFBundleIdentifier`) 138 | .toString() 139 | .trim(); 140 | } catch { 141 | try { 142 | bundleId = execSync( 143 | `/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${params.appPath}/Info.plist"`, 144 | ) 145 | .toString() 146 | .trim(); 147 | } catch (innerError: unknown) { 148 | throw new Error( 149 | `Could not extract bundle ID from Info.plist: ${innerError instanceof Error ? innerError.message : String(innerError)}`, 150 | ); 151 | } 152 | } 153 | 154 | log('info', `Extracted iOS bundle ID: ${bundleId}`); 155 | 156 | return { 157 | content: [ 158 | { 159 | type: 'text', 160 | text: ` Bundle ID for iOS app: ${bundleId}`, 161 | }, 162 | { 163 | type: 'text', 164 | text: `Next Steps: 165 | - Launch in simulator: launch_app_in_simulator({ simulatorUuid: "YOUR_SIMULATOR_UUID", bundleId: "${bundleId}" })`, 166 | }, 167 | ], 168 | }; 169 | } catch (error) { 170 | const errorMessage = error instanceof Error ? error.message : String(error); 171 | log('error', `Error extracting iOS bundle ID: ${errorMessage}`); 172 | 173 | return { 174 | content: [ 175 | { 176 | type: 'text', 177 | text: `Error extracting iOS bundle ID: ${errorMessage}`, 178 | }, 179 | { 180 | type: 'text', 181 | text: `Make sure the path points to a valid iOS app bundle (.app directory).`, 182 | }, 183 | ], 184 | }; 185 | } 186 | }, 187 | ); 188 | } 189 | -------------------------------------------------------------------------------- /src/tools/clean.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Clean Tool - Uses xcodebuild's native clean action to clean build products 3 | * 4 | * This module provides tools for cleaning build products from Xcode projects and workspaces 5 | * using xcodebuild's native 'clean' action. Cleaning is important for ensuring fresh builds 6 | * and resolving certain build issues. 7 | * 8 | * Responsibilities: 9 | * - Cleaning build products from project files 10 | * - Cleaning build products from workspaces 11 | * - Supporting configuration-specific cleaning 12 | * - Handling derived data path specification 13 | */ 14 | 15 | import { z } from 'zod'; 16 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 17 | import { log } from '../utils/logger.js'; 18 | import { XcodePlatform } from '../utils/xcode.js'; 19 | import { ToolResponse } from '../types/common.js'; 20 | import { executeXcodeBuildCommand } from '../utils/build-utils.js'; 21 | 22 | // --- Private Helper Function --- 23 | 24 | /** 25 | * Internal logic for cleaning build products. 26 | */ 27 | async function _handleCleanLogic(params: { 28 | workspacePath?: string; 29 | projectPath?: string; 30 | scheme?: string; 31 | configuration?: string; 32 | derivedDataPath?: string; 33 | extraArgs?: string[]; 34 | }): Promise { 35 | log('info', 'Starting xcodebuild clean request (internal)'); 36 | 37 | // For clean operations, we need to provide a default platform and configuration 38 | return executeXcodeBuildCommand( 39 | { 40 | ...params, 41 | scheme: params.scheme || '', // Empty string if not provided 42 | configuration: params.configuration || 'Debug', // Default to Debug if not provided 43 | }, 44 | { 45 | platform: XcodePlatform.macOS, // Default to macOS, but this doesn't matter much for clean 46 | logPrefix: 'Clean', 47 | }, 48 | false, 49 | 'clean', // Specify 'clean' as the build action 50 | ); 51 | } 52 | 53 | // --- Public Tool Definitions --- 54 | 55 | export function registerCleanWorkspaceTool(server: McpServer): void { 56 | server.tool( 57 | 'clean_ws', 58 | "Cleans build products for a specific workspace using xcodebuild. IMPORTANT: Requires workspacePath. Scheme/Configuration are optional. Example: clean_ws({ workspacePath: '/path/to/MyProject.xcworkspace', scheme: 'MyScheme' })", 59 | { 60 | workspacePath: z.string().describe('Path to the .xcworkspace file (Required)'), 61 | scheme: z.string().optional().describe('Optional: The scheme to clean'), 62 | configuration: z 63 | .string() 64 | .optional() 65 | .describe('Optional: Build configuration to clean (Debug, Release, etc.)'), 66 | derivedDataPath: z 67 | .string() 68 | .optional() 69 | .describe('Optional: Path where derived data might be located'), 70 | extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'), 71 | }, 72 | (params) => _handleCleanLogic(params), 73 | ); 74 | } 75 | 76 | export function registerCleanProjectTool(server: McpServer): void { 77 | server.tool( 78 | 'clean_proj', 79 | "Cleans build products for a specific project file using xcodebuild. IMPORTANT: Requires projectPath. Scheme/Configuration are optional. Example: clean_proj({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme' })", 80 | { 81 | projectPath: z.string().describe('Path to the .xcodeproj file (Required)'), 82 | scheme: z.string().optional().describe('Optional: The scheme to clean'), 83 | configuration: z 84 | .string() 85 | .optional() 86 | .describe('Optional: Build configuration to clean (Debug, Release, etc.)'), 87 | derivedDataPath: z 88 | .string() 89 | .optional() 90 | .describe('Optional: Path where derived data might be located'), 91 | extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'), 92 | }, 93 | (params) => _handleCleanLogic(params), 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /src/tools/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Common types and utilities shared across build tool modules 3 | * 4 | * This module provides shared parameter schemas, types, and utility functions used by 5 | * multiple tool modules. Centralizing these definitions ensures consistency across 6 | * the codebase and simplifies maintenance. 7 | * 8 | * Responsibilities: 9 | * - Defining common parameter schemas with descriptive documentation 10 | * - Providing base parameter interfaces for workspace and project operations 11 | * - Implementing shared tool registration utilities 12 | * - Standardizing response formatting across tools 13 | */ 14 | 15 | import { z } from 'zod'; 16 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 17 | import { ToolResponse, ToolResponseContent, XcodePlatform } from '../types/common.js'; 18 | 19 | /** 20 | * Common parameter schemas used across multiple tools 21 | */ 22 | export const workspacePathSchema = z.string().describe('Path to the .xcworkspace file (Required)'); 23 | export const projectPathSchema = z.string().describe('Path to the .xcodeproj file (Required)'); 24 | export const schemeSchema = z.string().describe('The scheme to use (Required)'); 25 | export const configurationSchema = z 26 | .string() 27 | .optional() 28 | .describe('Build configuration (Debug, Release, etc.)'); 29 | export const derivedDataPathSchema = z 30 | .string() 31 | .optional() 32 | .describe('Path where build products and other derived data will go'); 33 | export const extraArgsSchema = z 34 | .array(z.string()) 35 | .optional() 36 | .describe('Additional xcodebuild arguments'); 37 | export const simulatorNameSchema = z 38 | .string() 39 | .describe("Name of the simulator to use (e.g., 'iPhone 16') (Required)"); 40 | export const simulatorIdSchema = z 41 | .string() 42 | .describe('UUID of the simulator to use (obtained from listSimulators) (Required)'); 43 | export const useLatestOSSchema = z 44 | .boolean() 45 | .optional() 46 | .describe('Whether to use the latest OS version for the named simulator'); 47 | export const appPathSchema = z 48 | .string() 49 | .describe('Path to the .app bundle (full path to the .app directory)'); 50 | export const bundleIdSchema = z 51 | .string() 52 | .describe("Bundle identifier of the app (e.g., 'com.example.MyApp')"); 53 | export const launchArgsSchema = z 54 | .array(z.string()) 55 | .optional() 56 | .describe('Additional arguments to pass to the app'); 57 | export const preferXcodebuildSchema = z 58 | .boolean() 59 | .optional() 60 | .describe( 61 | 'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.', 62 | ); 63 | 64 | export const platformDeviceSchema = z 65 | .enum([ 66 | XcodePlatform.macOS, 67 | XcodePlatform.iOS, 68 | XcodePlatform.watchOS, 69 | XcodePlatform.tvOS, 70 | XcodePlatform.visionOS, 71 | ]) 72 | .describe('The target device platform (Required)'); 73 | 74 | export const platformSimulatorSchema = z 75 | .enum([ 76 | XcodePlatform.iOSSimulator, 77 | XcodePlatform.watchOSSimulator, 78 | XcodePlatform.tvOSSimulator, 79 | XcodePlatform.visionOSSimulator, 80 | ]) 81 | .describe('The target simulator platform (Required)'); 82 | 83 | /** 84 | * Swift Package Manager specific schemas 85 | */ 86 | export const swiftConfigurationSchema = z 87 | .enum(['debug', 'release']) 88 | .optional() 89 | .describe("Build configuration: 'debug' (default) or 'release'"); 90 | 91 | export const swiftArchitecturesSchema = z 92 | .enum(['arm64', 'x86_64']) 93 | .array() 94 | .optional() 95 | .describe('Architectures to build for (e.g. arm64, x86_64)'); 96 | 97 | export const parseAsLibrarySchema = z 98 | .boolean() 99 | .optional() 100 | .describe('Add -parse-as-library flag for @main support (default: false)'); 101 | 102 | /** 103 | * Base parameters for workspace tools 104 | */ 105 | export type BaseWorkspaceParams = { 106 | workspacePath: string; 107 | scheme: string; 108 | configuration?: string; 109 | derivedDataPath?: string; 110 | extraArgs?: string[]; 111 | preferXcodebuild?: boolean; 112 | }; 113 | 114 | /** 115 | * Base parameters for project tools 116 | */ 117 | export type BaseProjectParams = { 118 | projectPath: string; 119 | scheme: string; 120 | configuration?: string; 121 | derivedDataPath?: string; 122 | extraArgs?: string[]; 123 | preferXcodebuild?: boolean; 124 | }; 125 | 126 | /** 127 | * Base parameters for simulator tools with name 128 | */ 129 | export type BaseSimulatorNameParams = { 130 | simulatorName: string; 131 | useLatestOS?: boolean; 132 | }; 133 | 134 | /** 135 | * Base parameters for simulator tools with ID 136 | */ 137 | export type BaseSimulatorIdParams = { 138 | simulatorId: string; 139 | useLatestOS?: boolean; // May be ignored by xcodebuild when ID is provided 140 | }; 141 | 142 | /** 143 | * Specific Parameter Types for App Path 144 | */ 145 | export type BaseAppPathDeviceParams = { 146 | platform: (typeof platformDeviceSchema._def.values)[number]; 147 | }; 148 | 149 | export type BaseAppPathSimulatorNameParams = BaseSimulatorNameParams & { 150 | platform: (typeof platformSimulatorSchema._def.values)[number]; 151 | }; 152 | 153 | export type BaseAppPathSimulatorIdParams = BaseSimulatorIdParams & { 154 | platform: (typeof platformSimulatorSchema._def.values)[number]; 155 | }; 156 | 157 | /** 158 | * Helper function to register a tool with the MCP server 159 | */ 160 | export function registerTool( 161 | server: McpServer, 162 | name: string, 163 | description: string, 164 | schema: Record, 165 | handler: (params: T) => Promise, 166 | ): void { 167 | // Create a wrapper handler that matches the signature expected by server.tool 168 | const wrappedHandler = ( 169 | args: Record, 170 | _extra: unknown, 171 | ): Promise => { 172 | // Assert the type *before* calling the original handler 173 | // This confines the type assertion to one place 174 | const typedParams = args as T; 175 | return handler(typedParams); 176 | }; 177 | 178 | server.tool(name, description, schema, wrappedHandler); 179 | } 180 | 181 | /** 182 | * Helper to create a standard text response content. 183 | */ 184 | export function createTextContent(text: string): ToolResponseContent { 185 | return { type: 'text', text }; 186 | } 187 | -------------------------------------------------------------------------------- /src/tools/discover_projects.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Project Discovery Tools - Find Xcode projects and workspaces 3 | * 4 | * This module provides tools for scanning directories to discover Xcode project (.xcodeproj) 5 | * and workspace (.xcworkspace) files. This is useful for initial project exploration and 6 | * for identifying available projects to work with. 7 | * 8 | * Responsibilities: 9 | * - Recursively scanning directories for Xcode projects and workspaces 10 | * - Filtering out common directories that should be skipped (build, DerivedData, etc.) 11 | * - Respecting maximum depth limits to prevent excessive scanning 12 | * - Providing formatted output with absolute paths for discovered files 13 | */ 14 | 15 | import { z } from 'zod'; 16 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 17 | import { log } from '../utils/logger.js'; 18 | import { ToolResponse } from '../types/common.js'; 19 | import path from 'node:path'; 20 | import fs from 'node:fs/promises'; 21 | import { createTextContent } from './common.js'; 22 | 23 | // Constants 24 | const DEFAULT_MAX_DEPTH = 5; 25 | const SKIPPED_DIRS = new Set(['build', 'DerivedData', 'Pods', '.git', 'node_modules']); 26 | 27 | // Type definition for parameters 28 | type DiscoverProjectsParams = { 29 | scanPath?: string; 30 | maxDepth: number; 31 | workspaceRoot: string; 32 | }; 33 | 34 | // --- Private Helper Function --- 35 | 36 | /** 37 | * Recursively scans directories to find Xcode projects and workspaces. 38 | */ 39 | async function _findProjectsRecursive( 40 | currentDirAbs: string, 41 | workspaceRootAbs: string, 42 | currentDepth: number, 43 | maxDepth: number, 44 | results: { projects: string[]; workspaces: string[] }, 45 | ): Promise { 46 | // Explicit depth check (now simplified as maxDepth is always non-negative) 47 | if (currentDepth >= maxDepth) { 48 | log('debug', `Max depth ${maxDepth} reached at ${currentDirAbs}, stopping recursion.`); 49 | return; 50 | } 51 | 52 | log('debug', `Scanning directory: ${currentDirAbs} at depth ${currentDepth}`); 53 | const normalizedWorkspaceRoot = path.normalize(workspaceRootAbs); 54 | 55 | try { 56 | const entries = await fs.readdir(currentDirAbs, { withFileTypes: true }); 57 | for (const entry of entries) { 58 | const absoluteEntryPath = path.join(currentDirAbs, entry.name); 59 | const relativePath = path.relative(workspaceRootAbs, absoluteEntryPath); 60 | 61 | // --- Skip conditions --- 62 | if (entry.isSymbolicLink()) { 63 | log('debug', `Skipping symbolic link: ${relativePath}`); 64 | continue; 65 | } 66 | 67 | // Skip common build/dependency directories by name 68 | if (entry.isDirectory() && SKIPPED_DIRS.has(entry.name)) { 69 | log('debug', `Skipping standard directory: ${relativePath}`); 70 | continue; 71 | } 72 | 73 | // Ensure entry is within the workspace root (security/sanity check) 74 | if (!path.normalize(absoluteEntryPath).startsWith(normalizedWorkspaceRoot)) { 75 | log( 76 | 'warn', 77 | `Skipping entry outside workspace root: ${absoluteEntryPath} (Workspace: ${workspaceRootAbs})`, 78 | ); 79 | continue; 80 | } 81 | 82 | // --- Process entries --- 83 | if (entry.isDirectory()) { 84 | let isXcodeBundle = false; 85 | 86 | if (entry.name.endsWith('.xcodeproj')) { 87 | results.projects.push(absoluteEntryPath); // Use absolute path 88 | log('debug', `Found project: ${absoluteEntryPath}`); 89 | isXcodeBundle = true; 90 | } else if (entry.name.endsWith('.xcworkspace')) { 91 | results.workspaces.push(absoluteEntryPath); // Use absolute path 92 | log('debug', `Found workspace: ${absoluteEntryPath}`); 93 | isXcodeBundle = true; 94 | } 95 | 96 | // Recurse into regular directories, but not into found project/workspace bundles 97 | if (!isXcodeBundle) { 98 | await _findProjectsRecursive( 99 | absoluteEntryPath, 100 | workspaceRootAbs, 101 | currentDepth + 1, 102 | maxDepth, 103 | results, 104 | ); 105 | } 106 | } 107 | } 108 | } catch (error: unknown) { 109 | let code: string | undefined; 110 | let message = 'Unknown error'; 111 | 112 | if (error instanceof Error) { 113 | message = error.message; 114 | if ('code' in error) { 115 | code = (error as NodeJS.ErrnoException).code; 116 | } 117 | } else if (typeof error === 'object' && error !== null) { 118 | if ('message' in error && typeof error.message === 'string') { 119 | message = error.message; 120 | } 121 | if ('code' in error && typeof error.code === 'string') { 122 | code = error.code; 123 | } 124 | } else { 125 | message = String(error); 126 | } 127 | 128 | if (code === 'EPERM' || code === 'EACCES') { 129 | log('debug', `Permission denied scanning directory: ${currentDirAbs}`); 130 | } else { 131 | log( 132 | 'warning', 133 | `Error scanning directory ${currentDirAbs}: ${message} (Code: ${code ?? 'N/A'})`, 134 | ); 135 | } 136 | } 137 | } 138 | 139 | /** 140 | * Internal logic for discovering projects. 141 | */ 142 | async function _handleDiscoveryLogic(params: DiscoverProjectsParams): Promise { 143 | const { scanPath: relativeScanPath, maxDepth, workspaceRoot } = params; 144 | 145 | // Calculate and validate the absolute scan path 146 | const requestedScanPath = path.resolve(workspaceRoot, relativeScanPath || '.'); 147 | let absoluteScanPath = requestedScanPath; 148 | const normalizedWorkspaceRoot = path.normalize(workspaceRoot); 149 | if (!path.normalize(absoluteScanPath).startsWith(normalizedWorkspaceRoot)) { 150 | log( 151 | 'warn', 152 | `Requested scan path '${relativeScanPath}' resolved outside workspace root '${workspaceRoot}'. Defaulting scan to workspace root.`, 153 | ); 154 | absoluteScanPath = normalizedWorkspaceRoot; 155 | } 156 | 157 | const results = { projects: [] as string[], workspaces: [] as string[] }; 158 | 159 | log( 160 | 'info', 161 | `Starting project discovery request: path=${absoluteScanPath}, maxDepth=${maxDepth}, workspace=${workspaceRoot}`, 162 | ); 163 | 164 | try { 165 | // Ensure the scan path exists and is a directory 166 | const stats = await fs.stat(absoluteScanPath); 167 | if (!stats.isDirectory()) { 168 | const errorMsg = `Scan path is not a directory: ${absoluteScanPath}`; 169 | log('error', errorMsg); 170 | // Return ToolResponse error format 171 | return { 172 | content: [createTextContent(errorMsg)], 173 | isError: true, 174 | }; 175 | } 176 | } catch (error: unknown) { 177 | let code: string | undefined; 178 | let message = 'Unknown error accessing scan path'; 179 | 180 | // Type guards - refined 181 | if (error instanceof Error) { 182 | message = error.message; 183 | // Check for code property specific to Node.js fs errors 184 | if ('code' in error) { 185 | code = (error as NodeJS.ErrnoException).code; 186 | } 187 | } else if (typeof error === 'object' && error !== null) { 188 | if ('message' in error && typeof error.message === 'string') { 189 | message = error.message; 190 | } 191 | if ('code' in error && typeof error.code === 'string') { 192 | code = error.code; 193 | } 194 | } else { 195 | message = String(error); 196 | } 197 | 198 | const errorMsg = `Failed to access scan path: ${absoluteScanPath}. Error: ${message}`; 199 | log('error', `${errorMsg} - Code: ${code ?? 'N/A'}`); 200 | return { 201 | content: [createTextContent(errorMsg)], 202 | isError: true, 203 | }; 204 | } 205 | 206 | // Start the recursive scan from the validated absolute path 207 | await _findProjectsRecursive(absoluteScanPath, workspaceRoot, 0, maxDepth, results); 208 | 209 | log( 210 | 'info', 211 | `Discovery finished. Found ${results.projects.length} projects and ${results.workspaces.length} workspaces.`, 212 | ); 213 | 214 | const responseContent = [ 215 | createTextContent( 216 | `Discovery finished. Found ${results.projects.length} projects and ${results.workspaces.length} workspaces.`, 217 | ), 218 | ]; 219 | 220 | // Sort results for consistent output 221 | results.projects.sort(); 222 | results.workspaces.sort(); 223 | 224 | if (results.projects.length > 0) { 225 | responseContent.push( 226 | createTextContent(`Projects found:\n - ${results.projects.join('\n - ')}`), 227 | ); 228 | } 229 | 230 | if (results.workspaces.length > 0) { 231 | responseContent.push( 232 | createTextContent(`Workspaces found:\n - ${results.workspaces.join('\n - ')}`), 233 | ); 234 | } 235 | 236 | return { 237 | content: responseContent, 238 | projects: results.projects, 239 | workspaces: results.workspaces, 240 | isError: false, 241 | }; 242 | } 243 | 244 | // --- Public Tool Definition --- 245 | 246 | export function registerDiscoverProjectsTool(server: McpServer): void { 247 | server.tool( 248 | 'discover_projs', 249 | 'Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj) and workspace (.xcworkspace) files.', 250 | { 251 | workspaceRoot: z.string().describe('The absolute path of the workspace root to scan within.'), 252 | scanPath: z 253 | .string() 254 | .optional() 255 | .describe('Optional: Path relative to workspace root to scan. Defaults to workspace root.'), 256 | maxDepth: z.number().int().nonnegative().optional().default(DEFAULT_MAX_DEPTH).describe( 257 | `Optional: Maximum directory depth to scan. Defaults to ${DEFAULT_MAX_DEPTH}.`, // Removed mention of -1 258 | ), 259 | }, 260 | async (params) => { 261 | try { 262 | return await _handleDiscoveryLogic(params as DiscoverProjectsParams); 263 | } catch (error: unknown) { 264 | let errorMessage = ''; 265 | if (error instanceof Error) { 266 | errorMessage = `An unexpected error occurred during project discovery: ${error.message}`; 267 | log('error', `${errorMessage}\n${error.stack ?? ''}`); 268 | } else { 269 | const errorString = String(error); 270 | log('error', `Caught non-Error value during project discovery: ${errorString}`); 271 | errorMessage = `An unexpected non-error value was thrown: ${errorString}`; 272 | } 273 | return { 274 | content: [createTextContent(errorMessage)], 275 | isError: true, 276 | }; 277 | } 278 | }, 279 | ); 280 | } 281 | -------------------------------------------------------------------------------- /src/tools/launch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Launch Tools - Tools for launching macOS and iOS applications 3 | * 4 | * This module provides tools for launching applications on macOS and in iOS simulators. 5 | * It handles the platform-specific launch commands and provides appropriate validation 6 | * and error handling. 7 | * 8 | * Responsibilities: 9 | * - Launching macOS applications using the 'open' command 10 | * - Launching iOS applications in simulators using 'simctl launch' 11 | * - Validating application paths and bundle identifiers 12 | * - Supporting command-line arguments for launched applications 13 | */ 14 | 15 | import { z } from 'zod'; 16 | import { log } from '../utils/logger.js'; 17 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 18 | import { validateRequiredParam, validateFileExists } from '../utils/validation.js'; 19 | import { ToolResponse } from '../types/common.js'; 20 | import { promisify } from 'util'; 21 | import { exec } from 'child_process'; 22 | 23 | const execPromise = promisify(exec); 24 | 25 | /** 26 | * Launches a macOS application using the 'open' command. 27 | * IMPORTANT: You MUST provide the appPath parameter. 28 | * Example: launch_macos_app({ appPath: '/path/to/your/app.app' }) 29 | * Note: In some environments, this tool may be prefixed as mcp0_launch_macos_app. 30 | */ 31 | export function registerLaunchMacOSAppTool(server: McpServer): void { 32 | server.tool( 33 | 'launch_mac_app', 34 | "Launches a macOS application. IMPORTANT: You MUST provide the appPath parameter. Example: launch_mac_app({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_launch_macos_app.", 35 | { 36 | appPath: z 37 | .string() 38 | .describe('Path to the macOS .app bundle to launch (full path to the .app directory)'), 39 | args: z.array(z.string()).optional().describe('Additional arguments to pass to the app'), 40 | }, 41 | async (params): Promise => { 42 | // Validate required parameters 43 | const appPathValidation = validateRequiredParam('appPath', params.appPath); 44 | if (!appPathValidation.isValid) { 45 | return appPathValidation.errorResponse!; 46 | } 47 | 48 | // Validate that the app file exists 49 | const fileExistsValidation = await validateFileExists(params.appPath); 50 | if (!fileExistsValidation.isValid) { 51 | return fileExistsValidation.errorResponse!; 52 | } 53 | 54 | log('info', `Starting launch macOS app request for ${params.appPath}`); 55 | 56 | try { 57 | // Construct the command 58 | let command = `open "${params.appPath}"`; 59 | 60 | // Add any additional arguments if provided 61 | if (params.args && params.args.length > 0) { 62 | command += ` --args ${params.args.join(' ')}`; 63 | } 64 | 65 | // Execute the command 66 | await execPromise(command); 67 | 68 | // Return success response 69 | return { 70 | content: [ 71 | { 72 | type: 'text', 73 | text: `✅ macOS app launched successfully: ${params.appPath}`, 74 | }, 75 | ], 76 | }; 77 | } catch (error) { 78 | // Handle errors 79 | const errorMessage = error instanceof Error ? error.message : String(error); 80 | log('error', `Error during launch macOS app operation: ${errorMessage}`); 81 | return { 82 | content: [ 83 | { 84 | type: 'text', 85 | text: `❌ Launch macOS app operation failed: ${errorMessage}`, 86 | }, 87 | ], 88 | }; 89 | } 90 | }, 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /src/tools/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Log Tools - Functions for capturing and managing iOS simulator logs 3 | * 4 | * This module provides tools for capturing and managing logs from iOS simulators. 5 | * It supports starting and stopping log capture sessions, and retrieving captured logs. 6 | * 7 | * Responsibilities: 8 | * - Starting and stopping log capture sessions 9 | * - Managing in-memory log sessions 10 | * - Retrieving captured logs 11 | */ 12 | 13 | import { startLogCapture, stopLogCapture } from '../utils/log_capture.js'; 14 | import { z } from 'zod'; 15 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 16 | import { ToolResponse } from '../types/common.js'; 17 | import { validateRequiredParam } from '../utils/validation.js'; 18 | import { registerTool, createTextContent } from './common.js'; 19 | 20 | /** 21 | * Registers the tool to start capturing logs from an iOS simulator. 22 | * 23 | * @param server The MCP Server instance. 24 | */ 25 | export function registerStartSimulatorLogCaptureTool(server: McpServer): void { 26 | const schema = { 27 | simulatorUuid: z 28 | .string() 29 | .describe('UUID of the simulator to capture logs from (obtained from list_simulators).'), 30 | bundleId: z.string().describe('Bundle identifier of the app to capture logs for.'), 31 | captureConsole: z 32 | .boolean() 33 | .optional() 34 | .default(false) 35 | .describe('Whether to capture console output (requires app relaunch).'), 36 | }; 37 | 38 | async function handler(params: { 39 | simulatorUuid: string; 40 | bundleId: string; 41 | captureConsole?: boolean; 42 | }): Promise { 43 | const validationResult = validateRequiredParam('simulatorUuid', params.simulatorUuid); 44 | if (!validationResult.isValid) { 45 | return validationResult.errorResponse!; 46 | } 47 | 48 | const { sessionId, error } = await startLogCapture(params); 49 | if (error) { 50 | return { 51 | content: [createTextContent(`Error starting log capture: ${error}`)], 52 | isError: true, 53 | }; 54 | } 55 | return { 56 | content: [ 57 | createTextContent( 58 | `Log capture started successfully. Session ID: ${sessionId}.\n\n${params.captureConsole ? 'Note: Your app was relaunched to capture console output.' : 'Note: Only structured logs are being captured.'}\n\nNext Steps:\n1. Interact with your simulator and app.\n2. Use 'stop_sim_log_cap' with session ID '${sessionId}' to stop capture and retrieve logs.`, 59 | ), 60 | ], 61 | }; 62 | } 63 | 64 | registerTool( 65 | server, 66 | 'start_sim_log_cap', 67 | 'Starts capturing logs from a specified simulator. Returns a session ID. By default, captures only structured logs.', 68 | schema, 69 | handler, 70 | ); 71 | } 72 | 73 | /** 74 | * Registers the tool to stop log capture and retrieve the content in one operation. 75 | * 76 | * @param server The MCP Server instance. 77 | */ 78 | export function registerStopAndGetSimulatorLogTool(server: McpServer): void { 79 | const schema = { 80 | logSessionId: z.string().describe('The session ID returned by start_sim_log_cap.'), 81 | }; 82 | 83 | async function handler(params: { logSessionId: string }): Promise { 84 | const validationResult = validateRequiredParam('logSessionId', params.logSessionId); 85 | if (!validationResult.isValid) { 86 | return validationResult.errorResponse!; 87 | } 88 | const { logContent, error } = await stopLogCapture(params.logSessionId); 89 | if (error) { 90 | return { 91 | content: [ 92 | createTextContent(`Error stopping log capture session ${params.logSessionId}: ${error}`), 93 | ], 94 | isError: true, 95 | }; 96 | } 97 | return { 98 | content: [ 99 | createTextContent( 100 | `Log capture session ${params.logSessionId} stopped successfully. Log content follows:\n\n${logContent}`, 101 | ), 102 | ], 103 | }; 104 | } 105 | 106 | registerTool( 107 | server, 108 | 'stop_sim_log_cap', 109 | 'Stops an active simulator log capture session and returns the captured logs.', 110 | schema, 111 | handler, 112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /src/tools/screenshot.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Screenshot Tool - Capture screenshots from iOS Simulator 3 | * 4 | * This module provides a tool to capture screenshots from the iOS Simulator 5 | * using xcrun simctl commands. It does not depend on AXe. 6 | */ 7 | import * as os from 'os'; 8 | import * as path from 'path'; 9 | import * as fs from 'fs/promises'; 10 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 11 | import { z } from 'zod'; 12 | import { v4 as uuidv4 } from 'uuid'; 13 | import { ToolResponse } from '../types/common.js'; 14 | import { log } from '../utils/logger.js'; 15 | import { validateRequiredParam } from '../utils/validation.js'; 16 | import { SystemError, createErrorResponse } from '../utils/errors.js'; 17 | import { executeCommand } from '../utils/command.js'; 18 | 19 | const LOG_PREFIX = '[Screenshot]'; 20 | 21 | /** 22 | * Registers the screenshot tool with the dispatcher. 23 | * @param server The McpServer instance. 24 | */ 25 | export function registerScreenshotTool(server: McpServer): void { 26 | server.tool( 27 | 'screenshot', 28 | "Captures screenshot for visual verification. For UI coordinates, use describe_ui instead (don't determine coordinates from screenshots).", 29 | { 30 | simulatorUuid: z.string().uuid('Invalid Simulator UUID format'), 31 | }, 32 | async (params): Promise => { 33 | const toolName = 'screenshot'; 34 | const simUuidValidation = validateRequiredParam('simulatorUuid', params.simulatorUuid); 35 | if (!simUuidValidation.isValid) return simUuidValidation.errorResponse!; 36 | 37 | const { simulatorUuid } = params; 38 | const tempDir = os.tmpdir(); 39 | const screenshotFilename = `screenshot_${uuidv4()}.png`; 40 | const screenshotPath = path.join(tempDir, screenshotFilename); 41 | // Use xcrun simctl to take screenshot 42 | const commandArgs = ['xcrun', 'simctl', 'io', simulatorUuid, 'screenshot', screenshotPath]; 43 | 44 | log( 45 | 'info', 46 | `${LOG_PREFIX}/${toolName}: Starting capture to ${screenshotPath} on ${simulatorUuid}`, 47 | ); 48 | 49 | try { 50 | // Execute the screenshot command 51 | const result = await executeCommand(commandArgs, `${LOG_PREFIX}: screenshot`, false); 52 | 53 | if (!result.success) { 54 | throw new SystemError(`Failed to capture screenshot: ${result.error || result.output}`); 55 | } 56 | 57 | log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`); 58 | 59 | try { 60 | // Read the image file into memory 61 | const imageBuffer = await fs.readFile(screenshotPath); 62 | 63 | // Encode the image as a Base64 string 64 | const base64Image = imageBuffer.toString('base64'); 65 | 66 | log('info', `${LOG_PREFIX}/${toolName}: Successfully encoded image as Base64`); 67 | 68 | // Clean up the temporary file 69 | await fs.unlink(screenshotPath).catch((err) => { 70 | log('warning', `${LOG_PREFIX}/${toolName}: Failed to delete temporary file: ${err}`); 71 | }); 72 | 73 | // Return the image directly in the tool response 74 | return { 75 | content: [ 76 | { 77 | type: 'image', 78 | data: base64Image, 79 | mimeType: 'image/png', 80 | }, 81 | ], 82 | }; 83 | } catch (fileError) { 84 | log('error', `${LOG_PREFIX}/${toolName}: Failed to process image file: ${fileError}`); 85 | return createErrorResponse( 86 | `Screenshot captured but failed to process image file: ${fileError instanceof Error ? fileError.message : String(fileError)}`, 87 | undefined, 88 | 'FileProcessingError', 89 | ); 90 | } 91 | } catch (_error) { 92 | log('error', `${LOG_PREFIX}/${toolName}: Failed - ${_error}`); 93 | if (_error instanceof SystemError) { 94 | return createErrorResponse( 95 | `System error executing screenshot: ${_error.message}`, 96 | _error.originalError?.stack, 97 | _error.name, 98 | ); 99 | } 100 | return createErrorResponse( 101 | `An unexpected error occurred: ${_error instanceof Error ? _error.message : String(_error)}`, 102 | undefined, 103 | 'UnexpectedError', 104 | ); 105 | } 106 | }, 107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /src/tools/test-swift-package.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 3 | import path from 'node:path'; 4 | import { registerTool, swiftConfigurationSchema, parseAsLibrarySchema } from './common.js'; 5 | import { executeCommand } from '../utils/command.js'; 6 | import { createTextResponse, validateRequiredParam } from '../utils/validation.js'; 7 | import { ToolResponse } from '../types/common.js'; 8 | import { createErrorResponse } from '../utils/errors.js'; 9 | import { log } from '../utils/logger.js'; 10 | 11 | export function registerTestSwiftPackageTool(server: McpServer): void { 12 | registerTool( 13 | server, 14 | 'swift_package_test', 15 | 'Runs tests for a Swift Package with swift test', 16 | { 17 | packagePath: z.string().describe('Path to the Swift package root (Required)'), 18 | testProduct: z.string().optional().describe('Optional specific test product to run'), 19 | filter: z.string().optional().describe('Filter tests by name (regex pattern)'), 20 | configuration: swiftConfigurationSchema, 21 | parallel: z.boolean().optional().describe('Run tests in parallel (default: true)'), 22 | showCodecov: z.boolean().optional().describe('Show code coverage (default: false)'), 23 | parseAsLibrary: parseAsLibrarySchema, 24 | }, 25 | async (params: { 26 | packagePath: string; 27 | testProduct?: string; 28 | filter?: string; 29 | configuration?: 'debug' | 'release'; 30 | parallel?: boolean; 31 | showCodecov?: boolean; 32 | parseAsLibrary?: boolean; 33 | }): Promise => { 34 | const pkgValidation = validateRequiredParam('packagePath', params.packagePath); 35 | if (!pkgValidation.isValid) return pkgValidation.errorResponse!; 36 | 37 | const resolvedPath = path.resolve(params.packagePath); 38 | const args: string[] = ['test', '--package-path', resolvedPath]; 39 | 40 | if (params.configuration && params.configuration.toLowerCase() === 'release') { 41 | args.push('-c', 'release'); 42 | } else if (params.configuration && params.configuration.toLowerCase() !== 'debug') { 43 | return createTextResponse("Invalid configuration. Use 'debug' or 'release'.", true); 44 | } 45 | 46 | if (params.testProduct) { 47 | args.push('--test-product', params.testProduct); 48 | } 49 | 50 | if (params.filter) { 51 | args.push('--filter', params.filter); 52 | } 53 | 54 | if (params.parallel === false) { 55 | args.push('--no-parallel'); 56 | } 57 | 58 | if (params.showCodecov) { 59 | args.push('--show-code-coverage'); 60 | } 61 | 62 | if (params.parseAsLibrary) { 63 | args.push('-Xswiftc', '-parse-as-library'); 64 | } 65 | 66 | log('info', `Running swift ${args.join(' ')}`); 67 | try { 68 | const result = await executeCommand(['swift', ...args], 'Swift Package Test'); 69 | if (!result.success) { 70 | const errorMessage = result.error || result.output || 'Unknown error'; 71 | return createErrorResponse('Swift package tests failed', errorMessage, 'TestError'); 72 | } 73 | 74 | return { 75 | content: [ 76 | { type: 'text', text: '✅ Swift package tests completed.' }, 77 | { 78 | type: 'text', 79 | text: '💡 Next: Execute your app with swift_package_run if tests passed', 80 | }, 81 | { type: 'text', text: result.output }, 82 | ], 83 | }; 84 | } catch (error) { 85 | const message = error instanceof Error ? error.message : String(error); 86 | log('error', `Swift package test failed: ${message}`); 87 | return createErrorResponse('Failed to execute swift test', message, 'SystemError'); 88 | } 89 | }, 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/types/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Common type definitions used across the server 3 | * 4 | * This module provides core type definitions and interfaces used throughout the codebase. 5 | * It establishes a consistent type system for platform identification, tool responses, 6 | * and other shared concepts. 7 | * 8 | * Responsibilities: 9 | * - Defining the XcodePlatform enum for platform identification 10 | * - Establishing the ToolResponse interface for standardized tool outputs 11 | * - Providing ToolResponseContent types for different response formats 12 | * - Supporting error handling with standardized error response types 13 | */ 14 | 15 | /** 16 | * Enum representing Xcode build platforms. 17 | */ 18 | export enum XcodePlatform { 19 | macOS = 'macOS', 20 | iOS = 'iOS', 21 | iOSSimulator = 'iOS Simulator', 22 | watchOS = 'watchOS', 23 | watchOSSimulator = 'watchOS Simulator', 24 | tvOS = 'tvOS', 25 | tvOSSimulator = 'tvOS Simulator', 26 | visionOS = 'visionOS', 27 | visionOSSimulator = 'visionOS Simulator', 28 | } 29 | 30 | /** 31 | * ToolResponse - Standard response format for tools 32 | * Compatible with MCP CallToolResult interface from the SDK 33 | */ 34 | export interface ToolResponse { 35 | content: ToolResponseContent[]; 36 | isError?: boolean; 37 | _meta?: Record; 38 | [key: string]: unknown; // Index signature to match CallToolResult 39 | } 40 | 41 | /** 42 | * Contents that can be included in a tool response 43 | */ 44 | export type ToolResponseContent = 45 | | { 46 | type: 'text'; 47 | text: string; 48 | [key: string]: unknown; // Index signature to match ContentItem 49 | } 50 | | { 51 | type: 'image'; 52 | data: string; // Base64-encoded image data (without URI scheme prefix) 53 | mimeType: string; // e.g., 'image/png', 'image/jpeg' 54 | [key: string]: unknown; // Index signature to match ContentItem 55 | }; 56 | 57 | /** 58 | * ValidationResult - Result of parameter validation operations 59 | */ 60 | export interface ValidationResult { 61 | isValid: boolean; 62 | errorResponse?: ToolResponse; 63 | warningResponse?: ToolResponse; 64 | } 65 | 66 | /** 67 | * CommandResponse - Generic result of command execution 68 | */ 69 | export interface CommandResponse { 70 | success: boolean; 71 | output: string; 72 | error?: string; 73 | } 74 | 75 | /** 76 | * Interface for shared build parameters 77 | */ 78 | export interface SharedBuildParams { 79 | workspacePath?: string; 80 | projectPath?: string; 81 | scheme: string; 82 | configuration: string; 83 | derivedDataPath?: string; 84 | extraArgs?: string[]; 85 | } 86 | 87 | /** 88 | * Interface for platform-specific build options 89 | */ 90 | export interface PlatformBuildOptions { 91 | platform: XcodePlatform; 92 | simulatorName?: string; 93 | simulatorId?: string; 94 | useLatestOS?: boolean; 95 | arch?: string; 96 | logPrefix: string; 97 | } 98 | -------------------------------------------------------------------------------- /src/utils/axe-setup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AXe Availability Check - Checks if axe tool is available 3 | * 4 | * This utility module provides functions to check if AXe tool 5 | * is available in the system PATH. 6 | */ 7 | 8 | import { execSync } from 'child_process'; 9 | import { createTextResponse } from './validation.js'; 10 | import { ToolResponse } from '../types/common.js'; 11 | 12 | // Constants 13 | const AXE_COMMAND = 'axe'; 14 | 15 | /** 16 | * Check if a binary is available in the PATH 17 | */ 18 | function isBinaryAvailable(binary: string): boolean { 19 | try { 20 | execSync(`which ${binary}`, { encoding: 'utf8' }); 21 | return true; 22 | } catch { 23 | return false; 24 | } 25 | } 26 | 27 | /** 28 | * Check if axe tool is available 29 | */ 30 | export function areAxeToolsAvailable(): boolean { 31 | return isBinaryAvailable(AXE_COMMAND); 32 | } 33 | 34 | export function createAxeNotAvailableResponse(): ToolResponse { 35 | return createTextResponse( 36 | 'axe command not found. UI automation features are not available.\n\n' + 37 | 'To install axe, run:\n' + 38 | 'brew tap cameroncooke/axe\n' + 39 | 'brew install axe\n\n' + 40 | 'See section "Enabling UI Automation" in the README.', 41 | true, 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/command.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Command Utilities - Generic command execution utilities 3 | * 4 | * This utility module provides functions for executing shell commands. 5 | * It serves as a foundation for other utility modules that need to execute commands. 6 | * 7 | * Responsibilities: 8 | * - Executing shell commands with proper argument handling 9 | * - Managing process spawning, output capture, and error handling 10 | */ 11 | 12 | import { spawn, ChildProcess } from 'child_process'; 13 | import { log } from './logger.js'; 14 | 15 | /** 16 | * Command execution response interface 17 | */ 18 | export interface CommandResponse { 19 | success: boolean; 20 | output: string; 21 | error?: string; 22 | process: ChildProcess; 23 | } 24 | 25 | /** 26 | * Execute a command 27 | * @param command An array of command and arguments 28 | * @param logPrefix Prefix for logging 29 | * @param useShell Whether to use shell execution (true) or direct execution (false) 30 | * @returns Promise resolving to command response with the process 31 | */ 32 | export async function executeCommand( 33 | command: string[], 34 | logPrefix?: string, 35 | useShell: boolean = true, 36 | ): Promise { 37 | // Properly escape arguments for shell 38 | let escapedCommand = command; 39 | if (useShell) { 40 | // For shell execution, we need to format as ['sh', '-c', 'full command string'] 41 | const commandString = command 42 | .map((arg) => { 43 | // If the argument contains spaces or special characters, wrap it in quotes 44 | // Ensure existing quotes are escaped 45 | if (/[\s,"'=]/.test(arg) && !/^".*"$/.test(arg)) { 46 | // Check if needs quoting and isn't already quoted 47 | return `"${arg.replace(/(["\\])/g, '\\$1')}"`; // Escape existing quotes and backslashes 48 | } 49 | return arg; 50 | }) 51 | .join(' '); 52 | 53 | escapedCommand = ['sh', '-c', commandString]; 54 | } 55 | 56 | // Log the actual command that will be executed 57 | const displayCommand = 58 | useShell && escapedCommand.length === 3 ? escapedCommand[2] : escapedCommand.join(' '); 59 | log('info', `Executing ${logPrefix || ''} command: ${displayCommand}`); 60 | 61 | return new Promise((resolve, reject) => { 62 | const executable = escapedCommand[0]; 63 | const args = escapedCommand.slice(1); 64 | 65 | const process = spawn(executable, args, { 66 | stdio: ['ignore', 'pipe', 'pipe'], // ignore stdin, pipe stdout/stderr 67 | }); 68 | 69 | let stdout = ''; 70 | let stderr = ''; 71 | 72 | process.stdout.on('data', (data) => { 73 | stdout += data.toString(); 74 | }); 75 | 76 | process.stderr.on('data', (data) => { 77 | stderr += data.toString(); 78 | }); 79 | 80 | process.on('close', (code) => { 81 | const success = code === 0; 82 | const response: CommandResponse = { 83 | success, 84 | output: stdout, 85 | error: success ? undefined : stderr, 86 | process, 87 | }; 88 | 89 | resolve(response); 90 | }); 91 | 92 | process.on('error', (err) => { 93 | reject(err); 94 | }); 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | import { ToolResponse } from '../types/common.js'; 2 | 3 | /** 4 | * Error Utilities - Type-safe error hierarchy for the application 5 | * 6 | * This utility module defines a structured error hierarchy for the application, 7 | * providing specialized error types for different failure scenarios. Using these 8 | * typed errors enables more precise error handling, improves debugging, and 9 | * provides better error messages to users. 10 | * 11 | * Responsibilities: 12 | * - Providing a base error class (XcodeBuildMCPError) for all application errors 13 | * - Defining specialized error subtypes for different error categories: 14 | * - ValidationError: Parameter validation failures 15 | * - SystemError: Underlying system/OS issues 16 | * - ConfigurationError: Application configuration problems 17 | * - SimulatorError: iOS simulator-specific failures 18 | * - AxeError: axe-specific errors 19 | * 20 | * The structured hierarchy allows error consumers to handle errors with the 21 | * appropriate level of specificity using instanceof checks or catch clauses. 22 | */ 23 | 24 | /** 25 | * Custom error types for XcodeBuildMCP 26 | */ 27 | 28 | /** 29 | * Base error class for XcodeBuildMCP errors 30 | */ 31 | export class XcodeBuildMCPError extends Error { 32 | constructor(message: string) { 33 | super(message); 34 | this.name = 'XcodeBuildMCPError'; 35 | // This is necessary for proper inheritance in TypeScript 36 | Object.setPrototypeOf(this, XcodeBuildMCPError.prototype); 37 | } 38 | } 39 | 40 | /** 41 | * Error thrown when validation of parameters fails 42 | */ 43 | export class ValidationError extends XcodeBuildMCPError { 44 | constructor( 45 | message: string, 46 | public paramName?: string, 47 | ) { 48 | super(message); 49 | this.name = 'ValidationError'; 50 | Object.setPrototypeOf(this, ValidationError.prototype); 51 | } 52 | } 53 | 54 | /** 55 | * Error thrown for system-level errors (file access, permissions, etc.) 56 | */ 57 | export class SystemError extends XcodeBuildMCPError { 58 | constructor( 59 | message: string, 60 | public originalError?: Error, 61 | ) { 62 | super(message); 63 | this.name = 'SystemError'; 64 | Object.setPrototypeOf(this, SystemError.prototype); 65 | } 66 | } 67 | 68 | /** 69 | * Error thrown for configuration issues 70 | */ 71 | export class ConfigurationError extends XcodeBuildMCPError { 72 | constructor(message: string) { 73 | super(message); 74 | this.name = 'ConfigurationError'; 75 | Object.setPrototypeOf(this, ConfigurationError.prototype); 76 | } 77 | } 78 | 79 | /** 80 | * Error thrown for simulator-specific errors 81 | */ 82 | export class SimulatorError extends XcodeBuildMCPError { 83 | constructor( 84 | message: string, 85 | public simulatorName?: string, 86 | public simulatorId?: string, 87 | ) { 88 | super(message); 89 | this.name = 'SimulatorError'; 90 | Object.setPrototypeOf(this, SimulatorError.prototype); 91 | } 92 | } 93 | 94 | /** 95 | * Error thrown for axe-specific errors 96 | */ 97 | export class AxeError extends XcodeBuildMCPError { 98 | constructor( 99 | message: string, 100 | public command?: string, // The axe command that failed 101 | public axeOutput?: string, // Output from axe 102 | public simulatorId?: string, 103 | ) { 104 | super(message); 105 | this.name = 'AxeError'; 106 | Object.setPrototypeOf(this, AxeError.prototype); 107 | } 108 | } 109 | 110 | // Helper to create a standard error response 111 | export function createErrorResponse( 112 | message: string, 113 | details?: string, 114 | _errorType: string = 'UnknownError', 115 | ): ToolResponse { 116 | const detailText = details ? `\nDetails: ${details}` : ''; 117 | return { 118 | content: [ 119 | { 120 | type: 'text', 121 | text: `Error: ${message}${detailText}`, 122 | }, 123 | ], 124 | isError: true, 125 | }; 126 | } 127 | 128 | /** 129 | * Error class for missing dependencies 130 | */ 131 | export class DependencyError extends ConfigurationError { 132 | constructor( 133 | message: string, 134 | public details?: string, 135 | ) { 136 | super(message); 137 | this.name = 'DependencyError'; 138 | Object.setPrototypeOf(this, DependencyError.prototype); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/utils/log_capture.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as os from 'os'; 4 | import { spawn, ChildProcess } from 'child_process'; 5 | import { v4 as uuidv4 } from 'uuid'; 6 | import { log } from '../utils/logger.js'; 7 | 8 | /** 9 | * Log file retention policy: 10 | * - Old log files (older than LOG_RETENTION_DAYS) are automatically deleted from the temp directory 11 | * - Cleanup runs on every new log capture start 12 | */ 13 | const LOG_RETENTION_DAYS = 3; 14 | const LOG_FILE_PREFIX = 'xcodemcp_sim_log_'; 15 | 16 | export interface LogSession { 17 | processes: ChildProcess[]; 18 | logFilePath: string; 19 | simulatorUuid: string; 20 | bundleId: string; 21 | } 22 | 23 | export const activeLogSessions: Map = new Map(); 24 | 25 | /** 26 | * Start a log capture session for an iOS simulator. 27 | * Returns { sessionId, logFilePath, processes, error? } 28 | */ 29 | export async function startLogCapture(params: { 30 | simulatorUuid: string; 31 | bundleId: string; 32 | captureConsole?: boolean; 33 | }): Promise<{ sessionId: string; logFilePath: string; processes: ChildProcess[]; error?: string }> { 34 | // Clean up old logs before starting a new session 35 | await cleanOldLogs(); 36 | 37 | const { simulatorUuid, bundleId, captureConsole = false } = params; 38 | const logSessionId = uuidv4(); 39 | const logFileName = `${LOG_FILE_PREFIX}${logSessionId}.log`; 40 | const logFilePath = path.join(os.tmpdir(), logFileName); 41 | 42 | try { 43 | await fs.promises.mkdir(os.tmpdir(), { recursive: true }); 44 | await fs.promises.writeFile(logFilePath, ''); 45 | const logStream = fs.createWriteStream(logFilePath, { flags: 'a' }); 46 | const processes: ChildProcess[] = []; 47 | logStream.write('\n--- Log capture for bundle ID: ' + bundleId + ' ---\n'); 48 | 49 | if (captureConsole) { 50 | const stdoutLogProcess = spawn('xcrun', [ 51 | 'simctl', 52 | 'launch', 53 | '--console-pty', 54 | '--terminate-running-process', 55 | simulatorUuid, 56 | bundleId, 57 | ]); 58 | stdoutLogProcess.stdout.pipe(logStream); 59 | stdoutLogProcess.stderr.pipe(logStream); 60 | processes.push(stdoutLogProcess); 61 | } 62 | 63 | const osLogProcess = spawn('xcrun', [ 64 | 'simctl', 65 | 'spawn', 66 | simulatorUuid, 67 | 'log', 68 | 'stream', 69 | '--level=debug', 70 | '--predicate', 71 | `subsystem == "${bundleId}"`, 72 | ]); 73 | osLogProcess.stdout.pipe(logStream); 74 | osLogProcess.stderr.pipe(logStream); 75 | processes.push(osLogProcess); 76 | 77 | for (const process of processes) { 78 | process.on('close', (code) => { 79 | log('info', `A log capture process for session ${logSessionId} exited with code ${code}.`); 80 | }); 81 | } 82 | 83 | activeLogSessions.set(logSessionId, { 84 | processes, 85 | logFilePath, 86 | simulatorUuid, 87 | bundleId, 88 | }); 89 | 90 | log('info', `Log capture started with session ID: ${logSessionId}`); 91 | return { sessionId: logSessionId, logFilePath, processes }; 92 | } catch (error) { 93 | const message = error instanceof Error ? error.message : String(error); 94 | log('error', `Failed to start log capture: ${message}`); 95 | return { sessionId: '', logFilePath: '', processes: [], error: message }; 96 | } 97 | } 98 | 99 | /** 100 | * Stop a log capture session and retrieve the log content. 101 | */ 102 | export async function stopLogCapture( 103 | logSessionId: string, 104 | ): Promise<{ logContent: string; error?: string }> { 105 | const session = activeLogSessions.get(logSessionId); 106 | if (!session) { 107 | log('warning', `Log session not found: ${logSessionId}`); 108 | return { logContent: '', error: `Log capture session not found: ${logSessionId}` }; 109 | } 110 | 111 | try { 112 | log('info', `Attempting to stop log capture session: ${logSessionId}`); 113 | const logFilePath = session.logFilePath; 114 | for (const process of session.processes) { 115 | if (!process.killed && process.exitCode === null) { 116 | process.kill('SIGTERM'); 117 | } 118 | } 119 | activeLogSessions.delete(logSessionId); 120 | log( 121 | 'info', 122 | `Log capture session ${logSessionId} stopped. Log file retained at: ${logFilePath}`, 123 | ); 124 | await fs.promises.access(logFilePath, fs.constants.R_OK); 125 | const fileContent = await fs.promises.readFile(logFilePath, 'utf-8'); 126 | log('info', `Successfully read log content from ${logFilePath}`); 127 | return { logContent: fileContent }; 128 | } catch (error) { 129 | const message = error instanceof Error ? error.message : String(error); 130 | log('error', `Failed to stop log capture session ${logSessionId}: ${message}`); 131 | return { logContent: '', error: message }; 132 | } 133 | } 134 | 135 | /** 136 | * Deletes log files older than LOG_RETENTION_DAYS from the temp directory. 137 | * Runs quietly; errors are logged but do not throw. 138 | */ 139 | async function cleanOldLogs(): Promise { 140 | const tempDir = os.tmpdir(); 141 | let files: string[]; 142 | try { 143 | files = await fs.promises.readdir(tempDir); 144 | } catch (err) { 145 | log( 146 | 'warn', 147 | `Could not read temp dir for log cleanup: ${err instanceof Error ? err.message : String(err)}`, 148 | ); 149 | return; 150 | } 151 | const now = Date.now(); 152 | const retentionMs = LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000; 153 | await Promise.all( 154 | files 155 | .filter((f) => f.startsWith(LOG_FILE_PREFIX) && f.endsWith('.log')) 156 | .map(async (f) => { 157 | const filePath = path.join(tempDir, f); 158 | try { 159 | const stat = await fs.promises.stat(filePath); 160 | if (now - stat.mtimeMs > retentionMs) { 161 | await fs.promises.unlink(filePath); 162 | log('info', `Deleted old log file: ${filePath}`); 163 | } 164 | } catch (err) { 165 | log( 166 | 'warn', 167 | `Error during log cleanup for ${filePath}: ${err instanceof Error ? err.message : String(err)}`, 168 | ); 169 | } 170 | }), 171 | ); 172 | } 173 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Logger Utility - Simple logging implementation for the application 3 | * 4 | * This utility module provides a lightweight logging system that directs log 5 | * messages to stderr rather than stdout, ensuring they don't interfere with 6 | * the MCP protocol communication which uses stdout. 7 | * 8 | * Responsibilities: 9 | * - Formatting log messages with timestamps and level indicators 10 | * - Directing all logs to stderr to avoid MCP protocol interference 11 | * - Supporting different log levels (info, warning, error, debug) 12 | * - Providing a simple, consistent logging interface throughout the application 13 | * - Sending error-level logs to Sentry for monitoring and alerting 14 | * 15 | * While intentionally minimal, this logger provides the essential functionality 16 | * needed for operational monitoring and debugging throughout the application. 17 | * It's used by virtually all other modules for status reporting and error logging. 18 | */ 19 | 20 | import * as Sentry from '@sentry/node'; 21 | 22 | const SENTRY_ENABLED = process.env.SENTRY_DISABLED !== 'true'; 23 | 24 | if (!SENTRY_ENABLED) { 25 | log('info', 'Sentry disabled due to SENTRY_DISABLED environment variable'); 26 | } 27 | 28 | /** 29 | * Log a message with the specified level 30 | * @param level The log level (info, warning, error, debug) 31 | * @param message The message to log 32 | */ 33 | export function log(level: string, message: string): void { 34 | const timestamp = new Date().toISOString(); 35 | const logMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`; 36 | 37 | if (level === 'error' && SENTRY_ENABLED) { 38 | Sentry.captureMessage(logMessage); 39 | } 40 | 41 | // It's important to use console.error here to ensure logs don't interfere with MCP protocol communication 42 | // see https://modelcontextprotocol.io/docs/tools/debugging#server-side-logging 43 | console.error(logMessage); 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/sentry.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Sentry instrumentation for XcodeBuildMCP 3 | * 4 | * This file initializes Sentry as early as possible in the application lifecycle. 5 | * It should be imported at the top of the main entry point file. 6 | */ 7 | 8 | import * as Sentry from '@sentry/node'; 9 | import { version } from '../version.js'; 10 | import { 11 | getXcodeInfo, 12 | getEnvironmentVariables, 13 | checkBinaryAvailability, 14 | } from '../tools/diagnostic.js'; 15 | 16 | Sentry.init({ 17 | dsn: 'https://798607831167c7b9fe2f2912f5d3c665@o4509258288332800.ingest.de.sentry.io/4509258293837904', 18 | 19 | // Setting this option to true will send default PII data to Sentry 20 | // For example, automatic IP address collection on events 21 | sendDefaultPii: true, 22 | 23 | // Set release version to match application version 24 | release: `xcodebuildmcp@${version}`, 25 | 26 | // Set environment based on NODE_ENV 27 | environment: process.env.NODE_ENV || 'development', 28 | 29 | // Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring 30 | // We recommend adjusting this value in production 31 | tracesSampleRate: 1.0, 32 | }); 33 | 34 | // Add additional context that might be helpful for debugging 35 | const tags: Record = { 36 | nodeVersion: process.version, 37 | platform: process.platform, 38 | arch: process.arch, 39 | }; 40 | 41 | // Only add Xcode Info if it's available 42 | const xcodeInfo = getXcodeInfo(); 43 | if ('version' in xcodeInfo) { 44 | tags.xcodeVersion = xcodeInfo.version; 45 | tags.xcrunVersion = xcodeInfo.xcrunVersion; 46 | } else { 47 | tags.xcodeVersion = 'Unknown'; 48 | tags.xcrunVersion = 'Unknown'; 49 | } 50 | 51 | const envVars = getEnvironmentVariables(); 52 | tags.env_XCODEBUILDMCP_DEBUG = envVars.XCODEBUILDMCP_DEBUG || 'false'; 53 | tags.env_XCODEMAKE_ENABLED = envVars.INCREMENTAL_BUILDS_ENABLED || 'false'; 54 | 55 | const miseAvailable = checkBinaryAvailability('mise'); 56 | tags.miseAvailable = miseAvailable.available ? 'true' : 'false'; 57 | tags.miseVersion = miseAvailable.version || 'Unknown'; 58 | 59 | const axeAvailable = checkBinaryAvailability('axe'); 60 | tags.axeAvailable = axeAvailable.available ? 'true' : 'false'; 61 | tags.axeVersion = axeAvailable.version || 'Unknown'; 62 | 63 | Sentry.setTags(tags); 64 | -------------------------------------------------------------------------------- /src/utils/template-manager.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { mkdir, rm } from 'fs/promises'; 3 | import { join } from 'path'; 4 | import { tmpdir } from 'os'; 5 | import { randomUUID } from 'crypto'; 6 | import { spawn } from 'child_process'; 7 | import { log } from './logger.js'; 8 | import { templateVersion } from '../version.js'; 9 | 10 | /** 11 | * Template manager for downloading and managing project templates 12 | */ 13 | export class TemplateManager { 14 | private static readonly GITHUB_ORG = 'cameroncooke'; 15 | private static readonly IOS_TEMPLATE_REPO = 'XcodeBuildMCP-iOS-Template'; 16 | private static readonly MACOS_TEMPLATE_REPO = 'XcodeBuildMCP-macOS-Template'; 17 | 18 | /** 19 | * Get the template path for a specific platform 20 | * Checks for local override via environment variable first 21 | */ 22 | static async getTemplatePath(platform: 'iOS' | 'macOS'): Promise { 23 | // Check for local override 24 | const envVar = 25 | platform === 'iOS' 26 | ? 'XCODEBUILD_MCP_IOS_TEMPLATE_PATH' 27 | : 'XCODEBUILD_MCP_MACOS_TEMPLATE_PATH'; 28 | 29 | const localPath = process.env[envVar]; 30 | if (localPath && existsSync(localPath)) { 31 | const templateSubdir = join(localPath, 'template'); 32 | if (existsSync(templateSubdir)) { 33 | log('info', `Using local ${platform} template from: ${templateSubdir}`); 34 | return templateSubdir; 35 | } else { 36 | log('info', `Template directory not found in ${localPath}, using GitHub release`); 37 | } 38 | } 39 | 40 | // Download from GitHub release 41 | return await this.downloadTemplate(platform); 42 | } 43 | 44 | /** 45 | * Download template from GitHub release 46 | */ 47 | private static async downloadTemplate(platform: 'iOS' | 'macOS'): Promise { 48 | const repo = platform === 'iOS' ? this.IOS_TEMPLATE_REPO : this.MACOS_TEMPLATE_REPO; 49 | const version = process.env.XCODEBUILD_MCP_TEMPLATE_VERSION || templateVersion; 50 | 51 | // Create temp directory for download 52 | const tempDir = join(tmpdir(), `xcodebuild-mcp-template-${randomUUID()}`); 53 | await mkdir(tempDir, { recursive: true }); 54 | 55 | try { 56 | const downloadUrl = `https://github.com/${this.GITHUB_ORG}/${repo}/releases/download/${version}/${repo}-${version.substring(1)}.zip`; 57 | const zipPath = join(tempDir, 'template.zip'); 58 | 59 | log('info', `Downloading ${platform} template ${version} from GitHub...`); 60 | log('info', `Download URL: ${downloadUrl}`); 61 | 62 | // Download the release artifact 63 | await new Promise((resolve, reject) => { 64 | const curl = spawn('curl', ['-L', '-f', '-o', zipPath, downloadUrl], { cwd: tempDir }); 65 | let stderr = ''; 66 | 67 | curl.stderr.on('data', (data) => { 68 | stderr += data.toString(); 69 | }); 70 | 71 | curl.on('close', (code) => { 72 | if (code !== 0) { 73 | reject(new Error(`Failed to download template: ${stderr}`)); 74 | } else { 75 | resolve(); 76 | } 77 | }); 78 | 79 | curl.on('error', reject); 80 | }); 81 | 82 | // Extract the zip file 83 | await new Promise((resolve, reject) => { 84 | const unzip = spawn('unzip', ['-q', zipPath], { cwd: tempDir }); 85 | let stderr = ''; 86 | 87 | unzip.stderr.on('data', (data) => { 88 | stderr += data.toString(); 89 | }); 90 | 91 | unzip.on('close', (code) => { 92 | if (code !== 0) { 93 | reject(new Error(`Failed to extract template: ${stderr}`)); 94 | } else { 95 | resolve(); 96 | } 97 | }); 98 | 99 | unzip.on('error', reject); 100 | }); 101 | 102 | // Find the extracted directory and return the template subdirectory 103 | const extractedDir = join(tempDir, `${repo}-${version.substring(1)}`); 104 | if (!existsSync(extractedDir)) { 105 | throw new Error(`Expected template directory not found: ${extractedDir}`); 106 | } 107 | 108 | log('info', `Successfully downloaded ${platform} template ${version}`); 109 | return extractedDir; 110 | } catch (error) { 111 | // Clean up on error 112 | log('error', `Failed to download ${platform} template ${version}: ${error}`); 113 | await this.cleanup(tempDir); 114 | throw error; 115 | } 116 | } 117 | 118 | /** 119 | * Clean up downloaded template directory 120 | */ 121 | static async cleanup(templatePath: string): Promise { 122 | // Only clean up if it's in temp directory 123 | if (templatePath.startsWith(tmpdir())) { 124 | await rm(templatePath, { recursive: true, force: true }); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/utils/tool-groups.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tool Groups Configuration 3 | * 4 | * This file defines the groups of tools and provides utilities to determine 5 | * which tools should be enabled based on environment variables. 6 | */ 7 | 8 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 9 | import { log } from './logger.js'; 10 | 11 | // Tool group definitions 12 | export enum ToolGroup { 13 | // Workflow-based groups 14 | // Project/target discovery and analysis 15 | PROJECT_DISCOVERY = 'XCODEBUILDMCP_GROUP_PROJECT_DISCOVERY', 16 | 17 | // iOS Simulator development workflow 18 | IOS_SIMULATOR_WORKFLOW = 'XCODEBUILDMCP_GROUP_IOS_SIMULATOR_WORKFLOW', 19 | 20 | // iOS Device development workflow 21 | IOS_DEVICE_WORKFLOW = 'XCODEBUILDMCP_GROUP_IOS_DEVICE_WORKFLOW', 22 | 23 | // macOS application development workflow 24 | MACOS_WORKFLOW = 'XCODEBUILDMCP_GROUP_MACOS_WORKFLOW', 25 | 26 | // Swift Package Manager workflow 27 | SWIFT_PACKAGE_WORKFLOW = 'XCODEBUILDMCP_GROUP_SWIFT_PACKAGE_WORKFLOW', 28 | 29 | // Simulator device management 30 | SIMULATOR_MANAGEMENT = 'XCODEBUILDMCP_GROUP_SIMULATOR_MANAGEMENT', 31 | 32 | // Application deployment tools 33 | APP_DEPLOYMENT = 'XCODEBUILDMCP_GROUP_APP_DEPLOYMENT', 34 | 35 | // Logging and diagnostics 36 | DIAGNOSTICS = 'XCODEBUILDMCP_GROUP_DIAGNOSTICS', 37 | 38 | // UI testing and automation 39 | UI_TESTING = 'XCODEBUILDMCP_GROUP_UI_TESTING', 40 | } 41 | 42 | // Map tool registration functions to their respective groups and individual env var names 43 | export interface ToolRegistration { 44 | register: (server: McpServer) => void; 45 | groups: ToolGroup[]; 46 | envVar: string; 47 | } 48 | 49 | // Check if selective tool registration is enabled by checking if any tool or group env vars are set 50 | export function isSelectiveToolsEnabled(): boolean { 51 | // Check if any tool-specific environment variables are set 52 | const hasToolEnvVars = Object.keys(process.env).some( 53 | (key) => key.startsWith('XCODEBUILDMCP_TOOL_') && process.env[key] === 'true', 54 | ); 55 | 56 | // Check if any group-specific environment variables are set 57 | const hasGroupEnvVars = Object.keys(process.env).some( 58 | (key) => key.startsWith('XCODEBUILDMCP_GROUP_') && process.env[key] === 'true', 59 | ); 60 | 61 | const isEnabled = hasToolEnvVars || hasGroupEnvVars; 62 | return isEnabled; 63 | } 64 | 65 | // Check if a specific tool should be enabled 66 | export function isToolEnabled(toolEnvVar: string): boolean { 67 | // If selective tools mode is not enabled, all tools are enabled by default 68 | if (!isSelectiveToolsEnabled()) { 69 | return true; 70 | } 71 | 72 | const isEnabled = process.env[toolEnvVar] === 'true'; 73 | return isEnabled; 74 | } 75 | 76 | // Check if a tool group should be enabled 77 | export function isGroupEnabled(group: ToolGroup): boolean { 78 | // If selective tools mode is not enabled, all groups are enabled by default 79 | if (!isSelectiveToolsEnabled()) { 80 | return true; 81 | } 82 | 83 | // In selective mode, group must be explicitly enabled 84 | const isEnabled = process.env[group] === 'true'; 85 | return isEnabled; 86 | } 87 | 88 | // Check if a tool should be registered based on its groups and individual env var 89 | export function shouldRegisterTool(toolReg: ToolRegistration): boolean { 90 | // If selective tools mode is not enabled, register all tools 91 | if (!isSelectiveToolsEnabled()) { 92 | return true; 93 | } 94 | 95 | // Check if the tool is enabled individually 96 | if (isToolEnabled(toolReg.envVar)) { 97 | return true; 98 | } 99 | 100 | // Check if any of the tool's groups are enabled 101 | const enabledByGroup = toolReg.groups.some((group) => isGroupEnabled(group)); 102 | return enabledByGroup; 103 | } 104 | 105 | // List all enabled tool groups (for debugging purposes) 106 | export function listEnabledGroups(): string[] { 107 | if (!isSelectiveToolsEnabled()) { 108 | return Object.values(ToolGroup); 109 | } 110 | 111 | return Object.values(ToolGroup).filter((group) => isGroupEnabled(group as ToolGroup)); 112 | } 113 | 114 | // Helper to register a tool if it's enabled 115 | export function registerIfEnabled(server: McpServer, toolReg: ToolRegistration): void { 116 | const shouldRegister = shouldRegisterTool(toolReg); 117 | 118 | if (shouldRegister) { 119 | if (process.env.XCODEBUILDMCP_DEBUG === 'true') { 120 | log('debug', `Registering tool: ${toolReg.envVar}`); 121 | } 122 | toolReg.register(server); 123 | } else { 124 | if (process.env.XCODEBUILDMCP_DEBUG === 'true') { 125 | log('debug', `Skipping tool: ${toolReg.envVar} (not enabled)`); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/utils/validation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Validation Utilities - Input validation and error response generation 3 | * 4 | * This utility module provides a comprehensive set of validation functions to ensure 5 | * that tool inputs meet expected requirements. It centralizes validation logic, 6 | * error message formatting, and response generation for consistent error handling 7 | * across the application. 8 | * 9 | * Responsibilities: 10 | * - Validating required parameters (validateRequiredParam) 11 | * - Checking parameters against allowed values (validateAllowedValues, validateEnumParam) 12 | * - Verifying file existence (validateFileExists) 13 | * - Validating logical conditions (validateCondition) 14 | * - Ensuring at least one of multiple parameters is provided (validateAtLeastOneParam) 15 | * - Creating standardized response objects for tools (createTextResponse) 16 | * 17 | * Using these validation utilities ensures consistent error messaging and helps 18 | * provide clear feedback to users when their inputs don't meet requirements. 19 | * The functions return ValidationResult objects that make it easy to chain 20 | * validations and generate appropriate responses. 21 | */ 22 | 23 | import * as fs from 'fs'; 24 | import { log } from './logger.js'; 25 | import { ToolResponse, ValidationResult } from '../types/common.js'; 26 | 27 | /** 28 | * Creates a text response with the given message 29 | * @param message The message to include in the response 30 | * @param isError Whether this is an error response 31 | * @returns A ToolResponse object with the message 32 | */ 33 | export function createTextResponse(message: string, isError = false): ToolResponse { 34 | return { 35 | content: [ 36 | { 37 | type: 'text', 38 | text: message, 39 | }, 40 | ], 41 | isError, 42 | }; 43 | } 44 | 45 | /** 46 | * Validates that a required parameter is present 47 | * @param paramName Name of the parameter 48 | * @param paramValue Value of the parameter 49 | * @param helpfulMessage Optional helpful message to include in the error response 50 | * @returns Validation result 51 | */ 52 | export function validateRequiredParam( 53 | paramName: string, 54 | paramValue: unknown, 55 | helpfulMessage = `Required parameter '${paramName}' is missing. Please provide a value for this parameter.`, 56 | ): ValidationResult { 57 | if (paramValue === undefined || paramValue === null) { 58 | log('warning', `Required parameter '${paramName}' is missing`); 59 | return { 60 | isValid: false, 61 | errorResponse: createTextResponse(helpfulMessage, true), 62 | }; 63 | } 64 | 65 | return { isValid: true }; 66 | } 67 | 68 | /** 69 | * Validates that a parameter value is one of the allowed values 70 | * @param paramName Name of the parameter 71 | * @param paramValue Value of the parameter 72 | * @param allowedValues Array of allowed values 73 | * @returns Validation result 74 | */ 75 | export function validateAllowedValues( 76 | paramName: string, 77 | paramValue: T, 78 | allowedValues: T[], 79 | ): ValidationResult { 80 | if (!allowedValues.includes(paramValue)) { 81 | log( 82 | 'warning', 83 | `Parameter '${paramName}' has invalid value '${paramValue}'. Allowed values: ${allowedValues.join( 84 | ', ', 85 | )}`, 86 | ); 87 | return { 88 | isValid: false, 89 | errorResponse: createTextResponse( 90 | `Parameter '${paramName}' must be one of: ${allowedValues.join(', ')}. You provided: '${paramValue}'.`, 91 | true, 92 | ), 93 | }; 94 | } 95 | 96 | return { isValid: true }; 97 | } 98 | 99 | /** 100 | * Validates that a condition is true 101 | * @param condition Condition to validate 102 | * @param message Message to include in the warning response 103 | * @param logWarning Whether to log a warning message 104 | * @returns Validation result 105 | */ 106 | export function validateCondition( 107 | condition: boolean, 108 | message: string, 109 | logWarning: boolean = true, 110 | ): ValidationResult { 111 | if (!condition) { 112 | if (logWarning) { 113 | log('warning', message); 114 | } 115 | return { 116 | isValid: false, 117 | warningResponse: createTextResponse(message), 118 | }; 119 | } 120 | 121 | return { isValid: true }; 122 | } 123 | 124 | /** 125 | * Validates that a file exists 126 | * @param filePath Path to check 127 | * @returns Validation result 128 | */ 129 | export function validateFileExists(filePath: string): ValidationResult { 130 | if (!fs.existsSync(filePath)) { 131 | return { 132 | isValid: false, 133 | errorResponse: createTextResponse( 134 | `File not found: '${filePath}'. Please check the path and try again.`, 135 | true, 136 | ), 137 | }; 138 | } 139 | 140 | return { isValid: true }; 141 | } 142 | 143 | /** 144 | * Validates that at least one of two parameters is provided 145 | * @param param1Name Name of the first parameter 146 | * @param param1Value Value of the first parameter 147 | * @param param2Name Name of the second parameter 148 | * @param param2Value Value of the second parameter 149 | * @returns Validation result 150 | */ 151 | export function validateAtLeastOneParam( 152 | param1Name: string, 153 | param1Value: unknown, 154 | param2Name: string, 155 | param2Value: unknown, 156 | ): ValidationResult { 157 | if ( 158 | (param1Value === undefined || param1Value === null) && 159 | (param2Value === undefined || param2Value === null) 160 | ) { 161 | log('warning', `At least one of '${param1Name}' or '${param2Name}' must be provided`); 162 | return { 163 | isValid: false, 164 | errorResponse: createTextResponse( 165 | `At least one of '${param1Name}' or '${param2Name}' must be provided.`, 166 | true, 167 | ), 168 | }; 169 | } 170 | 171 | return { isValid: true }; 172 | } 173 | 174 | /** 175 | * Validates that a parameter value is one of the allowed enum values 176 | * @param paramName Name of the parameter 177 | * @param paramValue Value of the parameter 178 | * @param allowedValues Array of allowed enum values 179 | * @returns Validation result 180 | */ 181 | export function validateEnumParam( 182 | paramName: string, 183 | paramValue: T, 184 | allowedValues: T[], 185 | ): ValidationResult { 186 | if (!allowedValues.includes(paramValue)) { 187 | log( 188 | 'warning', 189 | `Parameter '${paramName}' has invalid value '${paramValue}'. Allowed values: ${allowedValues.join( 190 | ', ', 191 | )}`, 192 | ); 193 | return { 194 | isValid: false, 195 | errorResponse: createTextResponse( 196 | `Parameter '${paramName}' must be one of: ${allowedValues.join(', ')}. You provided: '${paramValue}'.`, 197 | true, 198 | ), 199 | }; 200 | } 201 | 202 | return { isValid: true }; 203 | } 204 | 205 | // Export the ToolResponse type for use in other files 206 | export { ToolResponse, ValidationResult }; 207 | -------------------------------------------------------------------------------- /src/utils/xcode.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Xcode Utilities - Core infrastructure for interacting with Xcode tools 3 | * 4 | * This utility module provides the foundation for all Xcode interactions across the codebase. 5 | * It offers platform-specific utilities, and common functionality that can be used by any module 6 | * requiring Xcode tool integration. 7 | * 8 | * Responsibilities: 9 | * - Constructing platform-specific destination strings (constructDestinationString) 10 | * 11 | * This file serves as the foundation layer for more specialized utilities like build-utils.ts, 12 | * which build upon these core functions to provide higher-level abstractions. 13 | */ 14 | 15 | import { log } from './logger.js'; 16 | import { XcodePlatform } from '../types/common.js'; 17 | 18 | // Re-export XcodePlatform for use in other modules 19 | export { XcodePlatform }; 20 | 21 | /** 22 | * Constructs a destination string for xcodebuild from platform and simulator parameters 23 | * @param platform The target platform 24 | * @param simulatorName Optional simulator name 25 | * @param simulatorId Optional simulator UUID 26 | * @param useLatest Whether to use the latest simulator version (primarily for named simulators) 27 | * @param arch Optional architecture for macOS builds (arm64 or x86_64) 28 | * @returns Properly formatted destination string for xcodebuild 29 | */ 30 | export function constructDestinationString( 31 | platform: XcodePlatform, 32 | simulatorName?: string, 33 | simulatorId?: string, 34 | useLatest: boolean = true, 35 | arch?: string, 36 | ): string { 37 | const isSimulatorPlatform = [ 38 | XcodePlatform.iOSSimulator, 39 | XcodePlatform.watchOSSimulator, 40 | XcodePlatform.tvOSSimulator, 41 | XcodePlatform.visionOSSimulator, 42 | ].includes(platform); 43 | 44 | // If ID is provided for a simulator, it takes precedence and uniquely identifies it. 45 | if (isSimulatorPlatform && simulatorId) { 46 | return `platform=${platform},id=${simulatorId}`; 47 | } 48 | 49 | // If name is provided for a simulator 50 | if (isSimulatorPlatform && simulatorName) { 51 | return `platform=${platform},name=${simulatorName}${useLatest ? ',OS=latest' : ''}`; 52 | } 53 | 54 | // If it's a simulator platform but neither ID nor name is provided (should be prevented by callers now) 55 | if (isSimulatorPlatform && !simulatorId && !simulatorName) { 56 | // Throw error as specific simulator is needed unless it's a generic build action 57 | // Allow fallback for generic simulator builds if needed, but generally require specifics for build/run 58 | log( 59 | 'warning', 60 | `Constructing generic destination for ${platform} without name or ID. This might not be specific enough.`, 61 | ); 62 | // Example: return `platform=${platform},name=Any ${platform} Device`; // Or similar generic target 63 | throw new Error(`Simulator name or ID is required for specific ${platform} operations`); 64 | } 65 | 66 | // Handle non-simulator platforms 67 | switch (platform) { 68 | case XcodePlatform.macOS: 69 | return arch ? `platform=macOS,arch=${arch}` : 'platform=macOS'; 70 | case XcodePlatform.iOS: 71 | return 'generic/platform=iOS'; 72 | case XcodePlatform.watchOS: 73 | return 'generic/platform=watchOS'; 74 | case XcodePlatform.tvOS: 75 | return 'generic/platform=tvOS'; 76 | case XcodePlatform.visionOS: 77 | return 'generic/platform=visionOS'; 78 | // No default needed as enum covers all cases unless extended 79 | // default: 80 | // throw new Error(`Unsupported platform for destination string: ${platform}`); 81 | } 82 | // Fallback just in case (shouldn't be reached with enum) 83 | log('error', `Reached unexpected point in constructDestinationString for platform: ${platform}`); 84 | return `platform=${platform}`; 85 | } 86 | -------------------------------------------------------------------------------- /src/utils/xcodemake.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * xcodemake Utilities - Support for using xcodemake as an alternative build strategy 3 | * 4 | * This utility module provides functions for using xcodemake (https://github.com/johnno1962/xcodemake) 5 | * as an alternative build strategy for Xcode projects. xcodemake logs xcodebuild output to generate 6 | * a Makefile for an Xcode project, allowing for faster incremental builds using the "make" command. 7 | * 8 | * Responsibilities: 9 | * - Checking if xcodemake is enabled via environment variable 10 | * - Executing xcodemake commands with proper argument handling 11 | * - Converting xcodebuild arguments to xcodemake arguments 12 | * - Handling xcodemake-specific output and error reporting 13 | * - Auto-downloading xcodemake if enabled but not found 14 | */ 15 | 16 | import { log } from './logger.js'; 17 | import { executeCommand, CommandResponse } from './command.js'; 18 | import { existsSync, readdirSync } from 'fs'; 19 | import * as path from 'path'; 20 | import * as os from 'os'; 21 | import * as fs from 'fs/promises'; 22 | 23 | // Environment variable to control xcodemake usage 24 | export const XCODEMAKE_ENV_VAR = 'INCREMENTAL_BUILDS_ENABLED'; 25 | 26 | // Store the overridden path for xcodemake if needed 27 | let overriddenXcodemakePath: string | null = null; 28 | 29 | /** 30 | * Check if xcodemake is enabled via environment variable 31 | * @returns boolean indicating if xcodemake should be used 32 | */ 33 | export function isXcodemakeEnabled(): boolean { 34 | const envValue = process.env[XCODEMAKE_ENV_VAR]; 35 | return envValue === '1' || envValue === 'true' || envValue === 'yes'; 36 | } 37 | 38 | /** 39 | * Get the xcodemake command to use 40 | * @returns The command string for xcodemake 41 | */ 42 | function getXcodemakeCommand(): string { 43 | return overriddenXcodemakePath || 'xcodemake'; 44 | } 45 | 46 | /** 47 | * Override the xcodemake command path 48 | * @param path Path to the xcodemake executable 49 | */ 50 | function overrideXcodemakeCommand(path: string): void { 51 | overriddenXcodemakePath = path; 52 | log('info', `Using overridden xcodemake path: ${path}`); 53 | } 54 | 55 | /** 56 | * Install xcodemake by downloading it from GitHub 57 | * @returns Promise resolving to boolean indicating if installation was successful 58 | */ 59 | async function installXcodemake(): Promise { 60 | const tempDir = os.tmpdir(); 61 | const xcodemakeDir = path.join(tempDir, 'xcodebuildmcp'); 62 | const xcodemakePath = path.join(xcodemakeDir, 'xcodemake'); 63 | 64 | log('info', `Attempting to install xcodemake to ${xcodemakePath}`); 65 | 66 | try { 67 | // Create directory if it doesn't exist 68 | await fs.mkdir(xcodemakeDir, { recursive: true }); 69 | 70 | // Download the script 71 | log('info', 'Downloading xcodemake from GitHub...'); 72 | const response = await fetch( 73 | 'https://raw.githubusercontent.com/cameroncooke/xcodemake/main/xcodemake', 74 | ); 75 | 76 | if (!response.ok) { 77 | throw new Error(`Failed to download xcodemake: ${response.status} ${response.statusText}`); 78 | } 79 | 80 | const scriptContent = await response.text(); 81 | await fs.writeFile(xcodemakePath, scriptContent, 'utf8'); 82 | 83 | // Make executable 84 | await fs.chmod(xcodemakePath, 0o755); 85 | log('info', 'Made xcodemake executable'); 86 | 87 | // Override the command to use the direct path 88 | overrideXcodemakeCommand(xcodemakePath); 89 | 90 | return true; 91 | } catch (error) { 92 | log( 93 | 'error', 94 | `Error installing xcodemake: ${error instanceof Error ? error.message : String(error)}`, 95 | ); 96 | return false; 97 | } 98 | } 99 | 100 | /** 101 | * Check if xcodemake is installed and available. If enabled but not available, attempts to download it. 102 | * @returns Promise resolving to boolean indicating if xcodemake is available 103 | */ 104 | export async function isXcodemakeAvailable(): Promise { 105 | // First check if xcodemake is enabled, if not, no need to check or install 106 | if (!isXcodemakeEnabled()) { 107 | log('debug', 'xcodemake is not enabled, skipping availability check'); 108 | return false; 109 | } 110 | 111 | try { 112 | // Check if we already have an overridden path 113 | if (overriddenXcodemakePath && existsSync(overriddenXcodemakePath)) { 114 | log('debug', `xcodemake found at overridden path: ${overriddenXcodemakePath}`); 115 | return true; 116 | } 117 | 118 | // Check if xcodemake is available in PATH 119 | const result = await executeCommand(['which', 'xcodemake']); 120 | if (result.success) { 121 | log('debug', 'xcodemake found in PATH'); 122 | return true; 123 | } 124 | 125 | // If not found, download and install it 126 | log('info', 'xcodemake not found in PATH, attempting to download...'); 127 | const installed = await installXcodemake(); 128 | if (installed) { 129 | log('info', 'xcodemake installed successfully'); 130 | return true; 131 | } else { 132 | log('warn', 'xcodemake installation failed'); 133 | return false; 134 | } 135 | } catch (error) { 136 | log( 137 | 'error', 138 | `Error checking for xcodemake: ${error instanceof Error ? error.message : String(error)}`, 139 | ); 140 | return false; 141 | } 142 | } 143 | 144 | /** 145 | * Check if a Makefile exists in the current directory 146 | * @returns boolean indicating if a Makefile exists 147 | */ 148 | export function doesMakefileExist(projectDir: string): boolean { 149 | return existsSync(`${projectDir}/Makefile`); 150 | } 151 | 152 | /** 153 | * Check if a Makefile log exists in the current directory 154 | * @param projectDir Directory containing the Makefile 155 | * @param command Command array to check for log file 156 | * @returns boolean indicating if a Makefile log exists 157 | */ 158 | export function doesMakeLogFileExist(projectDir: string, command: string[]): boolean { 159 | // Change to the project directory as xcodemake requires being in the project dir 160 | const originalDir = process.cwd(); 161 | 162 | try { 163 | process.chdir(projectDir); 164 | 165 | // Construct the expected log filename 166 | const xcodemakeCommand = ['xcodemake', ...command.slice(1)]; 167 | const escapedCommand = xcodemakeCommand.map((arg) => { 168 | // Remove projectDir from arguments if present at the start 169 | const prefix = projectDir + '/'; 170 | if (arg.startsWith(prefix)) { 171 | return arg.substring(prefix.length); 172 | } 173 | return arg; 174 | }); 175 | const commandString = escapedCommand.join(' '); 176 | const logFileName = `${commandString}.log`; 177 | log('debug', `Checking for Makefile log: ${logFileName} in directory: ${process.cwd()}`); 178 | 179 | // Read directory contents and check if the file exists 180 | const files = readdirSync('.'); 181 | const exists = files.includes(logFileName); 182 | log('debug', `Makefile log ${exists ? 'exists' : 'does not exist'}: ${logFileName}`); 183 | return exists; 184 | } catch (error) { 185 | // Log potential errors like directory not found, permissions issues, etc. 186 | log( 187 | 'error', 188 | `Error checking for Makefile log: ${error instanceof Error ? error.message : String(error)}`, 189 | ); 190 | return false; 191 | } finally { 192 | // Always restore the original directory 193 | process.chdir(originalDir); 194 | } 195 | } 196 | 197 | /** 198 | * Execute an xcodemake command to generate a Makefile 199 | * @param buildArgs Build arguments to pass to xcodemake (without the 'xcodebuild' command) 200 | * @param logPrefix Prefix for logging 201 | * @returns Promise resolving to command response 202 | */ 203 | export async function executeXcodemakeCommand( 204 | projectDir: string, 205 | buildArgs: string[], 206 | logPrefix: string, 207 | ): Promise { 208 | // Change directory to project directory, this is needed for xcodemake to work 209 | process.chdir(projectDir); 210 | 211 | const xcodemakeCommand = [getXcodemakeCommand(), ...buildArgs]; 212 | 213 | // Remove projectDir from arguments 214 | const command = xcodemakeCommand.map((arg) => arg.replace(projectDir + '/', '')); 215 | 216 | return executeCommand(command, logPrefix); 217 | } 218 | 219 | /** 220 | * Execute a make command for incremental builds 221 | * @param projectDir Directory containing the Makefile 222 | * @param logPrefix Prefix for logging 223 | * @returns Promise resolving to command response 224 | */ 225 | export async function executeMakeCommand( 226 | projectDir: string, 227 | logPrefix: string, 228 | ): Promise { 229 | const command = ['cd', projectDir, '&&', 'make']; 230 | return executeCommand(command, logPrefix); 231 | } 232 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "sourceMap": true, 13 | "inlineSources": true, 14 | 15 | // Set `sourceRoot` to "/" to strip the build path prefix 16 | // from generated source code references. 17 | // This improves issue grouping in Sentry. 18 | "sourceRoot": "/" 19 | }, 20 | "include": ["src/**/*"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | import { readFileSync, writeFileSync, chmodSync } from 'fs'; 3 | 4 | export default defineConfig({ 5 | entry: { 6 | index: 'src/index.ts', 7 | 'diagnostic-cli': 'src/diagnostic-cli.ts', 8 | }, 9 | format: ['esm'], 10 | target: 'node18', 11 | platform: 'node', 12 | outDir: 'build', 13 | clean: true, 14 | sourcemap: true, 15 | dts: true, 16 | splitting: false, 17 | shims: false, 18 | external: [], 19 | noExternal: [], 20 | treeshake: true, 21 | minify: false, 22 | onSuccess: async () => { 23 | console.log('Setting executable permissions...'); 24 | chmodSync('build/index.js', '755'); 25 | chmodSync('build/diagnostic-cli.js', '755'); 26 | console.log('Build complete!'); 27 | }, 28 | esbuildOptions(options) { 29 | // Set sourceRoot for Sentry 30 | options.sourceRoot = '/'; 31 | options.sourcesContent = true; 32 | }, 33 | }); --------------------------------------------------------------------------------