This is page 1 of 11. Use http://codebase.md/cameroncooke/xcodebuildmcp?page={x} to view the full context. # Directory Structure ``` ├── .axe-version ├── .claude │ └── agents │ └── xcodebuild-mcp-qa-tester.md ├── .cursor │ ├── BUGBOT.md │ └── environment.json ├── .cursorrules ├── .github │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ └── workflows │ ├── ci.yml │ ├── claude-code-review.yml │ ├── claude-dispatch.yml │ ├── claude.yml │ ├── droid-code-review.yml │ ├── README.md │ ├── release.yml │ └── sentry.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── mcp.json │ ├── settings.json │ └── tasks.json ├── AGENTS.md ├── banner.png ├── build-plugins │ ├── plugin-discovery.js │ ├── plugin-discovery.ts │ └── tsconfig.json ├── CHANGELOG.md ├── CLAUDE.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── docs │ ├── ARCHITECTURE.md │ ├── CODE_QUALITY.md │ ├── CONTRIBUTING.md │ ├── ESLINT_TYPE_SAFETY.md │ ├── MANUAL_TESTING.md │ ├── NODEJS_2025.md │ ├── PLUGIN_DEVELOPMENT.md │ ├── RELEASE_PROCESS.md │ ├── RELOADEROO_FOR_XCODEBUILDMCP.md │ ├── RELOADEROO_XCODEBUILDMCP_PRIMER.md │ ├── RELOADEROO.md │ ├── session_management_plan.md │ ├── session-aware-migration-todo.md │ ├── TEST_RUNNER_ENV_IMPLEMENTATION_PLAN.md │ ├── TESTING.md │ └── TOOLS.md ├── eslint.config.js ├── example_projects │ ├── .vscode │ │ └── launch.json │ ├── iOS │ │ ├── .cursor │ │ │ └── rules │ │ │ └── errors.mdc │ │ ├── .vscode │ │ │ └── settings.json │ │ ├── Makefile │ │ ├── MCPTest │ │ │ ├── Assets.xcassets │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── ContentView.swift │ │ │ ├── MCPTestApp.swift │ │ │ └── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ │ ├── MCPTest.xcodeproj │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── MCPTest.xcscheme │ │ └── MCPTestUITests │ │ └── MCPTestUITests.swift │ ├── iOS_Calculator │ │ ├── CalculatorApp │ │ │ ├── Assets.xcassets │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── CalculatorApp.swift │ │ │ └── CalculatorApp.xctestplan │ │ ├── CalculatorApp.xcodeproj │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── CalculatorApp.xcscheme │ │ ├── CalculatorApp.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ ├── CalculatorAppPackage │ │ │ ├── .gitignore │ │ │ ├── Package.swift │ │ │ ├── Sources │ │ │ │ └── CalculatorAppFeature │ │ │ │ ├── BackgroundEffect.swift │ │ │ │ ├── CalculatorButton.swift │ │ │ │ ├── CalculatorDisplay.swift │ │ │ │ ├── CalculatorInputHandler.swift │ │ │ │ ├── CalculatorService.swift │ │ │ │ └── ContentView.swift │ │ │ └── Tests │ │ │ └── CalculatorAppFeatureTests │ │ │ └── CalculatorServiceTests.swift │ │ ├── CalculatorAppTests │ │ │ └── CalculatorAppTests.swift │ │ └── Config │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ ├── Shared.xcconfig │ │ └── Tests.xcconfig │ ├── macOS │ │ ├── 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 │ │ └── MCPTest.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ └── xcschemes │ │ └── MCPTest.xcscheme │ └── spm │ ├── .gitignore │ ├── Package.resolved │ ├── Package.swift │ ├── Sources │ │ ├── long-server │ │ │ └── main.swift │ │ ├── quick-task │ │ │ └── main.swift │ │ ├── spm │ │ │ └── main.swift │ │ └── TestLib │ │ └── TaskManager.swift │ └── Tests │ └── TestLibTests │ └── SimpleTests.swift ├── LICENSE ├── mcp-install-dark.png ├── package-lock.json ├── package.json ├── README.md ├── scripts │ ├── analysis │ │ └── tools-analysis.ts │ ├── bundle-axe.sh │ ├── check-code-patterns.js │ ├── release.sh │ ├── tools-cli.ts │ └── update-tools-docs.ts ├── server.json ├── smithery.yaml ├── src │ ├── core │ │ ├── __tests__ │ │ │ └── resources.test.ts │ │ ├── dynamic-tools.ts │ │ ├── plugin-registry.ts │ │ ├── plugin-types.ts │ │ └── resources.ts │ ├── doctor-cli.ts │ ├── index.ts │ ├── mcp │ │ ├── resources │ │ │ ├── __tests__ │ │ │ │ ├── devices.test.ts │ │ │ │ ├── doctor.test.ts │ │ │ │ └── simulators.test.ts │ │ │ ├── devices.ts │ │ │ ├── doctor.ts │ │ │ └── simulators.ts │ │ └── tools │ │ ├── device │ │ │ ├── __tests__ │ │ │ │ ├── build_device.test.ts │ │ │ │ ├── get_device_app_path.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── install_app_device.test.ts │ │ │ │ ├── launch_app_device.test.ts │ │ │ │ ├── list_devices.test.ts │ │ │ │ ├── re-exports.test.ts │ │ │ │ ├── stop_app_device.test.ts │ │ │ │ └── test_device.test.ts │ │ │ ├── build_device.ts │ │ │ ├── clean.ts │ │ │ ├── discover_projs.ts │ │ │ ├── get_app_bundle_id.ts │ │ │ ├── get_device_app_path.ts │ │ │ ├── index.ts │ │ │ ├── install_app_device.ts │ │ │ ├── launch_app_device.ts │ │ │ ├── list_devices.ts │ │ │ ├── list_schemes.ts │ │ │ ├── show_build_settings.ts │ │ │ ├── start_device_log_cap.ts │ │ │ ├── stop_app_device.ts │ │ │ ├── stop_device_log_cap.ts │ │ │ └── test_device.ts │ │ ├── discovery │ │ │ ├── __tests__ │ │ │ │ └── discover_tools.test.ts │ │ │ ├── discover_tools.ts │ │ │ └── index.ts │ │ ├── doctor │ │ │ ├── __tests__ │ │ │ │ ├── doctor.test.ts │ │ │ │ └── index.test.ts │ │ │ ├── doctor.ts │ │ │ ├── index.ts │ │ │ └── lib │ │ │ └── doctor.deps.ts │ │ ├── logging │ │ │ ├── __tests__ │ │ │ │ ├── index.test.ts │ │ │ │ ├── start_device_log_cap.test.ts │ │ │ │ ├── start_sim_log_cap.test.ts │ │ │ │ ├── stop_device_log_cap.test.ts │ │ │ │ └── stop_sim_log_cap.test.ts │ │ │ ├── index.ts │ │ │ ├── start_device_log_cap.ts │ │ │ ├── start_sim_log_cap.ts │ │ │ ├── stop_device_log_cap.ts │ │ │ └── stop_sim_log_cap.ts │ │ ├── macos │ │ │ ├── __tests__ │ │ │ │ ├── build_macos.test.ts │ │ │ │ ├── build_run_macos.test.ts │ │ │ │ ├── get_mac_app_path.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── launch_mac_app.test.ts │ │ │ │ ├── re-exports.test.ts │ │ │ │ ├── stop_mac_app.test.ts │ │ │ │ └── test_macos.test.ts │ │ │ ├── build_macos.ts │ │ │ ├── build_run_macos.ts │ │ │ ├── clean.ts │ │ │ ├── discover_projs.ts │ │ │ ├── get_mac_app_path.ts │ │ │ ├── get_mac_bundle_id.ts │ │ │ ├── index.ts │ │ │ ├── launch_mac_app.ts │ │ │ ├── list_schemes.ts │ │ │ ├── show_build_settings.ts │ │ │ ├── stop_mac_app.ts │ │ │ └── test_macos.ts │ │ ├── project-discovery │ │ │ ├── __tests__ │ │ │ │ ├── discover_projs.test.ts │ │ │ │ ├── get_app_bundle_id.test.ts │ │ │ │ ├── get_mac_bundle_id.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── list_schemes.test.ts │ │ │ │ └── show_build_settings.test.ts │ │ │ ├── discover_projs.ts │ │ │ ├── get_app_bundle_id.ts │ │ │ ├── get_mac_bundle_id.ts │ │ │ ├── index.ts │ │ │ ├── list_schemes.ts │ │ │ └── show_build_settings.ts │ │ ├── project-scaffolding │ │ │ ├── __tests__ │ │ │ │ ├── index.test.ts │ │ │ │ ├── scaffold_ios_project.test.ts │ │ │ │ └── scaffold_macos_project.test.ts │ │ │ ├── index.ts │ │ │ ├── scaffold_ios_project.ts │ │ │ └── scaffold_macos_project.ts │ │ ├── session-management │ │ │ ├── __tests__ │ │ │ │ ├── index.test.ts │ │ │ │ ├── session_clear_defaults.test.ts │ │ │ │ ├── session_set_defaults.test.ts │ │ │ │ └── session_show_defaults.test.ts │ │ │ ├── index.ts │ │ │ ├── session_clear_defaults.ts │ │ │ ├── session_set_defaults.ts │ │ │ └── session_show_defaults.ts │ │ ├── simulator │ │ │ ├── __tests__ │ │ │ │ ├── boot_sim.test.ts │ │ │ │ ├── build_run_sim.test.ts │ │ │ │ ├── build_sim.test.ts │ │ │ │ ├── get_sim_app_path.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── install_app_sim.test.ts │ │ │ │ ├── launch_app_logs_sim.test.ts │ │ │ │ ├── launch_app_sim.test.ts │ │ │ │ ├── list_sims.test.ts │ │ │ │ ├── open_sim.test.ts │ │ │ │ ├── record_sim_video.test.ts │ │ │ │ ├── screenshot.test.ts │ │ │ │ ├── stop_app_sim.test.ts │ │ │ │ └── test_sim.test.ts │ │ │ ├── boot_sim.ts │ │ │ ├── build_run_sim.ts │ │ │ ├── build_sim.ts │ │ │ ├── clean.ts │ │ │ ├── describe_ui.ts │ │ │ ├── discover_projs.ts │ │ │ ├── get_app_bundle_id.ts │ │ │ ├── get_sim_app_path.ts │ │ │ ├── index.ts │ │ │ ├── install_app_sim.ts │ │ │ ├── launch_app_logs_sim.ts │ │ │ ├── launch_app_sim.ts │ │ │ ├── list_schemes.ts │ │ │ ├── list_sims.ts │ │ │ ├── open_sim.ts │ │ │ ├── record_sim_video.ts │ │ │ ├── screenshot.ts │ │ │ ├── show_build_settings.ts │ │ │ ├── stop_app_sim.ts │ │ │ └── test_sim.ts │ │ ├── simulator-management │ │ │ ├── __tests__ │ │ │ │ ├── erase_sims.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── reset_sim_location.test.ts │ │ │ │ ├── set_sim_appearance.test.ts │ │ │ │ ├── set_sim_location.test.ts │ │ │ │ └── sim_statusbar.test.ts │ │ │ ├── boot_sim.ts │ │ │ ├── erase_sims.ts │ │ │ ├── index.ts │ │ │ ├── list_sims.ts │ │ │ ├── open_sim.ts │ │ │ ├── reset_sim_location.ts │ │ │ ├── set_sim_appearance.ts │ │ │ ├── set_sim_location.ts │ │ │ └── sim_statusbar.ts │ │ ├── swift-package │ │ │ ├── __tests__ │ │ │ │ ├── active-processes.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── swift_package_build.test.ts │ │ │ │ ├── swift_package_clean.test.ts │ │ │ │ ├── swift_package_list.test.ts │ │ │ │ ├── swift_package_run.test.ts │ │ │ │ ├── swift_package_stop.test.ts │ │ │ │ └── swift_package_test.test.ts │ │ │ ├── active-processes.ts │ │ │ ├── index.ts │ │ │ ├── swift_package_build.ts │ │ │ ├── swift_package_clean.ts │ │ │ ├── swift_package_list.ts │ │ │ ├── swift_package_run.ts │ │ │ ├── swift_package_stop.ts │ │ │ └── swift_package_test.ts │ │ ├── ui-testing │ │ │ ├── __tests__ │ │ │ │ ├── button.test.ts │ │ │ │ ├── describe_ui.test.ts │ │ │ │ ├── gesture.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── key_press.test.ts │ │ │ │ ├── key_sequence.test.ts │ │ │ │ ├── long_press.test.ts │ │ │ │ ├── screenshot.test.ts │ │ │ │ ├── swipe.test.ts │ │ │ │ ├── tap.test.ts │ │ │ │ ├── touch.test.ts │ │ │ │ └── type_text.test.ts │ │ │ ├── button.ts │ │ │ ├── describe_ui.ts │ │ │ ├── gesture.ts │ │ │ ├── index.ts │ │ │ ├── key_press.ts │ │ │ ├── key_sequence.ts │ │ │ ├── long_press.ts │ │ │ ├── screenshot.ts │ │ │ ├── swipe.ts │ │ │ ├── tap.ts │ │ │ ├── touch.ts │ │ │ └── type_text.ts │ │ └── utilities │ │ ├── __tests__ │ │ │ ├── clean.test.ts │ │ │ └── index.test.ts │ │ ├── clean.ts │ │ └── index.ts │ ├── server │ │ └── server.ts │ ├── test-utils │ │ └── mock-executors.ts │ ├── types │ │ └── common.ts │ └── utils │ ├── __tests__ │ │ ├── build-utils.test.ts │ │ ├── environment.test.ts │ │ ├── session-aware-tool-factory.test.ts │ │ ├── session-store.test.ts │ │ ├── simulator-utils.test.ts │ │ ├── test-runner-env-integration.test.ts │ │ └── typed-tool-factory.test.ts │ ├── axe │ │ └── index.ts │ ├── axe-helpers.ts │ ├── build │ │ └── index.ts │ ├── build-utils.ts │ ├── capabilities.ts │ ├── command.ts │ ├── CommandExecutor.ts │ ├── environment.ts │ ├── errors.ts │ ├── execution │ │ └── index.ts │ ├── FileSystemExecutor.ts │ ├── log_capture.ts │ ├── log-capture │ │ └── index.ts │ ├── logger.ts │ ├── logging │ │ └── index.ts │ ├── plugin-registry │ │ └── index.ts │ ├── responses │ │ └── index.ts │ ├── schema-helpers.ts │ ├── sentry.ts │ ├── session-store.ts │ ├── simulator-utils.ts │ ├── template │ │ └── index.ts │ ├── template-manager.ts │ ├── test │ │ └── index.ts │ ├── test-common.ts │ ├── tool-registry.ts │ ├── typed-tool-factory.ts │ ├── validation │ │ └── index.ts │ ├── validation.ts │ ├── version │ │ └── index.ts │ ├── video_capture.ts │ ├── video-capture │ │ └── index.ts │ ├── xcode.ts │ ├── xcodemake │ │ └── index.ts │ └── xcodemake.ts ├── tsconfig.json ├── tsconfig.test.json ├── tsup.config.ts └── vitest.config.ts ``` # Files -------------------------------------------------------------------------------- /.axe-version: -------------------------------------------------------------------------------- ``` 1.1.1 ``` -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- ``` AGENTS.md ``` -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- ``` node_modules build dist coverage *.json *.md ``` -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- ```javascript export default { semi: true, trailingComma: 'all', singleQuote: true, printWidth: 100, tabWidth: 2, endOfLine: 'auto', }; ``` -------------------------------------------------------------------------------- /example_projects/iOS_Calculator/CalculatorAppPackage/.gitignore: -------------------------------------------------------------------------------- ``` .DS_Store /.build /Packages xcuserdata/ DerivedData/ .swiftpm/configuration/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc ``` -------------------------------------------------------------------------------- /example_projects/spm/.gitignore: -------------------------------------------------------------------------------- ``` .DS_Store /.build /Packages xcuserdata/ DerivedData/ .swiftpm/configuration/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* # TypeScript build output dist/ /build/ *.tsbuildinfo # Auto-generated files src/version.ts src/core/generated-plugins.ts src/core/generated-resources.ts # IDE and editor files .idea/ .vscode/* !.vscode/mcp.json !.vscode/launch.json !.vscode/settings.json !.vscode/tasks.json !.vscode/extensions.json *.swp *.swo .DS_Store .env .env.local .env.*.local # Logs logs/ *.log # Test coverage coverage/ # macOS specific .DS_Store .AppleDouble .LSOverride Icon ._* .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Xcode *.xcodeproj/* !*.xcodeproj/project.pbxproj !*.xcodeproj/xcshareddata/ !*.xcworkspace/contents.xcworkspacedata **/xcshareddata/WorkspaceSettings.xcsettings *.xcuserstate project.xcworkspace/ xcuserdata/ # Debug files .nyc_output/ *.map # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.test .env.production # parcel-bundler cache .cache .parcel-cache # Windsurf .windsurfrules # Sentry Config File .sentryclirc # Claude Config File **/.claude/settings.local.json # incremental builds Makefile buildServer.json # Bundled AXe artifacts (generated during build) bundled/ /.mcpregistry_github_token /.mcpregistry_registry_token /key.pem .mcpli .factory ``` -------------------------------------------------------------------------------- /.github/workflows/README.md: -------------------------------------------------------------------------------- ```markdown # Test workflow trigger ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown <img src="banner.png" alt="XcodeBuild MCP" width="600"/> A Model Context Protocol (MCP) server that provides Xcode-related tools for integration with AI assistants and other MCP clients. [](https://github.com/cameroncooke/XcodeBuildMCP/actions/workflows/ci.yml) [](https://badge.fury.io/js/xcodebuildmcp) [](https://opensource.org/licenses/MIT) [](https://nodejs.org/) [](https://developer.apple.com/xcode/) [](https://www.apple.com/macos/) [](https://modelcontextprotocol.io/) [](https://deepwiki.com/cameroncooke/XcodeBuildMCP) ## Table of contents - [Overview](#overview) - [Why?](#why) - [Features](#features) - [Xcode project management](#xcode-project-management) - [Swift Package Manager](#swift-package-manager) - [Simulator management](#simulator-management) - [Device management](#device-management) - [App utilities](#app-utilities) - [MCP Resources](#mcp-resources) - [Getting started](#getting-started) - [Prerequisites](#prerequisites) - [Configure your MCP client](#configure-your-mcp-client) - [One click install](#one-click-install) - [General installation](#general-installation) - [Specific client installation instructions](#specific-client-installation-instructions) - [OpenAI Codex CLI](#openai-codex-cli) - [Claude Code CLI](#claude-code-cli) - [Smithery](#smithery) - [MCP Compatibility](#mcp-compatibility) - [Incremental build support](#incremental-build-support) - [Dynamic Tools](#dynamic-tools) - [What is Dynamic Tools?](#what-is-dynamic-tools) - [How to Enable Dynamic Tools](#how-to-enable-dynamic-tools) - [Usage Example](#usage-example) - [Client Compatibility](#client-compatibility) - [Selective Workflow Loading (Static Mode)](#selective-workflow-loading-static-mode) - [Code Signing for Device Deployment](#code-signing-for-device-deployment) - [Troubleshooting](#troubleshooting) - [Doctor Tool](#doctor-tool) - [Privacy](#privacy) - [What is sent to Sentry?](#what-is-sent-to-sentry) - [Opting Out of Sentry](#opting-out-of-sentry) - [Demos](#demos) - [Autonomously fixing build errors in Cursor](#autonomously-fixing-build-errors-in-cursor) - [Utilising the new UI automation and screen capture features](#utilising-the-new-ui-automation-and-screen-capture-features) - [Building and running iOS app in Claude Desktop](#building-and-running-ios-app-in-claude-desktop) - [Contributing](#contributing) - [Licence](#licence) ## Overview XcodeBuildMCP is a Model Context Protocol (MCP) server that exposes Xcode operations as tools and resources for AI assistants and other MCP clients. Built with a modern plugin architecture, it provides a comprehensive set of self-contained tools organized into workflow-based directories, plus MCP resources for efficient data access, enabling programmatic interaction with Xcode projects, simulators, devices, and Swift packages through a standardized interface.  <caption>Using Cursor to build, install, and launch an app on the iOS simulator while capturing logs at run-time.</caption> ## Why? The XcodeBuild MCP tool exists primarily to streamline and standardise interaction between AI agents and Xcode projects. By providing dedicated tools for common Xcode operations, it removes reliance on manual or potentially incorrect command-line invocations. This ensures a reliable and efficient development process, allowing agents to seamlessly leverage Xcode's capabilities while reducing the risk of configuration errors. Critically, this MCP enables AI agents to independently validate code changes by building projects, inspecting errors, and iterating autonomously. In contrast to user-driven tools like Sweetpad, XcodeBuild MCP empowers agents to automate these workflows effectively. ## Features The XcodeBuildMCP server provides the following tool capabilities: ### Xcode project management - **Discover Projects**: Xcode projects and workspaces discovery - **Build Operations**: Platform-specific build tools for macOS, iOS simulator, and iOS device targets - **Project Information**: Tools to list schemes and show build settings for Xcode projects and workspaces - **Clean Operations**: Clean build products using xcodebuild's native clean action - **Incremental build support**: Lightning fast builds using incremental build support (experimental, opt-in required) - **Project Scaffolding**: Create new iOS and macOS projects from modern templates with workspace + SPM package architecture, customizable bundle identifiers, deployment targets, and device families ### Swift Package Manager - **Build Packages**: Build Swift packages with configuration and architecture options - **Run Tests**: Execute Swift package test suites with filtering and parallel execution - **Run Executables**: Execute package binaries with timeout handling and background execution support - **Process Management**: List and stop long-running executables started with Swift Package tools - **Clean Artifacts**: Remove build artifacts and derived data for fresh builds ### Simulator management - **Simulator Control**: List, boot, and open simulators - **App Lifecycle**: Complete app management - install, launch, and stop apps on simulators - **Log Capture**: Capture run-time logs from a simulator - **UI Automation**: Interact with simulator UI elements - **Screenshot**: Capture screenshots from a simulator - **Video Capture**: Start/stop simulator video capture to MP4 (AXe v1.1.0+) ### Device management - **Device Discovery**: List connected physical Apple devices over USB or Wi-Fi - **App Lifecycle**: Complete app management - build, install, launch, and stop apps on physical devices - **Testing**: Run test suites on physical devices with detailed results and cross-platform support - **Log Capture**: Capture console output from apps running on physical Apple devices - **Wireless Connectivity**: Support for devices connected over Wi-Fi networks ### App utilities - **Bundle ID Extraction**: Extract bundle identifiers from app bundles across all Apple platforms - **App Lifecycle Management**: Complete app lifecycle control across all platforms - Launch apps on simulators, physical devices, and macOS - Stop running apps with process ID or bundle ID management - Process monitoring and control for comprehensive app management ### MCP Resources For clients that support MCP resources XcodeBuildMCP provides efficient URI-based data access: - **Simulators Resource** (`xcodebuildmcp://simulators`): Direct access to available iOS simulators with UUIDs and states - **Devices Resource** (`xcodebuildmcp://devices`): Direct access to connected physical Apple devices with UDIDs and states - **Doctor Resource** (`xcodebuildmcp://doctor`): Direct access to environment information such as Xcode version, macOS version, and Node.js version ## Getting started ### Prerequisites - macOS 14.5 or later - Xcode 16.x or later - Node 18.x or later > Video capture requires the bundled AXe binary (v1.1.0+). Run `npm run bundle:axe` once locally before using `record_sim_video`. This is not required for unit tests. Configure your MCP client #### One click install For a quick install, you can use the following links: [](https://cursor.com/en/install-mcp?name=XcodeBuildMCP&config=eyJ0eXBlIjoic3RkaW8iLCJjb21tYW5kIjoibnB4IC15IHhjb2RlYnVpbGRtY3BAbGF0ZXN0IiwiZW52Ijp7IklOQ1JFTUVOVEFMX0JVSUxEU19FTkFCTEVEIjoiZmFsc2UiLCJYQ09ERUJVSUxETUNQX1NFTlRSWV9ESVNBQkxFRCI6ImZhbHNlIn19) [<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](https://insiders.vscode.dev/redirect/mcp/install?name=XcodeBuildMCP&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22xcodebuildmcp%40latest%22%5D%7D) [<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect/mcp/install?name=XcodeBuildMCP&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22xcodebuildmcp%40latest%22%5D%7D&quality=insiders) #### General installation Most MCP clients (Cursor, VS Code, Windsurf, Claude Desktop etc) have standardised on the following JSON configuration format, just add the the following to your client's JSON configuration's `mcpServers` object: ```json "XcodeBuildMCP": { "command": "npx", "args": [ "-y", "xcodebuildmcp@latest" ] } ``` #### Specific client installation instructions ##### OpenAI Codex CLI Codex uses a toml configuration file to configure MCP servers. To configure XcodeBuildMCP with [OpenAI's Codex CLI](https://github.com/openai/codex), add the following configuration to your Codex CLI config file: ```toml [mcp_servers.XcodeBuildMCP] command = "npx" args = ["-y", "xcodebuildmcp@latest"] env = { "INCREMENTAL_BUILDS_ENABLED" = "false", "XCODEBUILDMCP_SENTRY_DISABLED" = "false" } ``` For more information see [OpenAI Codex MCP Server Configuration](https://github.com/openai/codex/blob/main/codex-rs/config.md#mcp_servers) documentation. ##### Claude Code CLI To use XcodeBuildMCP with [Claude Code](https://code.anthropic.com), you can add it via the command line: ```bash # Add XcodeBuildMCP server to Claude Code claude mcp add XcodeBuildMCP npx xcodebuildmcp@latest # Or with environment variables claude mcp add XcodeBuildMCP npx xcodebuildmcp@latest -e INCREMENTAL_BUILDS_ENABLED=false -e XCODEBUILDMCP_SENTRY_DISABLED=false ``` ##### Smithery To install XcodeBuildMCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@cameroncooke/XcodeBuildMCP): ```bash npx -y @smithery/cli install @cameroncooke/XcodeBuildMCP --client claude ``` > [!IMPORTANT] > Please note that XcodeBuildMCP will request xcodebuild to skip macro validation. This is to avoid errors when building projects that use Swift Macros. #### MCP Compatibility XcodeBuildMCP supports both MCP tools, resources and sampling. At time of writing the following editors have varying levels of MCP feature support: | Editor | Tools | Resources | Samplng | |--------|-------|-----------|---------| | **VS Code** | ✅ | ✅ | ✅ | | **Cursor** | ✅ | ❌ | ❌ | | **Windsurf** | ✅ | ❌ | ❌ | | **Claude Code** | ✅ | ✅ | ❌ | | **Claude Desktop** | ✅ | ✅ | ❌ | ## Incremental build support XcodeBuildMCP includes experimental support for incremental builds. This feature is disabled by default and can be enabled by setting the `INCREMENTAL_BUILDS_ENABLED` environment variable to `true`: To enable incremental builds, set the `INCREMENTAL_BUILDS_ENABLED` environment variable to `true`: Example MCP configuration: ```json "XcodeBuildMCP": { ... "env": { "INCREMENTAL_BUILDS_ENABLED": "true" } } ``` > [!IMPORTANT] > Please note that incremental builds support is currently highly experimental and your mileage may vary. Please report any issues you encounter to the [issue tracker](https://github.com/cameroncooke/XcodeBuildMCP/issues). ## Dynamic Tools XcodeBuildMCP supports dynamic tool loading to optimize context window usage in AI assistants. This feature is particularly useful for managing the extensive toolset that XcodeBuildMCP provides. ### What is Dynamic Tools? By default, XcodeBuildMCP loads all available tools at startup (Static Mode), which provides immediate access to the complete toolset but uses a larger context window. Dynamic Tools mode solves this by: 1. **Starting minimal**: Only essential tools like `discover_tools` and `discover_projs` are available initially 2. **AI-powered discovery**: When an AI agent identifies XcodeBuildMCP can help with development tasks, it automatically uses the `discover_tools` tool 3. **Intelligent loading**: The server uses an LLM call to identify the most relevant workflow group and dynamically loads only those tools 4. **Context efficiency**: Reduces the initial context footprint from the entire list of tools to just 2 discovery tools while maintaining full functionality ### How to Enable Dynamic Tools To enable dynamic tools, set the `XCODEBUILDMCP_DYNAMIC_TOOLS` environment variable to `true`: Example MCP client configuration: ```json "XcodeBuildMCP": { ... "env": { "XCODEBUILDMCP_DYNAMIC_TOOLS": "true" } } ``` ### Usage Example Once enabled, AI agents automatically discover and load relevant tools based on context. For example, when you mention working on an iOS app or the agent detects iOS development tasks in your workspace, it will automatically use the `discover_tools` tool to load the appropriate simulator and project tools needed for your workflow. ### Client Compatibility Dynamic Tools requires MCP clients that support **MCP Sampling** for the AI-powered tool discovery to function: | Editor | Dynamic Tools Support | |--------|----------------------| | **VS Code** | ✅ | | **Cursor** | ❌ (No MCP Sampling) | | **Windsurf** | ❌ (No MCP Sampling) | | **Claude Code** | ❌ (No MCP Sampling) | | **Claude Desktop** | ❌ (No MCP Sampling) | > [!NOTE] > For clients that don't support MCP Sampling, XcodeBuildMCP will automatically fall back to Static Mode, loading all tools at startup regardless of the `XCODEBUILDMCP_DYNAMIC_TOOLS` setting. ### Selective Workflow Loading (Static Mode) For clients that don't support MCP Sampling but still want to reduce context window usage, you can selectively load only specific workflows using the `XCODEBUILDMCP_ENABLED_WORKFLOWS` environment variable: ```json "XcodeBuildMCP": { ... "env": { "XCODEBUILDMCP_ENABLED_WORKFLOWS": "simulator,device,project-discovery" } } ``` **Available Workflows:** - `device` (14 tools) - iOS Device Development - `simulator` (18 tools) - iOS Simulator Development - `simulator-management` (8 tools) - Simulator Management - `swift-package` (6 tools) - Swift Package Manager - `project-discovery` (5 tools) - Project Discovery - `macos` (11 tools) - macOS Development - `ui-testing` (11 tools) - UI Testing & Automation - `logging` (4 tools) - Log Capture & Management - `project-scaffolding` (2 tools) - Project Scaffolding - `utilities` (1 tool) - Project Utilities - `doctor` (1 tool) - System Doctor - `discovery` (1 tool) - Dynamic Tool Discovery > [!NOTE] > The `XCODEBUILDMCP_ENABLED_WORKFLOWS` setting only works in Static Mode. If `XCODEBUILDMCP_DYNAMIC_TOOLS=true` is set, the selective workflow setting will be ignored. ## Code Signing for Device Deployment For device deployment features to work, code signing must be properly configured in Xcode **before** using XcodeBuildMCP device tools: 1. Open your project in Xcode 2. Select your project target 3. Go to "Signing & Capabilities" tab 4. Configure "Automatically manage signing" and select your development team 5. Ensure a valid provisioning profile is selected > **Note**: XcodeBuildMCP cannot configure code signing automatically. This initial setup must be done once in Xcode, after which the MCP device tools can build, install, and test apps on physical devices. ## Troubleshooting If you encounter issues with XcodeBuildMCP, the doctor tool can help identify the problem by providing detailed information about your environment and dependencies. ### Doctor Tool The doctor tool is a standalone utility that checks your system configuration and reports on the status of all dependencies required by XcodeBuildMCP. It's particularly useful when reporting issues. ```bash # Run the doctor tool using npx npx --package xcodebuildmcp@latest xcodebuildmcp-doctor ``` The doctor tool will output comprehensive information about: - System and Node.js environment - Xcode installation and configuration - Required dependencies (xcodebuild, AXe, etc.) - Environment variables affecting XcodeBuildMCP - Feature availability status When reporting issues on GitHub, please include the full output from the doctor tool to help with troubleshooting. ## Privacy This project uses [Sentry](https://sentry.io/) for error monitoring and diagnostics. Sentry helps us track issues, crashes, and unexpected errors to improve the reliability and stability of XcodeBuildMCP. ### What is sent to Sentry? - Only error-level logs and diagnostic information are sent to Sentry by default. - Error logs may include details such as error messages, stack traces, and (in some cases) file paths or project names. You can review the sources in this repository to see exactly what is logged. ### Opting Out of Sentry - If you do not wish to send error logs to Sentry, you can opt out by setting the environment variable `XCODEBUILDMCP_SENTRY_DISABLED=true`. Example MCP client configuration: ```json "XcodeBuildMCP": { ... "env": { "XCODEBUILDMCP_SENTRY_DISABLED": "true" } } ``` ## Demos ### Autonomously fixing build errors in Cursor  ### Utilising the new UI automation and screen capture features  ### Building and running iOS app in Claude Desktop https://github.com/user-attachments/assets/e3c08d75-8be6-4857-b4d0-9350b26ef086 ## Contributing [](https://www.typescriptlang.org/) [](https://nodejs.org/) Contributions are welcome! Here's how you can help improve XcodeBuildMCP. See our documentation for development: - [CONTRIBUTING](docs/CONTRIBUTING.md) - Contribution guidelines and development setup - [CODE_QUALITY](docs/CODE_QUALITY.md) - Code quality standards, linting, and architectural rules - [TESTING](docs/TESTING.md) - Testing principles and patterns - [ARCHITECTURE](docs/ARCHITECTURE.md) - System architecture and design principles ## Licence This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ``` -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- ```markdown AGENTS.md ``` -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- ```markdown # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [email protected]. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ``` -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- ```markdown This file provides guidance to AI assisants (Claude Code, Cursor etc) when working with code in this repository. ## Project Overview XcodeBuildMCP is a Model Context Protocol (MCP) server providing standardized tools for AI assistants to interact with Xcode projects, iOS simulators, devices, and Apple development workflows. It's a TypeScript/Node.js project that runs as a stdio-based MCP server. ## Common Commands ### Build & Development ```bash npm run build # Compile TypeScript with tsup, generates version info npm run dev # Watch mode development npm run bundle:axe # Bundle axe CLI tool for simulator automation (needed when using local MCP server) npm run test # Run complete Vitest test suite npm run test:watch # Watch mode testing npm run lint # ESLint code checking npm run lint:fix # ESLint code checking and fixing npm run format:check # Prettier code checking npm run format # Prettier code formatting npm run typecheck # TypeScript type checking npm run inspect # Run interactive MCP protocol inspector npm run doctor # Doctor CLI ``` ### Development with Reloaderoo **Reloaderoo** (v1.1.2+) provides CLI-based testing and hot-reload capabilities for XcodeBuildMCP without requiring MCP client configuration. #### Quick Start **CLI Mode (Testing & Development):** ```bash # List all tools npx reloaderoo inspect list-tools -- node build/index.js # Call any tool npx reloaderoo inspect call-tool list_devices --params '{}' -- node build/index.js # Get server information npx reloaderoo inspect server-info -- node build/index.js # List and read resources npx reloaderoo inspect list-resources -- node build/index.js npx reloaderoo inspect read-resource "xcodebuildmcp://devices" -- node build/index.js ``` **Proxy Mode (MCP Client Integration):** ```bash # Start persistent server for MCP clients npx reloaderoo proxy -- node build/index.js # With debug logging npx reloaderoo proxy --log-level debug -- node build/index.js # Then ask AI: "Please restart the MCP server to load my changes" ``` #### All CLI Inspect Commands Reloaderoo provides 8 inspect subcommands for comprehensive MCP server testing: ```bash # Server capabilities and information npx reloaderoo inspect server-info -- node build/index.js # Tool management npx reloaderoo inspect list-tools -- node build/index.js npx reloaderoo inspect call-tool <tool_name> --params '<json>' -- node build/index.js # Resource access npx reloaderoo inspect list-resources -- node build/index.js npx reloaderoo inspect read-resource "<uri>" -- node build/index.js # Prompt management npx reloaderoo inspect list-prompts -- node build/index.js npx reloaderoo inspect get-prompt <name> --args '<json>' -- node build/index.js # Connectivity testing npx reloaderoo inspect ping -- node build/index.js ``` #### Advanced Options ```bash # Custom working directory npx reloaderoo inspect list-tools --working-dir /custom/path -- node build/index.js # Timeout configuration npx reloaderoo inspect call-tool slow_tool --timeout 60000 --params '{}' -- node build/index.js # Use timeout configuration if needed npx reloaderoo inspect server-info --timeout 60000 -- node build/index.js # Debug logging (use proxy mode for detailed logging) npx reloaderoo proxy --log-level debug -- node build/index.js ``` #### Key Benefits - ✅ **No MCP Client Setup**: Direct CLI access to all 84+ tools - ✅ **Raw JSON Output**: Perfect for AI agents and programmatic use - ✅ **Hot-Reload Support**: `restart_server` tool for MCP client development - ✅ **Claude Code Compatible**: Automatic content block consolidation - ✅ **8 Inspect Commands**: Complete MCP protocol testing capabilities - ✅ **Universal Compatibility**: Works on any system via npx For complete documentation, examples, and troubleshooting, see @docs/RELOADEROO.md ## Architecture Overview ### Plugin-Based MCP architecture XcodeBuildMCP uses the concept of configuration by convention for MCP exposing and running MCP capabilities like tools and resources. This means to add a new tool or resource, you simply create a new file in the appropriate directory and it will be automatically loaded and exposed to MCP clients. #### Tools Tools are the core of the MCP server and are the primary way to interact with the server. They are organized into directories by their functionality and are automatically loaded and exposed to MCP clients. For more information see @docs/PLUGIN_DEVELOPMENT.md #### Resources Resources are the secondary way to interact with the server. They are used to provide data to tools and are organized into directories by their functionality and are automatically loaded and exposed to MCP clients. For more information see @docs/PLUGIN_DEVELOPMENT.md ### Operating Modes XcodeBuildMCP has two modes to manage its extensive toolset, controlled by the `XCODEBUILDMCP_DYNAMIC_TOOLS` environment variable. #### Static Mode (Default) - **Environment**: `XCODEBUILDMCP_DYNAMIC_TOOLS=false` or unset. - **Behavior**: All tools are loaded at startup. This provides immediate access to the full toolset but uses a larger context window. #### Dynamic Mode (AI-Powered) - **Environment**: `XCODEBUILDMCP_DYNAMIC_TOOLS=true`. - **Behavior**: Only the `discover_tools` tool is available initially. You can use this tool by providing a natural language task description. The server then uses an LLM call (via MCP Sampling) to identify the most relevant workflow group and dynamically loads only those tools. This conserves context window space. #### Claude Code Compatibility Workaround - **Detection**: Automatic detection when running under Claude Code. - **Purpose**: Workaround for Claude Code's MCP specification violation where it only displays the first content block in tool responses. - **Behavior**: When Claude Code is detected, multiple content blocks are automatically consolidated into a single text response, separated by `---` dividers. This ensures all information (including test results and stderr warnings) is visible to Claude Code users. ### Core Architecture Layers 1. **MCP Transport**: stdio protocol communication 2. **Plugin Discovery**: Automatic tool AND resource registration system 3. **MCP Resources**: URI-based data access (e.g., `xcodebuildmcp://simulators`) 4. **Tool Implementation**: Self-contained workflow modules 5. **Shared Utilities**: Command execution, build management, validation 6. **Types**: Shared interfaces and Zod schemas For more information see @docs/ARCHITECTURE.md ## Testing The project enforces a strict **Dependency Injection (DI)** testing philosophy. - **NO Vitest Mocking**: The use of `vi.mock()`, `vi.fn()`, `vi.spyOn()`, etc., is **completely banned**. - **Executors**: All external interactions (like running commands or accessing the file system) are handled through injectable "executors". - `CommandExecutor`: For running shell commands. - `FileSystemExecutor`: For file system operations. - **Testing Logic**: Tests import the core `...Logic` function from a tool file and pass in a mock executor (`createMockExecutor` or `createMockFileSystemExecutor`) to simulate different outcomes. This approach ensures that tests are robust, easy to maintain, and verify the actual integration between components without being tightly coupled to implementation details. For complete guidelines, refer to @docs/TESTING.md. ## TypeScript Import Standards This project uses **TypeScript file extensions** (`.ts`) for all relative imports to ensure compatibility with native TypeScript runtimes. ### Import Rules - ✅ **Use `.ts` extensions**: `import { tool } from './tool.ts'` - ✅ **Use `.ts` for re-exports**: `export { default } from '../shared/tool.ts'` - ✅ **External packages use `.js`**: `import { McpServer } from '@camsoft/mcp-sdk/server/mcp.js'` - ❌ **Never use `.js` for internal files**: `import { tool } from './tool.js'` ← ESLint error ### Benefits 1. **Future-proof**: Compatible with native TypeScript runtimes (Bun, Deno, Node.js --loader) 2. **IDE Experience**: Direct navigation to source TypeScript files 3. **Consistency**: Import path matches the actual file you're editing 4. **Modern Standard**: Aligns with TypeScript 4.7+ `allowImportingTsExtensions` ### ESLint Enforcement The project automatically enforces this standard: ```bash npm run lint # Will catch .js imports for internal files ``` This ensures all new code follows the `.ts` import pattern and maintains compatibility with both current and future TypeScript execution environments. ## Release Process Follow standardized development workflow with feature branches, structured pull requests, and linear commit history. **Never push to main directly or force push without permission.** For complete guidelines, refer to @docs/RELEASE_PROCESS.md ## Useful external resources ### Model Context Protocol https://modelcontextprotocol.io/llms-full.txt ### MCP Specification https://modelcontextprotocol.io/specification ### MCP Inspector https://github.com/modelcontextprotocol/inspector ### MCP Client SDKs https://github.com/modelcontextprotocol/typescript-sdk ``` -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- ```markdown # Contributing Contributions are welcome! Here's how you can help improve XcodeBuildMCP. ## Local development setup ### Prerequisites In addition to the prerequisites mentioned in the [Getting started](README.md/#getting-started) section of the README, you will also need: - Node.js (v18 or later) - npm #### Optional: Enabling UI Automation When running locally, you'll need to install AXe for UI automation: ```bash # Install axe (required for UI automation) brew tap cameroncooke/axe brew install axe ``` ### Installation 1. Clone the repository 2. Install dependencies: ``` npm install ``` 3. Build the project: ``` npm run build ``` 4. Start the server: ``` node build/index.js ``` ### Configure your MCP client Most MCP clients (Cursor, VS Code, Windsurf, Claude Desktop etc) have standardised on the following JSON configuration format, just add the the following to your client's JSON configuration's `mcpServers` object: ```json { "mcpServers": { "XcodeBuildMCP": { "command": "node", "args": [ "/path_to/XcodeBuildMCP/build/index.js" ] } } } ``` ### Developing using VS Code 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. To make your development workflow in VS Code more efficient: 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. 2. **Launch the Debugger**: Press `F5` to attach the Node.js debugger. Once these steps are completed, you can utilize the tools from the MCP server you are developing within this repository in agent mode. For more details on how to work with MCP servers in VS Code see: https://code.visualstudio.com/docs/copilot/chat/mcp-servers ### Debugging #### MCP Inspector (Basic Debugging) You can use MCP Inspector for basic debugging via: ```bash npm run inspect ``` or if you prefer the explicit command: ```bash npx @modelcontextprotocol/inspector node build/index.js ``` #### Reloaderoo (Advanced Debugging) - **RECOMMENDED** For development and debugging, we strongly recommend using **Reloaderoo**, which provides hot-reloading capabilities and advanced debugging features for MCP servers. Reloaderoo operates in two modes: ##### 1. Proxy Mode (Hot-Reloading) Provides transparent hot-reloading without disconnecting your MCP client: ```bash # Install reloaderoo globally npm install -g reloaderoo # Start XcodeBuildMCP through reloaderoo proxy reloaderoo -- node build/index.js ``` **Benefits**: - 🔄 Hot-reload server without restarting client - 🛠️ Automatic `restart_server` tool added to toolset - 🌊 Transparent MCP protocol forwarding - 📡 Full protocol support (tools, resources, prompts) **MCP Client Configuration for Proxy Mode**: ```json "XcodeBuildMCP": { "command": "reloaderoo", "args": ["--", "node", "/path/to/XcodeBuildMCP/build/index.js"], "env": { "XCODEBUILDMCP_DYNAMIC_TOOLS": "true", "XCODEBUILDMCP_DEBUG": "true" } } ``` ##### 2. Inspection Mode (Raw MCP Debugging) Exposes debug tools for making raw MCP protocol calls and inspecting server responses: ```bash # Start reloaderoo in inspection mode reloaderoo inspect mcp -- node build/index.js ``` **Available Debug Tools**: - `list_tools` - List all server tools - `call_tool` - Execute any server tool with parameters - `list_resources` - List all server resources - `read_resource` - Read any server resource - `list_prompts` - List all server prompts - `get_prompt` - Get any server prompt - `get_server_info` - Get comprehensive server information - `ping` - Test server connectivity **MCP Client Configuration for Inspection Mode**: ```json "XcodeBuildMCP": { "command": "node", "args": [ "/path/to/reloaderoo/dist/bin/reloaderoo.js", "inspect", "mcp", "--working-dir", "/path/to/XcodeBuildMCP", "--", "node", "/path/to/XcodeBuildMCP/build/index.js" ], "env": { "XCODEBUILDMCP_DYNAMIC_TOOLS": "true", "XCODEBUILDMCP_DEBUG": "true" } } ``` #### Operating Mode Testing Test both static and dynamic modes during development: ```bash # Test static mode (all tools loaded immediately) XCODEBUILDMCP_DYNAMIC_TOOLS=false reloaderoo inspect mcp -- node build/index.js # Test dynamic mode (only discover_tools initially available) XCODEBUILDMCP_DYNAMIC_TOOLS=true reloaderoo inspect mcp -- node build/index.js ``` **Key Differences to Test**: - **Static Mode**: 50+ tools available immediately via `list_tools` - **Dynamic Mode**: Only 2 tools (`discover_tools` and `discover_projs`) available initially - **Dynamic Discovery**: After calling `discover_tools`, additional workflow tools become available #### Using XcodeBuildMCP doctor tool Running the XcodeBuildMCP server with the environmental variable `XCODEBUILDMCP_DEBUG=true` will expose a new doctor MCP tool called `doctor` which your agent can call to get information about the server's environment, available tools, and configuration status. > [!NOTE] > You can also call the doctor tool directly using the following command but be advised that the output may vary from that of the MCP tool call due to environmental differences: > ```bash > npm run doctor > ``` #### Development Workflow with Reloaderoo 1. **Start Development Session**: ```bash # Terminal 1: Start in hot-reload mode reloaderoo -- node build/index.js # Terminal 2: Start build watcher npm run build:watch ``` 2. **Make Changes**: Edit source code in `src/` 3. **Test Changes**: Ask your AI client to restart the server: ``` "Please restart the MCP server to load my changes" ``` The AI will automatically call the `restart_server` tool provided by reloaderoo. 4. **Verify Changes**: New functionality immediately available without reconnecting client ## Architecture and Code Standards Before making changes, please familiarize yourself with: - [ARCHITECTURE.md](ARCHITECTURE.md) - Comprehensive architectural overview - [CLAUDE.md](CLAUDE.md) - AI assistant guidelines and testing principles - [TOOLS.md](TOOLS.md) - Complete tool documentation - [TOOL_OPTIONS.md](TOOL_OPTIONS.md) - Tool configuration options ### Code Quality Requirements 1. **Follow existing code patterns and structure** 2. **Use TypeScript strictly** - no `any` types, proper typing throughout 3. **Add proper error handling and logging** - all failures must set `isError: true` 4. **Update documentation for new features** 5. **Test with example projects before submitting** ### Testing Standards All contributions must adhere to the testing standards outlined in the [**XcodeBuildMCP Plugin Testing Guidelines (docs/TESTING.md)**](docs/TESTING.md). This is the canonical source of truth for all testing practices. **Key Principles (Summary):** - **No Vitest Mocking**: All forms of `vi.mock`, `vi.fn`, `vi.spyOn`, etc., are strictly forbidden. - **Dependency Injection**: All external dependencies (command execution, file system access) must be injected into tool logic functions using the `CommandExecutor` and `FileSystemExecutor` patterns. - **Test Production Code**: Tests must import and execute the actual tool logic, not mock implementations. - **Comprehensive Coverage**: Tests must cover input validation, command generation, and output processing. Please read [docs/TESTING.md](docs/TESTING.md) in its entirety before writing tests. ### Pre-Commit Checklist **MANDATORY**: Run these commands before any commit and ensure they all pass: ```bash # 1. Run linting (must pass with 0 errors) npm run lint # 2. Run formatting (must format all files) npm run format # 3. Run build (must compile successfully) npm run build # 4. Run tests (all tests must pass) npm test ``` **NO EXCEPTIONS**: Code that fails any of these commands cannot be committed. ## Making changes 1. Fork the repository and create a new branch 2. Follow the TypeScript best practices and existing code style 3. Add proper parameter validation and error handling ## Plugin Development For comprehensive instructions on creating new tools and workflow groups, see our dedicated [Plugin Development Guide](docs/PLUGIN_DEVELOPMENT.md). The plugin development guide covers: - Auto-discovery system architecture - Tool creation with dependency injection patterns - Workflow group organization - Testing guidelines and patterns - Integration with dynamic tool discovery ### Quick Plugin Development Checklist 1. Choose appropriate workflow directory in `src/mcp/tools/` 2. Follow naming conventions: `{action}_{target}_{specifier}_{projectType}` 3. Use dependency injection pattern with separate logic functions 4. Create comprehensive tests using `createMockExecutor()` 5. Add workflow metadata if creating new workflow group See [PLUGIN_DEVELOPMENT.md](docs/PLUGIN_DEVELOPMENT.md) for complete details. ### Working with Project Templates XcodeBuildMCP uses external template repositories for the iOS and macOS project scaffolding features. These templates are maintained separately to allow independent versioning and updates. #### Template Repositories - **iOS Template**: [XcodeBuildMCP-iOS-Template](https://github.com/cameroncooke/XcodeBuildMCP-iOS-Template) - **macOS Template**: [XcodeBuildMCP-macOS-Template](https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template) #### Local Template Development When developing or testing changes to the templates: 1. Clone the template repository you want to work on: ```bash git clone https://github.com/cameroncooke/XcodeBuildMCP-iOS-Template.git git clone https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template.git ``` 2. Set the appropriate environment variable to use your local template: ```bash # For iOS template development export XCODEBUILDMCP_IOS_TEMPLATE_PATH=/path/to/XcodeBuildMCP-iOS-Template # For macOS template development export XCODEBUILDMCP_MACOS_TEMPLATE_PATH=/path/to/XcodeBuildMCP-macOS-Template ``` 3. When using MCP clients, add these environment variables to your MCP configuration: ```json "XcodeBuildMCP": { "command": "node", "args": ["/path_to/XcodeBuildMCP/build/index.js"], "env": { "XCODEBUILDMCP_IOS_TEMPLATE_PATH": "/path/to/XcodeBuildMCP-iOS-Template", "XCODEBUILDMCP_MACOS_TEMPLATE_PATH": "/path/to/XcodeBuildMCP-macOS-Template" } } ``` 4. The scaffold tools will use your local templates instead of downloading from GitHub releases. #### Template Versioning - Templates are versioned independently from XcodeBuildMCP - The default template version is specified in `package.json` under `templateVersion` - You can override the template version with `XCODEBUILD_MCP_TEMPLATE_VERSION` environment variable - To update the default template version: 1. Update `templateVersion` in `package.json` 2. Run `npm run build` to regenerate version.ts 3. Create a new XcodeBuildMCP release #### Testing Template Changes 1. Make changes to your local template 2. Test scaffolding with your changes using the local override 3. Verify the scaffolded project builds and runs correctly 4. Once satisfied, create a PR in the template repository 5. After merging, create a new release in the template repository using the release script ## Testing 1. Build the project with `npm run build` 2. Test your changes with MCP Inspector 3. Verify tools work correctly with different MCP clients ## Submitting 1. Run `npm run lint` to check for linting issues (use `npm run lint:fix` to auto-fix) 2. Run `npm run format:check` to verify formatting (use `npm run format` to fix) 3. Update documentation if you've added or modified features 4. Add your changes to the CHANGELOG.md file 5. Push your changes and create a pull request with a clear description 6. Link any related issues For major changes or new features, please open an issue first to discuss your proposed changes. ## Code of Conduct Please follow our [Code of Conduct](CODE_OF_CONDUCT.md) and community guidelines. ``` -------------------------------------------------------------------------------- /src/utils/capabilities.ts: -------------------------------------------------------------------------------- ```typescript ``` -------------------------------------------------------------------------------- /example_projects/iOS/.vscode/settings.json: -------------------------------------------------------------------------------- ```json {} ``` -------------------------------------------------------------------------------- /example_projects/spm/Sources/spm/main.swift: -------------------------------------------------------------------------------- ```swift print("Hello, world!") ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- ```yaml blank_issues_enabled: false ``` -------------------------------------------------------------------------------- /.cursor/environment.json: -------------------------------------------------------------------------------- ```json { "agentCanUpdateSnapshot": true } ``` -------------------------------------------------------------------------------- /src/utils/version/index.ts: -------------------------------------------------------------------------------- ```typescript export { version } from '../../version.ts'; ``` -------------------------------------------------------------------------------- /src/utils/test/index.ts: -------------------------------------------------------------------------------- ```typescript export { handleTestLogic } from '../test-common.ts'; ``` -------------------------------------------------------------------------------- /src/utils/template/index.ts: -------------------------------------------------------------------------------- ```typescript export { TemplateManager } from '../template-manager.ts'; ``` -------------------------------------------------------------------------------- /example_projects/iOS_Calculator/CalculatorApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- ```json { "info" : { "author" : "xcode", "version" : 1 } } ``` -------------------------------------------------------------------------------- /example_projects/iOS/MCPTest/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- ```json { "info" : { "author" : "xcode", "version" : 1 } } ``` -------------------------------------------------------------------------------- /example_projects/iOS/MCPTest/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- ```json { "info" : { "author" : "xcode", "version" : 1 } } ``` -------------------------------------------------------------------------------- /example_projects/macOS/MCPTest/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- ```json { "info" : { "author" : "xcode", "version" : 1 } } ``` -------------------------------------------------------------------------------- /example_projects/macOS/MCPTest/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- ```json { "info" : { "author" : "xcode", "version" : 1 } } ``` -------------------------------------------------------------------------------- /src/utils/log-capture/index.ts: -------------------------------------------------------------------------------- ```typescript export { startLogCapture, stopLogCapture } from '../log_capture.ts'; ``` -------------------------------------------------------------------------------- /src/utils/xcodemake/index.ts: -------------------------------------------------------------------------------- ```typescript export { isXcodemakeEnabled, isXcodemakeAvailable, doesMakefileExist } from '../xcodemake.ts'; ``` -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- ```yaml # These are supported funding model platforms github: cameroncooke buy_me_a_coffee: cameroncooke ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator-management/boot_sim.ts: -------------------------------------------------------------------------------- ```typescript // Re-export from simulator to avoid duplication export { default } from '../simulator/boot_sim.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator-management/open_sim.ts: -------------------------------------------------------------------------------- ```typescript // Re-export from simulator to avoid duplication export { default } from '../simulator/open_sim.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator-management/list_sims.ts: -------------------------------------------------------------------------------- ```typescript // Re-export from simulator to avoid duplication export { default } from '../simulator/list_sims.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/screenshot.ts: -------------------------------------------------------------------------------- ```typescript // Re-export from ui-testing to avoid duplication export { default } from '../ui-testing/screenshot.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/describe_ui.ts: -------------------------------------------------------------------------------- ```typescript // Re-export from ui-testing to avoid duplication export { default } from '../ui-testing/describe_ui.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/device/stop_device_log_cap.ts: -------------------------------------------------------------------------------- ```typescript // Re-export from logging to complete workflow export { default } from '../logging/stop_device_log_cap.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/macos/clean.ts: -------------------------------------------------------------------------------- ```typescript // Re-export unified clean tool for macos-project workflow export { default } from '../utilities/clean.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/device/clean.ts: -------------------------------------------------------------------------------- ```typescript // Re-export unified clean tool for device-project workflow export { default } from '../utilities/clean.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/device/start_device_log_cap.ts: -------------------------------------------------------------------------------- ```typescript // Re-export from logging to complete workflow export { default } from '../logging/start_device_log_cap.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/clean.ts: -------------------------------------------------------------------------------- ```typescript // Re-export unified clean tool for simulator-project workflow export { default } from '../utilities/clean.ts'; ``` -------------------------------------------------------------------------------- /src/utils/video-capture/index.ts: -------------------------------------------------------------------------------- ```typescript export { startSimulatorVideoCapture, stopSimulatorVideoCapture, type AxeHelpers, } from '../video_capture.ts'; ``` -------------------------------------------------------------------------------- /example_projects/iOS_Calculator/CalculatorApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- ```json { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ``` -------------------------------------------------------------------------------- /example_projects/iOS/MCPTest/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- ```json { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ``` -------------------------------------------------------------------------------- /example_projects/macOS/MCPTest/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- ```json { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ``` -------------------------------------------------------------------------------- /src/mcp/tools/device/discover_projs.ts: -------------------------------------------------------------------------------- ```typescript // Re-export from project-discovery to complete workflow export { default } from '../project-discovery/discover_projs.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/macos/discover_projs.ts: -------------------------------------------------------------------------------- ```typescript // Re-export from project-discovery to complete workflow export { default } from '../project-discovery/discover_projs.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/discover_projs.ts: -------------------------------------------------------------------------------- ```typescript // Re-export from project-discovery to complete workflow export { default } from '../project-discovery/discover_projs.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/macos/show_build_settings.ts: -------------------------------------------------------------------------------- ```typescript // Re-export unified tool for macos-project workflow export { default } from '../project-discovery/show_build_settings.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/device/show_build_settings.ts: -------------------------------------------------------------------------------- ```typescript // Re-export unified tool for device-project workflow export { default } from '../project-discovery/show_build_settings.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/device/get_app_bundle_id.ts: -------------------------------------------------------------------------------- ```typescript // Re-export from project-discovery to complete workflow export { default } from '../project-discovery/get_app_bundle_id.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/macos/get_mac_bundle_id.ts: -------------------------------------------------------------------------------- ```typescript // Re-export from project-discovery to complete workflow export { default } from '../project-discovery/get_mac_bundle_id.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/get_app_bundle_id.ts: -------------------------------------------------------------------------------- ```typescript // Re-export from project-discovery to complete workflow export { default } from '../project-discovery/get_app_bundle_id.ts'; ``` -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- ```json { "recommendations": [ "dbaeumer.vscode-eslint" ], "unwantedRecommendations": [ "esbenp.prettier-vscode" ] } ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/show_build_settings.ts: -------------------------------------------------------------------------------- ```typescript // Re-export unified tool for simulator-project workflow export { default } from '../project-discovery/show_build_settings.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/macos/list_schemes.ts: -------------------------------------------------------------------------------- ```typescript // Re-export unified list_schemes tool for macos-project workflow export { default } from '../project-discovery/list_schemes.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/device/list_schemes.ts: -------------------------------------------------------------------------------- ```typescript // Re-export unified list_schemes tool for device-project workflow export { default } from '../project-discovery/list_schemes.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/list_schemes.ts: -------------------------------------------------------------------------------- ```typescript // Re-export unified list_schemes tool for simulator-project workflow export { default } from '../project-discovery/list_schemes.ts'; ``` -------------------------------------------------------------------------------- /src/utils/plugin-registry/index.ts: -------------------------------------------------------------------------------- ```typescript export { loadWorkflowGroups, loadPlugins } from '../../core/plugin-registry.ts'; export { getEnabledWorkflows } from '../../core/dynamic-tools.ts'; ``` -------------------------------------------------------------------------------- /src/utils/logging/index.ts: -------------------------------------------------------------------------------- ```typescript /** * Focused logging facade. * Prefer importing from 'utils/logging/index.js' instead of the legacy utils barrel. */ export { log } from '../logger.ts'; ``` -------------------------------------------------------------------------------- /src/utils/axe/index.ts: -------------------------------------------------------------------------------- ```typescript export { createAxeNotAvailableResponse, getAxePath, getBundledAxeEnvironment, areAxeToolsAvailable, isAxeAtLeastVersion, } from '../axe-helpers.ts'; ``` -------------------------------------------------------------------------------- /src/utils/validation/index.ts: -------------------------------------------------------------------------------- ```typescript /** * Focused validation facade. * Prefer importing from 'utils/validation/index.js' instead of the legacy utils barrel. */ export * from '../validation.ts'; ``` -------------------------------------------------------------------------------- /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift: -------------------------------------------------------------------------------- ```swift import SwiftUI import CalculatorAppFeature @main struct CalculatorApp: App { var body: some Scene { WindowGroup { ContentView() } } } #Preview { ContentView() } ``` -------------------------------------------------------------------------------- /example_projects/iOS/MCPTest/MCPTestApp.swift: -------------------------------------------------------------------------------- ```swift // // MCPTestApp.swift // MCPTest // // Created by Cameron on 16/02/2025. // import SwiftUI @main struct MCPTestApp: App { var body: some Scene { WindowGroup { ContentView() } } } ``` -------------------------------------------------------------------------------- /example_projects/macOS/MCPTest/MCPTestApp.swift: -------------------------------------------------------------------------------- ```swift // // MCPTestApp.swift // MCPTest // // Created by Cameron on 16/02/2025. // import SwiftUI @main struct MCPTestApp: App { var body: some Scene { WindowGroup { ContentView() } } } ``` -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- ```json { "extends": "./tsconfig.json", "compilerOptions": { "types": ["vitest/globals", "node"], "allowJs": true, "noEmit": true }, "include": ["src/**/*.test.ts", "tests-vitest/**/*"], "exclude": ["node_modules", "dist"] } ``` -------------------------------------------------------------------------------- /build-plugins/tsconfig.json: -------------------------------------------------------------------------------- ```json { "extends": "../tsconfig.json", "compilerOptions": { "module": "ESNext", "target": "ES2022", "outDir": "../build-plugins-dist", "rootDir": ".", "allowSyntheticDefaultImports": true, "esModuleInterop": true }, "include": ["**/*.ts"], "exclude": ["node_modules", "dist"] } ``` -------------------------------------------------------------------------------- /src/mcp/tools/doctor/index.ts: -------------------------------------------------------------------------------- ```typescript export const workflow = { name: 'System Doctor', description: 'Debug tools and system doctor for troubleshooting XcodeBuildMCP server, development environment, and tool availability.', platforms: ['system'], capabilities: [ 'doctor', 'server-diagnostics', 'troubleshooting', 'system-analysis', 'environment-validation', ], }; ``` -------------------------------------------------------------------------------- /src/utils/responses/index.ts: -------------------------------------------------------------------------------- ```typescript /** * Focused responses facade. * Prefer importing from 'utils/responses/index.js' instead of the legacy utils barrel. */ export { createTextResponse } from '../validation.ts'; export { createErrorResponse, DependencyError, AxeError, SystemError, ValidationError, } from '../errors.ts'; // Types export type { ToolResponse } from '../../types/common.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/macos/index.ts: -------------------------------------------------------------------------------- ```typescript export const workflow = { name: 'macOS Development', description: 'Complete macOS development workflow for both .xcodeproj and .xcworkspace files. Build, test, deploy, and manage macOS applications.', platforms: ['macOS'], targets: ['native'], projectTypes: ['project', 'workspace'], capabilities: ['build', 'test', 'deploy', 'debug', 'app-management'], }; ``` -------------------------------------------------------------------------------- /src/mcp/tools/project-discovery/index.ts: -------------------------------------------------------------------------------- ```typescript export const workflow = { name: 'Project Discovery', description: 'Discover and examine Xcode projects, workspaces, and Swift packages. Analyze project structure, schemes, build settings, and bundle information.', platforms: ['iOS', 'macOS', 'watchOS', 'tvOS', 'visionOS'], capabilities: ['project-analysis', 'scheme-discovery', 'build-settings', 'bundle-inspection'], }; ``` -------------------------------------------------------------------------------- /example_projects/macOS/MCPTest/ContentView.swift: -------------------------------------------------------------------------------- ```swift // // ContentView.swift // MCPTest // // Created by Cameron on 16/02/2025. // import SwiftUI struct ContentView: View { var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) Text("Hello, world!") } .padding() } } #Preview { ContentView() } ``` -------------------------------------------------------------------------------- /src/utils/execution/index.ts: -------------------------------------------------------------------------------- ```typescript /** * Focused execution facade. * Prefer importing from 'utils/execution/index.js' instead of the legacy utils barrel. */ export { getDefaultCommandExecutor, getDefaultFileSystemExecutor } from '../command.ts'; // Types export type { CommandExecutor, CommandResponse, CommandExecOptions } from '../CommandExecutor.ts'; export type { FileSystemExecutor } from '../FileSystemExecutor.ts'; ``` -------------------------------------------------------------------------------- /src/mcp/tools/logging/index.ts: -------------------------------------------------------------------------------- ```typescript export const workflow = { name: 'Log Capture & Management', description: 'Log capture and management tools for iOS simulators and physical devices. Start, stop, and analyze application and system logs during development and testing.', platforms: ['iOS', 'watchOS', 'tvOS', 'visionOS'], targets: ['simulator', 'device'], capabilities: ['log-capture', 'log-analysis', 'debugging', 'monitoring'], }; ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/index.ts: -------------------------------------------------------------------------------- ```typescript export const workflow = { name: 'iOS Simulator Development', description: 'Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting simulators. Build, test, deploy, and interact with iOS apps on simulators.', platforms: ['iOS'], targets: ['simulator'], projectTypes: ['project', 'workspace'], capabilities: ['build', 'test', 'deploy', 'debug', 'ui-automation'], }; ``` -------------------------------------------------------------------------------- /src/mcp/tools/utilities/index.ts: -------------------------------------------------------------------------------- ```typescript export const workflow = { name: 'Project Utilities', description: 'Essential project maintenance utilities for cleaning and managing existing projects. Provides clean operations for both .xcodeproj and .xcworkspace files.', platforms: ['iOS', 'macOS'], targets: ['simulator', 'device', 'mac'], projectTypes: ['project', 'workspace'], capabilities: ['project-cleaning', 'project-maintenance'], }; ``` -------------------------------------------------------------------------------- /src/mcp/tools/ui-testing/index.ts: -------------------------------------------------------------------------------- ```typescript export const workflow = { name: 'UI Testing & Automation', description: 'UI automation and accessibility testing tools for iOS simulators. Perform gestures, interactions, screenshots, and UI analysis for automated testing workflows.', platforms: ['iOS'], targets: ['simulator'], capabilities: [ 'ui-automation', 'gesture-simulation', 'screenshot-capture', 'accessibility-testing', 'ui-analysis', ], }; ``` -------------------------------------------------------------------------------- /src/mcp/tools/session-management/session_show_defaults.ts: -------------------------------------------------------------------------------- ```typescript import { sessionStore } from '../../../utils/session-store.ts'; import type { ToolResponse } from '../../../types/common.ts'; export default { name: 'session-show-defaults', description: 'Show current session defaults.', schema: {}, handler: async (): Promise<ToolResponse> => { const current = sessionStore.getAll(); return { content: [{ type: 'text', text: JSON.stringify(current, null, 2) }], isError: false }; }, }; ``` -------------------------------------------------------------------------------- /src/mcp/tools/swift-package/index.ts: -------------------------------------------------------------------------------- ```typescript export const workflow = { name: 'Swift Package Manager', description: 'Swift Package Manager operations for building, testing, running, and managing Swift packages and dependencies. Complete SPM workflow support.', platforms: ['iOS', 'macOS', 'watchOS', 'tvOS', 'visionOS', 'Linux'], targets: ['package'], projectTypes: ['swift-package'], capabilities: ['build', 'test', 'run', 'clean', 'dependency-management', 'package-management'], }; ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml # Smithery configuration file: https://smithery.ai/docs/build/project-config startCommand: type: stdio configSchema: # JSON Schema defining the configuration options for the MCP. type: object description: Configuration for XcodeBuildMCP (no options needed) commandFunction: # A JS function that produces the CLI command based on the given config to start the MCP on stdio. |- (config) => ({ command: 'xcodebuildmcp', args: [], env: {} }) exampleConfig: {} ``` -------------------------------------------------------------------------------- /src/mcp/tools/session-management/index.ts: -------------------------------------------------------------------------------- ```typescript export const workflow = { name: 'session-management', description: 'Manage session defaults for projectPath/workspacePath, scheme, configuration, simulatorName/simulatorId, deviceId, useLatestOS and arch. These defaults are required by many tools and must be set before attempting to call tools that would depend on these values.', platforms: ['iOS', 'macOS', 'tvOS', 'watchOS', 'visionOS'], targets: ['simulator', 'device'], capabilities: ['configuration', 'state-management'], }; ``` -------------------------------------------------------------------------------- /src/mcp/tools/device/index.ts: -------------------------------------------------------------------------------- ```typescript export const workflow = { name: 'iOS Device Development', description: 'Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting physical devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). Build, test, deploy, and debug apps on real hardware.', platforms: ['iOS', 'watchOS', 'tvOS', 'visionOS'], targets: ['device'], projectTypes: ['project', 'workspace'], capabilities: ['build', 'test', 'deploy', 'debug', 'log-capture', 'device-management'], }; ``` -------------------------------------------------------------------------------- /src/mcp/tools/discovery/index.ts: -------------------------------------------------------------------------------- ```typescript /** * Discovery Workflow * * Dynamic tool discovery and workflow recommendation system */ export const workflow = { name: 'Dynamic Tool Discovery', description: 'Intelligent discovery and recommendation of appropriate development workflows based on project structure and requirements', platforms: ['iOS', 'macOS', 'watchOS', 'tvOS', 'visionOS'], targets: ['simulator', 'device'], projectTypes: ['project', 'workspace', 'package'], capabilities: ['discovery', 'recommendation', 'workflow-analysis'], }; ``` -------------------------------------------------------------------------------- /src/utils/CommandExecutor.ts: -------------------------------------------------------------------------------- ```typescript import { ChildProcess } from 'child_process'; export interface CommandExecOptions { env?: Record<string, string>; cwd?: string; } /** * Command executor function type for dependency injection */ export type CommandExecutor = ( command: string[], logPrefix?: string, useShell?: boolean, opts?: CommandExecOptions, detached?: boolean, ) => Promise<CommandResponse>; /** * Command execution response interface */ export interface CommandResponse { success: boolean; output: string; error?: string; process: ChildProcess; exitCode?: number; } ``` -------------------------------------------------------------------------------- /example_projects/iOS_Calculator/CalculatorApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- ```json { "images" : [ { "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ``` -------------------------------------------------------------------------------- /example_projects/iOS/MCPTest/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- ```json { "images" : [ { "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ``` -------------------------------------------------------------------------------- /src/mcp/tools/project-scaffolding/index.ts: -------------------------------------------------------------------------------- ```typescript /** * Project Scaffolding workflow * * Provides tools for creating new iOS and macOS projects from templates. * These tools are used at project inception to bootstrap new applications * with best practices and standard configurations. */ export const workflow = { name: 'Project Scaffolding', description: 'Tools for creating new iOS and macOS projects from templates. Bootstrap new applications with best practices, standard configurations, and modern project structures.', platforms: ['iOS', 'macOS'], targets: ['simulator', 'device', 'mac'], projectTypes: ['project'], capabilities: ['project-creation', 'template-generation', 'project-initialization'], }; ``` -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- ```yaml name: CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build-and-test: runs-on: ubuntu-latest strategy: matrix: node-version: [24.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} cache: 'npm' - name: Install dependencies run: npm ci - name: Build run: npm run build - name: Lint run: npm run lint - name: Check formatting run: npm run format:check - name: Type check run: npm run typecheck - name: Run tests run: npm test - run: npx pkg-pr-new publish ``` -------------------------------------------------------------------------------- /src/utils/FileSystemExecutor.ts: -------------------------------------------------------------------------------- ```typescript /** * File system executor interface for dependency injection */ export interface FileSystemExecutor { mkdir(path: string, options?: { recursive?: boolean }): Promise<void>; readFile(path: string, encoding?: BufferEncoding): Promise<string>; writeFile(path: string, content: string, encoding?: BufferEncoding): Promise<void>; cp(source: string, destination: string, options?: { recursive?: boolean }): Promise<void>; readdir(path: string, options?: { withFileTypes?: boolean }): Promise<unknown[]>; rm(path: string, options?: { recursive?: boolean; force?: boolean }): Promise<void>; existsSync(path: string): boolean; stat(path: string): Promise<{ isDirectory(): boolean }>; mkdtemp(prefix: string): Promise<string>; tmpdir(): string; } ``` -------------------------------------------------------------------------------- /src/utils/schema-helpers.ts: -------------------------------------------------------------------------------- ```typescript /** * Schema Helper Utilities * * Shared utility functions for schema validation and preprocessing. */ /** * Convert empty strings to undefined in an object (shallow transformation) * Used for preprocessing Zod schemas with optional fields * * @param value - The value to process * @returns The processed value with empty strings converted to undefined */ export function nullifyEmptyStrings(value: unknown): unknown { if (value && typeof value === 'object' && !Array.isArray(value)) { const copy: Record<string, unknown> = { ...(value as Record<string, unknown>) }; for (const key of Object.keys(copy)) { const v = copy[key]; if (typeof v === 'string' && v.trim() === '') copy[key] = undefined; } return copy; } return value; } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./build", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "sourceMap": true, "inlineSources": true, // Set `sourceRoot` to "/" to strip the build path prefix // from generated source code references. // This improves issue grouping in Sentry. "sourceRoot": "/", "allowImportingTsExtensions": true, "noEmit": true }, "include": ["src/**/*"], "exclude": [ "node_modules", "**/*.test.ts", "tests-vitest/**/*", "plugins/**/*", "src/core/generated-plugins.ts", "src/core/generated-resources.ts" ] } ``` -------------------------------------------------------------------------------- /src/core/plugin-types.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { ToolResponse } from '../types/common.ts'; export interface PluginMeta { readonly name: string; // Verb used by MCP readonly schema: Record<string, z.ZodTypeAny>; // Zod validation schema (object schema) readonly description?: string; // One-liner shown in help handler(params: Record<string, unknown>): Promise<ToolResponse>; } export interface WorkflowMeta { readonly name: string; readonly description: string; readonly platforms?: string[]; readonly targets?: string[]; readonly projectTypes?: string[]; readonly capabilities?: string[]; } export interface WorkflowGroup { readonly workflow: WorkflowMeta; readonly tools: PluginMeta[]; readonly directoryName: string; } export const defineTool = (meta: PluginMeta): PluginMeta => meta; ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator-management/index.ts: -------------------------------------------------------------------------------- ```typescript /** * Simulator Management workflow * * Provides tools for working with simulators like booting and opening simulators, launching apps, * listing sims, stopping apps, erasing simulator content and settings, and setting sim environment * options like location, network, statusbar and appearance. */ export const workflow = { name: 'Simulator Management', description: 'Tools for managing simulators from booting, opening simulators, listing simulators, stopping simulators, erasing simulator content and settings, and setting simulator environment options like location, network, statusbar and appearance.', platforms: ['iOS'], targets: ['simulator'], projectTypes: ['project', 'workspace'], capabilities: ['boot', 'open', 'list', 'appearance', 'location', 'network', 'statusbar', 'erase'], }; ``` -------------------------------------------------------------------------------- /example_projects/macOS/MCPTest/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- ```json { "images" : [ { "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ``` -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- ```typescript import { defineConfig } from 'tsup'; import { chmodSync, existsSync } from 'fs'; import { createPluginDiscoveryPlugin } from './build-plugins/plugin-discovery.js'; export default defineConfig({ entry: { index: 'src/index.ts', 'doctor-cli': 'src/doctor-cli.ts', }, format: ['esm'], target: 'node18', platform: 'node', outDir: 'build', clean: true, sourcemap: true, // Enable source maps for debugging dts: { entry: { index: 'src/index.ts', }, }, splitting: false, shims: false, treeshake: true, minify: false, esbuildPlugins: [createPluginDiscoveryPlugin()], onSuccess: async () => { console.log('✅ Build complete!'); // Set executable permissions for built files if (existsSync('build/index.js')) { chmodSync('build/index.js', '755'); } if (existsSync('build/doctor-cli.js')) { chmodSync('build/doctor-cli.js', '755'); } }, }); ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile # Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config # Use a small base image with Node.js LTS FROM node:lts-alpine AS build # Install dependencies needed for building RUN apk add --no-cache python3 g++ make git # Create app directory WORKDIR /usr/src/app # Copy package manifests COPY package.json package-lock.json tsconfig.json eslint.config.js ./ # Copy source COPY src ./src # Install dependencies ignoring any prepare scripts, then build RUN npm ci --ignore-scripts RUN npm run prebuild && npm run build # Stage for runtime FROM node:lts-alpine WORKDIR /usr/src/app # Install minimal runtime dependencies # No build tools needed, install production deps COPY package.json package-lock.json ./ RUN npm ci --omit=dev --ignore-scripts # Copy built files COPY --from=build /usr/src/app/build ./build # Symlink binary RUN npm link # Default command ENTRYPOINT ["xcodebuildmcp"] CMD [] ``` -------------------------------------------------------------------------------- /src/mcp/tools/swift-package/active-processes.ts: -------------------------------------------------------------------------------- ```typescript /** * Shared process state management for Swift Package tools * This module provides a centralized way to manage active processes * between swift_package_run and swift_package_stop tools */ export interface ProcessInfo { process: { kill: (signal?: string) => void; on: (event: string, callback: () => void) => void; pid?: number; }; startedAt: Date; } // Global map to track active processes export const activeProcesses = new Map<number, ProcessInfo>(); // Helper functions for process management export const getProcess = (pid: number): ProcessInfo | undefined => { return activeProcesses.get(pid); }; export const addProcess = (pid: number, processInfo: ProcessInfo): void => { activeProcesses.set(pid, processInfo); }; export const removeProcess = (pid: number): boolean => { return activeProcesses.delete(pid); }; export const clearAllProcesses = (): void => { activeProcesses.clear(); }; ``` -------------------------------------------------------------------------------- /.github/workflows/sentry.yml: -------------------------------------------------------------------------------- ```yaml name: Sentry Release on: push: tags: - 'v*' jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install dependencies run: npm ci - name: Build project run: npm run build - name: Run tests run: npm test - name: Extract version from build/version.js id: get_version run: echo "MCP_VERSION=$(grep -oE "'[0-9]+\.[0-9]+\.[0-9]+'" build/version.js | tr -d "'")" >> $GITHUB_OUTPUT - name: Create Sentry release uses: getsentry/action-release@v3 env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} with: environment: production sourcemaps: "./build" version: ${{ steps.get_version.outputs.MCP_VERSION }} ``` -------------------------------------------------------------------------------- /example_projects/iOS_Calculator/CalculatorAppPackage/Package.swift: -------------------------------------------------------------------------------- ```swift // swift-tools-version: 5.10 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "CalculatorAppFeature", platforms: [.iOS(.v17)], products: [ // Products define the executables and libraries a package produces, making them visible to other packages. .library( name: "CalculatorAppFeature", targets: ["CalculatorAppFeature"] ), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .target( name: "CalculatorAppFeature" ), .testTarget( name: "CalculatorAppFeatureTests", dependencies: [ "CalculatorAppFeature" ] ), ] ) ``` -------------------------------------------------------------------------------- /example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/BackgroundEffect.swift: -------------------------------------------------------------------------------- ```swift import SwiftUI // MARK: - Background State Management enum BackgroundState { case normal, calculated, error var colors: [Color] { switch self { case .normal: return [Color.blue.opacity(0.8), Color.purple.opacity(0.8), Color.indigo.opacity(0.9)] case .calculated: return [Color.green.opacity(0.7), Color.mint.opacity(0.8), Color.teal.opacity(0.9)] case .error: return [Color.red.opacity(0.7), Color.pink.opacity(0.8), Color.orange.opacity(0.9)] } } } // MARK: - Animated Background Component struct AnimatedBackground: View { let backgroundGradient: BackgroundState var body: some View { AngularGradient( colors: backgroundGradient.colors, center: .topLeading, angle: .degrees(45) ) .ignoresSafeArea() .animation(.easeInOut(duration: 0.8), value: backgroundGradient) } } ``` -------------------------------------------------------------------------------- /example_projects/spm/Tests/TestLibTests/SimpleTests.swift: -------------------------------------------------------------------------------- ```swift import Testing @Test("Basic truth assertions") func basicTruthTest() { #expect(true == true) #expect(false == false) #expect(true != false) } @Test("Basic math operations") func basicMathTest() { #expect(2 + 2 == 4) #expect(5 - 3 == 2) #expect(3 * 4 == 12) #expect(10 / 2 == 5) } @Test("String operations") func stringTest() { let greeting = "Hello" let world = "World" #expect(greeting + " " + world == "Hello World") #expect(greeting.count == 5) #expect(world.isEmpty == false) } @Test("Array operations") func arrayTest() { let numbers = [1, 2, 3, 4, 5] #expect(numbers.count == 5) #expect(numbers.first == 1) #expect(numbers.last == 5) #expect(numbers.contains(3) == true) } @Test("Optional handling") func optionalTest() { let someValue: Int? = 42 let nilValue: Int? = nil #expect(someValue != nil) #expect(nilValue == nil) #expect(someValue! == 42) } ``` -------------------------------------------------------------------------------- /example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/CalculatorInputHandler.swift: -------------------------------------------------------------------------------- ```swift import Foundation // MARK: - Input Handling /// Handles input parsing and routing to the calculator service struct CalculatorInputHandler { private let service: CalculatorService init(service: CalculatorService) { self.service = service } func handleInput(_ input: String) { switch input { case "C": service.clear() case "±": service.toggleSign() case "%": service.percentage() case "+", "-", "×", "÷": if let operation = CalculatorService.Operation(rawValue: input) { service.setOperation(operation) } case "=": service.calculate() case ".": service.inputDecimal() case "0"..."9": service.inputNumber(input) default: break // Ignore unknown inputs } } func deleteLastDigit() { service.deleteLastDigit() } } ``` -------------------------------------------------------------------------------- /example_projects/spm/Sources/quick-task/main.swift: -------------------------------------------------------------------------------- ```swift import Foundation import TestLib import ArgumentParser @main struct QuickTask: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "quick-task", abstract: "A quick task that finishes within 5 seconds", version: "1.0.0" ) @Option(name: .shortAndLong, help: "Number of seconds to work (default: 3)") var duration: Int = 3 @Flag(name: .shortAndLong, help: "Enable verbose output") var verbose: Bool = false @Option(name: .shortAndLong, help: "Task name to display") var taskName: String = "DefaultTask" func run() async throws { let taskManager = TaskManager() if verbose { print("🚀 Starting quick task: \(taskName)") print("⏱️ Duration: \(duration) seconds") } await taskManager.executeQuickTask(name: taskName, duration: duration, verbose: verbose) if verbose { print("✅ Quick task completed successfully!") } } } ``` -------------------------------------------------------------------------------- /example_projects/spm/Package.swift: -------------------------------------------------------------------------------- ```swift // swift-tools-version: 6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "spm", platforms: [ .macOS(.v15), ], dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.1"), ], targets: [ .executableTarget( name: "spm" ), .executableTarget( name: "quick-task", dependencies: [ "TestLib", .product(name: "ArgumentParser", package: "swift-argument-parser"), ] ), .executableTarget( name: "long-server", dependencies: [ "TestLib", .product(name: "ArgumentParser", package: "swift-argument-parser"), ] ), .target( name: "TestLib" ), .testTarget( name: "TestLibTests", dependencies: ["TestLib"] ), ] ) ``` -------------------------------------------------------------------------------- /example_projects/iOS/MCPTest/ContentView.swift: -------------------------------------------------------------------------------- ```swift // // ContentView.swift // MCPTest // // Created by Cameron on 16/02/2025. // import SwiftUI import OSLog struct ContentView: View { @State private var text: String = "" var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) TextField("Enter text", text: $text) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding(.horizontal) Text(text) Button("Log something") { let message = ProcessInfo.processInfo.environment.map { "\($0.key): \($0.value)" }.joined(separator: "\n") Logger.myApp.debug("Environment: \(message)") debugPrint("Button was pressed.") text = "You just pressed the button!" } } .padding() } } #Preview { ContentView() } // OS Log Extension extension Logger { static let myApp = Logger( subsystem: "com.cameroncooke.MCPTest", category: "default" ) } ``` -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- ```typescript import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { environment: 'node', globals: true, include: [ 'src/**/__tests__/**/*.test.ts' // Only __tests__ directories ], exclude: [ 'node_modules/**', 'build/**', 'coverage/**', 'bundled/**', 'example_projects/**', '.git/**', '**/*.d.ts', '**/temp_*', '**/full-output.txt', '**/experiments/**', '**/__pycache__/**', '**/dist/**' ], pool: 'threads', poolOptions: { threads: { maxThreads: 4 } }, env: { NODE_OPTIONS: '--max-old-space-size=4096' }, testTimeout: 30000, hookTimeout: 10000, teardownTimeout: 5000, coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], exclude: [ 'node_modules/**', 'build/**', 'tests/**', 'example_projects/**', '**/*.config.*', '**/*.d.ts' ] } }, resolve: { alias: { // Handle .js imports in TypeScript files '^(\\.{1,2}/.*)\\.js$': '$1' } } }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/session-management/session_clear_defaults.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { sessionStore } from '../../../utils/session-store.ts'; import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; import type { ToolResponse } from '../../../types/common.ts'; const keys = [ 'projectPath', 'workspacePath', 'scheme', 'configuration', 'simulatorName', 'simulatorId', 'deviceId', 'useLatestOS', 'arch', ] as const; const schemaObj = z.object({ keys: z.array(z.enum(keys)).optional(), all: z.boolean().optional(), }); type Params = z.infer<typeof schemaObj>; export async function sessionClearDefaultsLogic(params: Params): Promise<ToolResponse> { if (params.all || !params.keys) sessionStore.clear(); else sessionStore.clear(params.keys); return { content: [{ type: 'text', text: 'Session defaults cleared' }], isError: false }; } export default { name: 'session-clear-defaults', description: 'Clear selected or all session defaults.', schema: schemaObj.shape, handler: createTypedTool(schemaObj, sessionClearDefaultsLogic, getDefaultCommandExecutor), }; ``` -------------------------------------------------------------------------------- /.vscode/mcp.json: -------------------------------------------------------------------------------- ```json { "servers": { "XcodeBuildMCP": { "type": "stdio", "command": "npx", "args": [ "-y", "xcodebuildmcp@latest" ], "env": { "XCODEBUILDMCP_DEBUG": "true", "XCODEBUILDMCP_DYNAMIC_TOOLS": "true", "INCREMENTAL_BUILDS_ENABLED": "false", "XCODEBUILDMCP_IOS_TEMPLATE_PATH": "/Volumes/Developer/XcodeBuildMCP-iOS-Template", "XCODEBUILDMCP_MACOS_TEMPLATE_PATH": "/Volumes/Developer/XcodeBuildMCP-macOS-Template" } }, "XcodeBuildMCP-Dev": { "type": "stdio", "command": "node", "args": [ "--inspect-brk=9999", "--trace-warnings", "/Users/cameroncooke/Developer/XcodeBuildMCP/build/index.js" ], "env": { "XCODEBUILDMCP_DEBUG": "true", "XCODEBUILDMCP_DYNAMIC_TOOLS": "true", "INCREMENTAL_BUILDS_ENABLED": "false", "XCODEBUILDMCP_IOS_TEMPLATE_PATH": "/Volumes/Developer/XcodeBuildMCP-iOS-Template", "XCODEBUILDMCP_MACOS_TEMPLATE_PATH": "/Volumes/Developer/XcodeBuildMCP-macOS-Template" } }, } } ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- ```yaml name: Feature Request description: Suggest a new feature for XcodeBuildMCP title: "[Feature]: " labels: ["enhancement"] body: - type: markdown attributes: value: | Thanks for suggesting a new feature for XcodeBuildMCP! - type: textarea id: feature-description attributes: label: Feature Description description: Describe the new capability you'd like to add to XcodeBuildMCP placeholder: I would like the AI assistant to be able to... validations: required: true - type: textarea id: use-cases attributes: label: Use Cases description: Describe specific scenarios where this feature would be useful placeholder: | - Building and testing iOS apps with custom schemes - Managing multiple simulator configurations - Automating complex Xcode workflows validations: required: false - type: textarea id: example-interactions attributes: label: Example Interactions description: Provide examples of how you envision using this feature placeholder: | You: [Example request to the AI] AI: [Desired response/action] validations: required: false ``` -------------------------------------------------------------------------------- /src/utils/session-store.ts: -------------------------------------------------------------------------------- ```typescript import { log } from './logger.ts'; export type SessionDefaults = { projectPath?: string; workspacePath?: string; scheme?: string; configuration?: string; simulatorName?: string; simulatorId?: string; deviceId?: string; useLatestOS?: boolean; arch?: 'arm64' | 'x86_64'; }; class SessionStore { private defaults: SessionDefaults = {}; setDefaults(partial: Partial<SessionDefaults>): void { this.defaults = { ...this.defaults, ...partial }; log('info', `[Session] Defaults updated: ${Object.keys(partial).join(', ')}`); } clear(keys?: (keyof SessionDefaults)[]): void { if (keys == null) { this.defaults = {}; log('info', '[Session] All defaults cleared'); return; } if (keys.length === 0) { // No-op when an empty array is provided (e.g., empty UI selection) log('info', '[Session] No keys provided to clear; no changes made'); return; } for (const k of keys) delete this.defaults[k]; log('info', `[Session] Defaults cleared: ${keys.join(', ')}`); } get<K extends keyof SessionDefaults>(key: K): SessionDefaults[K] { return this.defaults[key]; } getAll(): SessionDefaults { return { ...this.defaults }; } } export const sessionStore = new SessionStore(); ``` -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- ```json { "eslint.useFlatConfig": true, "eslint.validate": [ "javascript", "typescript" ], "eslint.runtime": "/opt/homebrew/bin/node", "eslint.format.enable": true, "eslint.nodePath": "${workspaceFolder}/node_modules", "eslint.workingDirectories": [ { "directory": "${workspaceFolder}", "changeProcessCWD": true } ], "typescript.tsdk": "node_modules/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true, "typescript.tsserver.maxTsServerMemory": 4096, "javascript.validate.enable": false, "typescript.validate.enable": false, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, "editor.defaultFormatter": "vscode.typescript-language-features", "[typescript]": { "editor.defaultFormatter": "vscode.typescript-language-features" }, "[javascript]": { "editor.defaultFormatter": "vscode.typescript-language-features" }, "terminal.integrated.shellIntegration.decorationsEnabled": "never", "vitest.nodeExecutable": "/opt/homebrew/bin/node", "[json]": { "editor.defaultFormatter": "vscode.json-language-features" }, "[jsonc]": { "editor.defaultFormatter": "vscode.json-language-features" }, "chat.mcp.serverSampling": { "XcodeBuildMCP/.vscode/mcp.json: XcodeBuildMCP-Dev": { "allowedDuringChat": true } }, } ``` -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- ```json { "version": "0.2.0", "configurations": [ { "type": "node", "request": "attach", "name": "Wait for MCP Server to Start", "port": 9999, "address": "localhost", "restart": true, "skipFiles": [ "<node_internals>/**" ], "sourceMaps": true, "outFiles": [ "${workspaceFolder}/build/**/*.js" ], "cwd": "${workspaceFolder}", "sourceMapPathOverrides": { "/*": "${workspaceFolder}/src/*" }, "timeout": 60000, "localRoot": "${workspaceFolder}", "remoteRoot": "${workspaceFolder}" }, { "type": "node", "request": "launch", "name": "Launch MCP Server Dev", "program": "${workspaceFolder}/build/index.js", "cwd": "${workspaceFolder}", "runtimeArgs": [ "--inspect=9999" ], "env": { "XCODEBUILDMCP_DEBUG": "true", "XCODEBUILDMCP_DYNAMIC_TOOLS": "true", "INCREMENTAL_BUILDS_ENABLED": "false", "XCODEBUILDMCP_IOS_TEMPLATE_PATH": "/Volumes/Developer/XcodeBuildMCP-iOS-Template", "XCODEBUILDMCP_MACOS_TEMPLATE_PATH": "/Volumes/Developer/XcodeBuildMCP-macOS-Template" }, "sourceMaps": true, "outFiles": [ "${workspaceFolder}/build/**/*.js" ], "skipFiles": [ "<node_internals>/**" ], "sourceMapPathOverrides": { "/*": "${workspaceFolder}/src/*" } } ] } ``` -------------------------------------------------------------------------------- /.github/workflows/claude-dispatch.yml: -------------------------------------------------------------------------------- ```yaml # IMPORTANT: Do not move this file in your repo! Make sure it's located at .github/workflows/claude-dispatch.yml name: Claude Code Dispatch # IMPORTANT: Do not modify this `on` section! on: repository_dispatch: types: [claude-dispatch] jobs: claude-dispatch: runs-on: ubuntu-latest permissions: contents: write pull-requests: write issues: write id-token: write steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 1 # - name: Preliminary Setup # run: | # echo "Setting up environment..." # Add any preliminary setup commands here to setup Claude's dev environment # e.g., npm install, etc. - name: Run Claude Code id: claude uses: anthropics/claude-code-action@eap with: mode: 'remote-agent' # Optional: Specify an API key, otherwise we'll use your Claude account automatically # anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) # model: "claude-opus-4-20250514" # Optional: Allow Claude to run specific commands # allowed_tools: | # Bash(npm run lint) # Bash(npm run test) # Bash(npm run build) # Optional: Custom environment variables for Claude # claude_env: | # NODE_ENV: test ``` -------------------------------------------------------------------------------- /src/mcp/tools/session-management/__tests__/session_show_defaults.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { sessionStore } from '../../../../utils/session-store.ts'; import plugin from '../session_show_defaults.ts'; describe('session-show-defaults tool', () => { beforeEach(() => { sessionStore.clear(); }); afterEach(() => { sessionStore.clear(); }); describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(plugin.name).toBe('session-show-defaults'); }); it('should have correct description', () => { expect(plugin.description).toBe('Show current session defaults.'); }); it('should have handler function', () => { expect(typeof plugin.handler).toBe('function'); }); it('should have empty schema', () => { expect(plugin.schema).toEqual({}); }); }); describe('Handler Behavior', () => { it('should return empty defaults when none set', async () => { const result = await plugin.handler({}); expect(result.isError).toBe(false); const parsed = JSON.parse(result.content[0].text); expect(parsed).toEqual({}); }); it('should return current defaults when set', async () => { sessionStore.setDefaults({ scheme: 'MyScheme', simulatorId: 'SIM-123' }); const result = await plugin.handler({}); expect(result.isError).toBe(false); const parsed = JSON.parse(result.content[0].text); expect(parsed.scheme).toBe('MyScheme'); expect(parsed.simulatorId).toBe('SIM-123'); }); }); }); ``` -------------------------------------------------------------------------------- /src/utils/__tests__/session-store.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect, beforeEach } from 'vitest'; import { sessionStore } from '../session-store.ts'; describe('SessionStore', () => { beforeEach(() => { sessionStore.clear(); }); it('should set and get defaults', () => { sessionStore.setDefaults({ scheme: 'App', useLatestOS: true }); expect(sessionStore.get('scheme')).toBe('App'); expect(sessionStore.get('useLatestOS')).toBe(true); }); it('should merge defaults on set', () => { sessionStore.setDefaults({ scheme: 'App' }); sessionStore.setDefaults({ simulatorName: 'iPhone 16' }); const all = sessionStore.getAll(); expect(all.scheme).toBe('App'); expect(all.simulatorName).toBe('iPhone 16'); }); it('should clear specific keys', () => { sessionStore.setDefaults({ scheme: 'App', simulatorId: 'SIM-1', deviceId: 'DEV-1' }); sessionStore.clear(['simulatorId']); const all = sessionStore.getAll(); expect(all.scheme).toBe('App'); expect(all.simulatorId).toBeUndefined(); expect(all.deviceId).toBe('DEV-1'); }); it('should clear all when no keys provided', () => { sessionStore.setDefaults({ scheme: 'App', simulatorId: 'SIM-1' }); sessionStore.clear(); const all = sessionStore.getAll(); expect(Object.keys(all).length).toBe(0); }); it('should be a no-op when empty keys array provided', () => { sessionStore.setDefaults({ scheme: 'App', simulatorId: 'SIM-1' }); sessionStore.clear([]); const all = sessionStore.getAll(); expect(all.scheme).toBe('App'); expect(all.simulatorId).toBe('SIM-1'); }); }); ``` -------------------------------------------------------------------------------- /example_projects/spm/Sources/long-server/main.swift: -------------------------------------------------------------------------------- ```swift import Foundation import TestLib import ArgumentParser @main struct LongServer: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "long-server", abstract: "A long-running server that runs indefinitely until stopped" ) @Option(name: .shortAndLong, help: "Port to listen on (default: 8080)") var port: Int = 8080 @Flag(name: .shortAndLong, help: "Enable verbose logging") var verbose: Bool = false @Option(name: .shortAndLong, help: "Auto-shutdown after N seconds (0 = run forever)") var autoShutdown: Int = 0 func run() async throws { let taskManager = TaskManager() if verbose { print("🚀 Starting long-running server...") print("🌐 Port: \(port)") if autoShutdown > 0 { print("⏰ Auto-shutdown: \(autoShutdown) seconds") } else { print("♾️ Running indefinitely (use SIGTERM to stop)") } } // Set up signal handling for graceful shutdown let signalSource = DispatchSource.makeSignalSource(signal: SIGTERM, queue: .main) signalSource.setEventHandler { if verbose { print("\n🛑 Received SIGTERM, shutting down gracefully...") } taskManager.stopServer() } signalSource.resume() signal(SIGTERM, SIG_IGN) await taskManager.startLongRunningServer( port: port, verbose: verbose, autoShutdown: autoShutdown ) } } ``` -------------------------------------------------------------------------------- /example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/CalculatorDisplay.swift: -------------------------------------------------------------------------------- ```swift import SwiftUI // MARK: - Calculator Display Component struct CalculatorDisplay: View { let expressionDisplay: String let display: String var onDeleteLastDigit: (() -> Void)? = nil var body: some View { VStack(alignment: .trailing, spacing: 8) { // Expression display (smaller, secondary) Text(expressionDisplay) .font(.title2) .foregroundColor(.white.opacity(0.7)) .frame(maxWidth: .infinity, alignment: .trailing) .lineLimit(1) .minimumScaleFactor(0.5) // Main result display Text(display) .font(.system(size: 56, weight: .light, design: .rounded)) .foregroundColor(.white) .frame(maxWidth: .infinity, alignment: .trailing) .lineLimit(1) .minimumScaleFactor(0.3) .gesture(DragGesture(minimumDistance: 20, coordinateSpace: .local) .onEnded { value in if value.translation.width < -20 || value.translation.width > 20 { onDeleteLastDigit?() } } ) } .padding(.horizontal, 24) .padding(.bottom, 30) .frame(height: 140) } } struct CalculatorDisplay_Previews: PreviewProvider { static var previews: some View { CalculatorDisplay(expressionDisplay: "12 + 7", display: "19", onDeleteLastDigit: nil) .background(Color.black) .previewLayout(.sizeThatFits) } } ``` -------------------------------------------------------------------------------- /src/mcp/tools/logging/stop_sim_log_cap.ts: -------------------------------------------------------------------------------- ```typescript /** * Logging Plugin: Stop Simulator Log Capture * * Stops an active simulator log capture session and returns the captured logs. */ import { z } from 'zod'; import { stopLogCapture as _stopLogCapture } from '../../../utils/log-capture/index.ts'; import { ToolResponse, createTextContent } from '../../../types/common.ts'; import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; import { getDefaultCommandExecutor } from '../../../utils/command.ts'; // Define schema as ZodObject const stopSimLogCapSchema = z.object({ logSessionId: z.string().describe('The session ID returned by start_sim_log_cap.'), }); // Use z.infer for type safety type StopSimLogCapParams = z.infer<typeof stopSimLogCapSchema>; /** * Business logic for stopping simulator log capture session */ export async function stop_sim_log_capLogic(params: StopSimLogCapParams): Promise<ToolResponse> { const { logContent, error } = await _stopLogCapture(params.logSessionId); if (error) { return { content: [ createTextContent(`Error stopping log capture session ${params.logSessionId}: ${error}`), ], isError: true, }; } return { content: [ createTextContent( `Log capture session ${params.logSessionId} stopped successfully. Log content follows:\n\n${logContent}`, ), ], }; } export default { name: 'stop_sim_log_cap', description: 'Stops an active simulator log capture session and returns the captured logs.', schema: stopSimLogCapSchema.shape, // MCP SDK compatibility handler: createTypedTool(stopSimLogCapSchema, stop_sim_log_capLogic, getDefaultCommandExecutor), }; ``` -------------------------------------------------------------------------------- /src/doctor-cli.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node /** * XcodeBuildMCP Doctor CLI * * This standalone script runs the doctor tool and outputs the results * to the console. It's designed to be run directly via npx or mise. */ import { version } from './version.ts'; import { doctorLogic } from './mcp/tools/doctor/doctor.ts'; import { getDefaultCommandExecutor } from './utils/execution/index.ts'; async function runDoctor(): Promise<void> { try { // Using console.error to avoid linting issues as it's allowed by the project's linting rules console.error(`Running XcodeBuildMCP Doctor (v${version})...`); console.error('Collecting system information and checking dependencies...\n'); // Run the doctor tool logic directly with CLI flag enabled const executor = getDefaultCommandExecutor(); const result = await doctorLogic({}, executor, true); // showAsciiLogo = true for CLI // Output the doctor information if (result.content && result.content.length > 0) { const textContent = result.content.find((item) => item.type === 'text'); if (textContent && textContent.type === 'text') { // eslint-disable-next-line no-console console.log(textContent.text); } else { console.error('Error: Unexpected doctor result format'); } } else { console.error('Error: No doctor information returned'); } console.error('\nDoctor run complete. Please include this output when reporting issues.'); } catch (error) { console.error('Error running doctor:', error); process.exit(1); } } // Run the doctor runDoctor().catch((error) => { console.error('Unhandled exception:', error); process.exit(1); }); ``` -------------------------------------------------------------------------------- /example_projects/iOS/MCPTestUITests/MCPTestUITests.swift: -------------------------------------------------------------------------------- ```swift import XCTest /// Reproduction tests for TEST_RUNNER_ environment variable passthrough. /// GitHub Issue: https://github.com/cameroncooke/XcodeBuildMCP/issues/101 /// /// Expected behavior: /// - When invoking xcodebuild test with TEST_RUNNER_USE_DEV_MODE=YES, /// the test runner environment should contain USE_DEV_MODE=YES /// (the TEST_RUNNER_ prefix is stripped by xcodebuild). /// /// Current behavior (before implementation in Node layer): /// - Running via XcodeBuildMCP test tools does not yet pass TEST_RUNNER_ /// variables through, so this test will fail and serve as a repro. final class MCPTestUITests: XCTestCase { override func setUpWithError() throws { continueAfterFailure = false } /// Verifies that USE_DEV_MODE=YES is present in the test runner environment. /// This proves TEST_RUNNER_USE_DEV_MODE=YES was passed to xcodebuild. func testEnvironmentVariablePassthrough() throws { let env = ProcessInfo.processInfo.environment let value = env["USE_DEV_MODE"] ?? "<nil>" XCTAssertEqual( value, "YES", "Expected USE_DEV_MODE=YES via TEST_RUNNER_USE_DEV_MODE. Actual: \(value)" ) } /// Example of how a project might use the env var to alter behavior in dev mode. /// This does not change test runner configuration; it simply demonstrates conditional logic. func testDevModeBehaviorPlaceholder() throws { let isDevMode = ProcessInfo.processInfo.environment["USE_DEV_MODE"] == "YES" if isDevMode { XCTSkip("Dev mode: skipping heavy or duplicated UI configuration runs") } XCTAssertTrue(true) } } ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator-management/__tests__/index.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for simulator-management workflow metadata */ import { describe, it, expect } from 'vitest'; import { workflow } from '../index.ts'; describe('simulator-management workflow metadata', () => { describe('Workflow Structure', () => { it('should export workflow object with required properties', () => { expect(workflow).toHaveProperty('name'); expect(workflow).toHaveProperty('description'); expect(workflow).toHaveProperty('platforms'); expect(workflow).toHaveProperty('targets'); expect(workflow).toHaveProperty('projectTypes'); expect(workflow).toHaveProperty('capabilities'); }); it('should have correct workflow name', () => { expect(workflow.name).toBe('Simulator Management'); }); it('should have correct description', () => { expect(workflow.description).toBe( 'Tools for managing simulators from booting, opening simulators, listing simulators, stopping simulators, erasing simulator content and settings, and setting simulator environment options like location, network, statusbar and appearance.', ); }); it('should have correct platforms array', () => { expect(workflow.platforms).toEqual(['iOS']); }); it('should have correct targets array', () => { expect(workflow.targets).toEqual(['simulator']); }); it('should have correct projectTypes array', () => { expect(workflow.projectTypes).toEqual(['project', 'workspace']); }); it('should have correct capabilities array', () => { expect(workflow.capabilities).toEqual([ 'boot', 'open', 'list', 'appearance', 'location', 'network', 'statusbar', 'erase', ]); }); }); }); ``` -------------------------------------------------------------------------------- /src/server/server.ts: -------------------------------------------------------------------------------- ```typescript /** * Server Configuration - MCP Server setup and lifecycle management * * This module handles the creation, configuration, and lifecycle management of the * Model Context Protocol (MCP) server. It provides the foundation for all tool * registrations and server capabilities. * * Responsibilities: * - Creating and configuring the MCP server instance * - Setting up server capabilities and options * - Managing server lifecycle (start/stop) * - Handling transport configuration (stdio) */ import { McpServer } from '@camsoft/mcp-sdk/server/mcp.js'; import { StdioServerTransport } from '@camsoft/mcp-sdk/server/stdio.js'; import { log } from '../utils/logger.ts'; import { version } from '../version.ts'; import * as Sentry from '@sentry/node'; /** * Create and configure the MCP server * @returns Configured MCP server instance */ export function createServer(): McpServer { // Create server instance const baseServer = new McpServer( { name: 'xcodebuildmcp', version, }, { capabilities: { tools: { listChanged: true, }, resources: { subscribe: true, listChanged: true, }, logging: {}, }, }, ); // Wrap server with Sentry for MCP instrumentation const server = Sentry.wrapMcpServerWithSentry(baseServer); // Log server initialization log('info', `Server initialized with Sentry MCP instrumentation (version ${version})`); return server; } /** * Start the MCP server with stdio transport * @param server The MCP server instance to start */ export async function startServer(server: McpServer): Promise<void> { const transport = new StdioServerTransport(); await server.connect(transport); log('info', 'XcodeBuildMCP Server running on stdio'); } ``` -------------------------------------------------------------------------------- /src/mcp/resources/devices.ts: -------------------------------------------------------------------------------- ```typescript /** * Devices Resource Plugin * * Provides access to connected Apple devices through MCP resource system. * This resource reuses the existing list_devices tool logic to maintain consistency. */ import { log } from '../../utils/logging/index.ts'; import type { CommandExecutor } from '../../utils/execution/index.ts'; import { getDefaultCommandExecutor } from '../../utils/execution/index.ts'; import { list_devicesLogic } from '../tools/device/list_devices.ts'; // Testable resource logic separated from MCP handler export async function devicesResourceLogic( executor: CommandExecutor = getDefaultCommandExecutor(), ): Promise<{ contents: Array<{ text: string }> }> { try { log('info', 'Processing devices resource request'); const result = await list_devicesLogic({}, executor); if (result.isError) { const errorText = result.content[0]?.text; throw new Error(typeof errorText === 'string' ? errorText : 'Failed to retrieve device data'); } return { contents: [ { text: typeof result.content[0]?.text === 'string' ? result.content[0].text : 'No device data available', }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); log('error', `Error in devices resource handler: ${errorMessage}`); return { contents: [ { text: `Error retrieving device data: ${errorMessage}`, }, ], }; } } export default { uri: 'xcodebuildmcp://devices', name: 'devices', description: 'Connected physical Apple devices with their UUIDs, names, and connection status', mimeType: 'text/plain', async handler(): Promise<{ contents: Array<{ text: string }> }> { return devicesResourceLogic(); }, }; ``` -------------------------------------------------------------------------------- /src/mcp/resources/simulators.ts: -------------------------------------------------------------------------------- ```typescript /** * Simulator Resource Plugin * * Provides access to available iOS simulators through MCP resource system. * This resource reuses the existing list_sims tool logic to maintain consistency. */ import { log } from '../../utils/logging/index.ts'; import { getDefaultCommandExecutor } from '../../utils/execution/index.ts'; import type { CommandExecutor } from '../../utils/execution/index.ts'; import { list_simsLogic } from '../tools/simulator/list_sims.ts'; // Testable resource logic separated from MCP handler export async function simulatorsResourceLogic( executor: CommandExecutor = getDefaultCommandExecutor(), ): Promise<{ contents: Array<{ text: string }> }> { try { log('info', 'Processing simulators resource request'); const result = await list_simsLogic({}, executor); if (result.isError) { const errorText = result.content[0]?.text; throw new Error( typeof errorText === 'string' ? errorText : 'Failed to retrieve simulator data', ); } return { contents: [ { text: typeof result.content[0]?.text === 'string' ? result.content[0].text : 'No simulator data available', }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); log('error', `Error in simulators resource handler: ${errorMessage}`); return { contents: [ { text: `Error retrieving simulator data: ${errorMessage}`, }, ], }; } } export default { uri: 'xcodebuildmcp://simulators', name: 'simulators', description: 'Available iOS simulators with their UUIDs and states', mimeType: 'text/plain', async handler(): Promise<{ contents: Array<{ text: string }> }> { return simulatorsResourceLogic(); }, }; ``` -------------------------------------------------------------------------------- /src/mcp/resources/doctor.ts: -------------------------------------------------------------------------------- ```typescript /** * Doctor Resource Plugin * * Provides access to development environment doctor information through MCP resource system. * This resource reuses the existing doctor tool logic to maintain consistency. */ import { log } from '../../utils/logging/index.ts'; import { getDefaultCommandExecutor, CommandExecutor } from '../../utils/execution/index.ts'; import { doctorLogic } from '../tools/doctor/doctor.ts'; // Testable resource logic separated from MCP handler export async function doctorResourceLogic( executor: CommandExecutor = getDefaultCommandExecutor(), ): Promise<{ contents: Array<{ text: string }> }> { try { log('info', 'Processing doctor resource request'); const result = await doctorLogic({}, executor); if (result.isError) { const textItem = result.content.find((i) => i.type === 'text') as | { type: 'text'; text: string } | undefined; const errorText = textItem?.text; const errorMessage = typeof errorText === 'string' ? errorText : 'Failed to retrieve doctor data'; log('error', `Error in doctor resource handler: ${errorMessage}`); return { contents: [ { text: `Error retrieving doctor data: ${errorMessage}`, }, ], }; } const okTextItem = result.content.find((i) => i.type === 'text') as | { type: 'text'; text: string } | undefined; return { contents: [ { text: okTextItem?.text ?? 'No doctor data available', }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); log('error', `Error in doctor resource handler: ${errorMessage}`); return { contents: [ { text: `Error retrieving doctor data: ${errorMessage}`, }, ], }; } } export default { uri: 'xcodebuildmcp://doctor', name: 'doctor', description: 'Comprehensive development environment diagnostic information and configuration status', mimeType: 'text/plain', async handler(): Promise<{ contents: Array<{ text: string }> }> { return doctorResourceLogic(); }, }; ``` -------------------------------------------------------------------------------- /server.json: -------------------------------------------------------------------------------- ```json { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", "name": "com.xcodebuildmcp/XcodeBuildMCP", "description": "XcodeBuildMCP provides tools for Xcode project management, simulator management, and app utilities.", "status": "active", "repository": { "url": "https://github.com/cameroncooke/XcodeBuildMCP", "source": "github", "id": "945551361" }, "version": "1.14.1", "packages": [ { "registryType": "npm", "registryBaseUrl": "https://registry.npmjs.org", "identifier": "xcodebuildmcp", "version": "1.14.1", "transport": { "type": "stdio" }, "runtimeHint": "npx", "environmentVariables": [ { "name": "INCREMENTAL_BUILDS_ENABLED", "description": "Enable experimental xcodemake incremental builds (true/false or 1/0).", "format": "boolean", "default": "false", "choices": [ "true", "false", "1", "0" ] }, { "name": "XCODEBUILDMCP_DYNAMIC_TOOLS", "description": "Enable AI-powered dynamic tool discovery to load only relevant workflows.", "format": "boolean", "default": "false", "choices": [ "true", "false" ] }, { "name": "XCODEBUILDMCP_ENABLED_WORKFLOWS", "description": "Comma-separated list of workflows to load in Static Mode (e.g., 'simulator,device,project-discovery').", "format": "string", "default": "" }, { "name": "XCODEBUILDMCP_SENTRY_DISABLED", "description": "Disable Sentry error reporting (preferred flag).", "format": "boolean", "default": "false", "choices": [ "true", "false" ] }, { "name": "XCODEBUILDMCP_DEBUG", "description": "Enable verbose debug logging from the server.", "format": "boolean", "default": "false", "choices": [ "true", "false" ] } ] } ] } ``` -------------------------------------------------------------------------------- /example_projects/.vscode/launch.json: -------------------------------------------------------------------------------- ```json { "configurations": [ { "type": "swift", "request": "launch", "args": [], "cwd": "${workspaceFolder:example_projects}/spm", "name": "Debug spm (spm)", "program": "${workspaceFolder:example_projects}/spm/.build/debug/spm", "preLaunchTask": "swift: Build Debug spm (spm)" }, { "type": "swift", "request": "launch", "args": [], "cwd": "${workspaceFolder:example_projects}/spm", "name": "Release spm (spm)", "program": "${workspaceFolder:example_projects}/spm/.build/release/spm", "preLaunchTask": "swift: Build Release spm (spm)" }, { "type": "swift", "request": "launch", "args": [], "cwd": "${workspaceFolder:example_projects}/spm", "name": "Debug quick-task (spm)", "program": "${workspaceFolder:example_projects}/spm/.build/debug/quick-task", "preLaunchTask": "swift: Build Debug quick-task (spm)" }, { "type": "swift", "request": "launch", "args": [], "cwd": "${workspaceFolder:example_projects}/spm", "name": "Release quick-task (spm)", "program": "${workspaceFolder:example_projects}/spm/.build/release/quick-task", "preLaunchTask": "swift: Build Release quick-task (spm)" }, { "type": "swift", "request": "launch", "args": [], "cwd": "${workspaceFolder:example_projects}/spm", "name": "Debug long-server (spm)", "program": "${workspaceFolder:example_projects}/spm/.build/debug/long-server", "preLaunchTask": "swift: Build Debug long-server (spm)" }, { "type": "swift", "request": "launch", "args": [], "cwd": "${workspaceFolder:example_projects}/spm", "name": "Release long-server (spm)", "program": "${workspaceFolder:example_projects}/spm/.build/release/long-server", "preLaunchTask": "swift: Build Release long-server (spm)" } ] } ``` -------------------------------------------------------------------------------- /src/mcp/tools/swift-package/swift_package_clean.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import path from 'node:path'; import type { CommandExecutor } from '../../../utils/execution/index.ts'; import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; import { createErrorResponse } from '../../../utils/responses/index.ts'; import { log } from '../../../utils/logging/index.ts'; import { ToolResponse } from '../../../types/common.ts'; import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const swiftPackageCleanSchema = z.object({ packagePath: z.string().describe('Path to the Swift package root (Required)'), }); // Use z.infer for type safety type SwiftPackageCleanParams = z.infer<typeof swiftPackageCleanSchema>; export async function swift_package_cleanLogic( params: SwiftPackageCleanParams, executor: CommandExecutor, ): Promise<ToolResponse> { const resolvedPath = path.resolve(params.packagePath); const swiftArgs = ['package', '--package-path', resolvedPath, 'clean']; log('info', `Running swift ${swiftArgs.join(' ')}`); try { const result = await executor(['swift', ...swiftArgs], 'Swift Package Clean', true, undefined); if (!result.success) { const errorMessage = result.error ?? result.output ?? 'Unknown error'; return createErrorResponse('Swift package clean failed', errorMessage); } return { content: [ { type: 'text', text: '✅ Swift package cleaned successfully.' }, { type: 'text', text: '💡 Build artifacts and derived data removed. Ready for fresh build.', }, { type: 'text', text: result.output || '(clean completed silently)' }, ], isError: false, }; } catch (error) { const message = error instanceof Error ? error.message : String(error); log('error', `Swift package clean failed: ${message}`); return createErrorResponse('Failed to execute swift package clean', message); } } export default { name: 'swift_package_clean', description: 'Cleans Swift Package build artifacts and derived data', schema: swiftPackageCleanSchema.shape, // MCP SDK compatibility handler: createTypedTool( swiftPackageCleanSchema, swift_package_cleanLogic, getDefaultCommandExecutor, ), }; ``` -------------------------------------------------------------------------------- /example_projects/spm/Sources/TestLib/TaskManager.swift: -------------------------------------------------------------------------------- ```swift import Foundation public class TaskManager { private var isServerRunning = false public init() {} public func executeQuickTask(name: String, duration: Int, verbose: Bool) async { if verbose { print("📝 Task '\(name)' started at \(Date())") } // Simulate work with periodic output using Swift Concurrency for i in 1...duration { if verbose { print("⚙️ Working... step \(i)/\(duration)") } try? await Task.sleep(for: .seconds(1)) } if verbose { print("🎉 Task '\(name)' completed at \(Date())") } else { print("Task '\(name)' completed in \(duration)s") } } public func startLongRunningServer(port: Int, verbose: Bool, autoShutdown: Int) async { if verbose { print("🔧 Initializing server on port \(port)...") } var secondsRunning = 0 let startTime = Date() isServerRunning = true // Simulate server startup try? await Task.sleep(for: .milliseconds(500)) print("✅ Server running on port \(port)") // Main server loop using Swift Concurrency while isServerRunning { try? await Task.sleep(for: .seconds(1)) secondsRunning += 1 if verbose && secondsRunning % 5 == 0 { print("📊 Server heartbeat: \(secondsRunning)s uptime") } // Handle auto-shutdown if autoShutdown > 0 && secondsRunning >= autoShutdown { if verbose { print("⏰ Auto-shutdown triggered after \(autoShutdown)s") } break } } let uptime = Date().timeIntervalSince(startTime) print("🛑 Server stopped after \(String(format: "%.1f", uptime))s uptime") isServerRunning = false } public func stopServer() { isServerRunning = false } public func calculateSum(_ a: Int, _ b: Int) -> Int { return a + b } public func validateInput(_ input: String) -> Bool { return !input.isEmpty && input.count <= 100 } } ``` -------------------------------------------------------------------------------- /src/mcp/tools/session-management/__tests__/index.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for session-management workflow metadata */ import { describe, it, expect } from 'vitest'; import { workflow } from '../index.ts'; describe('session-management workflow metadata', () => { describe('Workflow Structure', () => { it('should export workflow object with required properties', () => { expect(workflow).toHaveProperty('name'); expect(workflow).toHaveProperty('description'); expect(workflow).toHaveProperty('platforms'); expect(workflow).toHaveProperty('targets'); expect(workflow).toHaveProperty('capabilities'); }); it('should have correct workflow name', () => { expect(workflow.name).toBe('session-management'); }); it('should have correct description', () => { expect(workflow.description).toBe( 'Manage session defaults for projectPath/workspacePath, scheme, configuration, simulatorName/simulatorId, deviceId, useLatestOS and arch. These defaults are required by many tools and must be set before attempting to call tools that would depend on these values.', ); }); it('should have correct platforms array', () => { expect(workflow.platforms).toEqual(['iOS', 'macOS', 'tvOS', 'watchOS', 'visionOS']); }); it('should have correct targets array', () => { expect(workflow.targets).toEqual(['simulator', 'device']); }); it('should have correct capabilities array', () => { expect(workflow.capabilities).toEqual(['configuration', 'state-management']); }); }); describe('Workflow Validation', () => { it('should have valid string properties', () => { expect(typeof workflow.name).toBe('string'); expect(typeof workflow.description).toBe('string'); expect(workflow.name.length).toBeGreaterThan(0); expect(workflow.description.length).toBeGreaterThan(0); }); it('should have valid array properties', () => { expect(Array.isArray(workflow.platforms)).toBe(true); expect(Array.isArray(workflow.targets)).toBe(true); expect(Array.isArray(workflow.capabilities)).toBe(true); expect(workflow.platforms.length).toBeGreaterThan(0); expect(workflow.targets.length).toBeGreaterThan(0); expect(workflow.capabilities.length).toBeGreaterThan(0); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/logging/start_sim_log_cap.ts: -------------------------------------------------------------------------------- ```typescript /** * Logging Plugin: Start Simulator Log Capture * * Starts capturing logs from a specified simulator. */ import { z } from 'zod'; import { startLogCapture } from '../../../utils/log-capture/index.ts'; import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/command.ts'; import { ToolResponse, createTextContent } from '../../../types/common.ts'; import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const startSimLogCapSchema = z.object({ simulatorUuid: z .string() .describe('UUID of the simulator to capture logs from (obtained from list_simulators).'), bundleId: z.string().describe('Bundle identifier of the app to capture logs for.'), captureConsole: z .boolean() .optional() .describe('Whether to capture console output (requires app relaunch).'), }); // Use z.infer for type safety type StartSimLogCapParams = z.infer<typeof startSimLogCapSchema>; export async function start_sim_log_capLogic( params: StartSimLogCapParams, _executor: CommandExecutor = getDefaultCommandExecutor(), logCaptureFunction: typeof startLogCapture = startLogCapture, ): Promise<ToolResponse> { const paramsWithDefaults = { ...params, captureConsole: params.captureConsole ?? false, }; const { sessionId, error } = await logCaptureFunction(paramsWithDefaults, _executor); if (error) { return { content: [createTextContent(`Error starting log capture: ${error}`)], isError: true, }; } return { content: [ createTextContent( `Log capture started successfully. Session ID: ${sessionId}.\n\n${paramsWithDefaults.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.`, ), ], }; } export default { name: 'start_sim_log_cap', description: 'Starts capturing logs from a specified simulator. Returns a session ID. By default, captures only structured logs.', schema: startSimLogCapSchema.shape, // MCP SDK compatibility handler: createTypedTool(startSimLogCapSchema, start_sim_log_capLogic, getDefaultCommandExecutor), }; ``` -------------------------------------------------------------------------------- /.github/workflows/claude.yml: -------------------------------------------------------------------------------- ```yaml name: Claude Code on: issue_comment: types: [created] pull_request_review_comment: types: [created] issues: types: [opened, assigned] pull_request_review: types: [submitted] jobs: claude: if: | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) runs-on: ubuntu-latest permissions: contents: write pull-requests: write issues: write id-token: write actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 1 - name: Run Claude Code id: claude uses: anthropics/claude-code-action@beta with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # This is an optional setting that allows Claude to read CI results on PRs additional_permissions: | actions: read # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) # model: "claude-opus-4-20250514" # Optional: Customize the trigger phrase (default: @claude) # trigger_phrase: "/claude" # Optional: Trigger when specific user is assigned to an issue assignee_trigger: "claude-bot" # Optional: Allow Claude to run specific commands allowed_tools: "Bash(npm install),Bash(npm run build:*),Bash(npm run test:*),Bash(npm run lint:*),Bash(npm run format:*),Bash(npm run doctor)" # Optional: Add custom instructions for Claude to customize its behavior for your project # custom_instructions: | # Follow our coding standards # Ensure all new code has tests # Use TypeScript for new files # Optional: Custom environment variables for Claude # claude_env: | # NODE_ENV: test ``` -------------------------------------------------------------------------------- /src/mcp/tools/session-management/session_set_defaults.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { sessionStore, type SessionDefaults } from '../../../utils/session-store.ts'; import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; import type { ToolResponse } from '../../../types/common.ts'; const baseSchema = z.object({ projectPath: z.string().optional(), workspacePath: z.string().optional(), scheme: z.string().optional(), configuration: z.string().optional(), simulatorName: z.string().optional(), simulatorId: z.string().optional(), deviceId: z.string().optional(), useLatestOS: z.boolean().optional(), arch: z.enum(['arm64', 'x86_64']).optional(), }); const schemaObj = baseSchema .refine((v) => !(v.projectPath && v.workspacePath), { message: 'projectPath and workspacePath are mutually exclusive', path: ['projectPath'], }) .refine((v) => !(v.simulatorId && v.simulatorName), { message: 'simulatorId and simulatorName are mutually exclusive', path: ['simulatorId'], }); type Params = z.infer<typeof schemaObj>; export async function sessionSetDefaultsLogic(params: Params): Promise<ToolResponse> { // Clear mutually exclusive counterparts before merging new defaults const toClear = new Set<keyof SessionDefaults>(); if (Object.prototype.hasOwnProperty.call(params, 'projectPath')) toClear.add('workspacePath'); if (Object.prototype.hasOwnProperty.call(params, 'workspacePath')) toClear.add('projectPath'); if (Object.prototype.hasOwnProperty.call(params, 'simulatorId')) toClear.add('simulatorName'); if (Object.prototype.hasOwnProperty.call(params, 'simulatorName')) toClear.add('simulatorId'); if (toClear.size > 0) { sessionStore.clear(Array.from(toClear)); } sessionStore.setDefaults(params as Partial<SessionDefaults>); const current = sessionStore.getAll(); return { content: [{ type: 'text', text: `Defaults updated:\n${JSON.stringify(current, null, 2)}` }], isError: false, }; } export default { name: 'session-set-defaults', description: 'Set the session defaults needed by many tools. Most tools require one or more session defaults to be set before they can be used. Agents should set the relevant defaults at the beginning of a session.', schema: baseSchema.shape, handler: createTypedTool(schemaObj, sessionSetDefaultsLogic, getDefaultCommandExecutor), }; ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/boot_sim.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { ToolResponse } from '../../../types/common.ts'; import { log } from '../../../utils/logging/index.ts'; import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; import type { CommandExecutor } from '../../../utils/execution/index.ts'; import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; const bootSimSchemaObject = z.object({ simulatorId: z.string().describe('UUID of the simulator to boot'), }); // Use z.infer for type safety type BootSimParams = z.infer<typeof bootSimSchemaObject>; const publicSchemaObject = bootSimSchemaObject.omit({ simulatorId: true, } as const); export async function boot_simLogic( params: BootSimParams, executor: CommandExecutor, ): Promise<ToolResponse> { log('info', `Starting xcrun simctl boot request for simulator ${params.simulatorId}`); try { const command = ['xcrun', 'simctl', 'boot', params.simulatorId]; const result = await executor(command, 'Boot Simulator', true); if (!result.success) { return { content: [ { type: 'text', text: `Boot simulator operation failed: ${result.error}`, }, ], }; } return { content: [ { type: 'text', text: `✅ Simulator booted successfully. To make it visible, use: open_sim() Next steps: 1. Open the Simulator app (makes it visible): open_sim() 2. Install an app: install_app_sim({ simulatorId: "${params.simulatorId}", appPath: "PATH_TO_YOUR_APP" }) 3. Launch an app: launch_app_sim({ simulatorId: "${params.simulatorId}", bundleId: "YOUR_APP_BUNDLE_ID" })`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); log('error', `Error during boot simulator operation: ${errorMessage}`); return { content: [ { type: 'text', text: `Boot simulator operation failed: ${errorMessage}`, }, ], }; } } export default { name: 'boot_sim', description: 'Boots an iOS simulator.', schema: publicSchemaObject.shape, handler: createSessionAwareTool<BootSimParams>({ internalSchema: bootSimSchemaObject, logicFunction: boot_simLogic, getExecutor: getDefaultCommandExecutor, requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }], }), }; ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/open_sim.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { ToolResponse } from '../../../types/common.ts'; import { log } from '../../../utils/logging/index.ts'; import type { CommandExecutor } from '../../../utils/execution/index.ts'; import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const openSimSchema = z.object({}); // Use z.infer for type safety type OpenSimParams = z.infer<typeof openSimSchema>; export async function open_simLogic( params: OpenSimParams, executor: CommandExecutor, ): Promise<ToolResponse> { log('info', 'Starting open simulator request'); try { const command = ['open', '-a', 'Simulator']; const result = await executor(command, 'Open Simulator', true); if (!result.success) { return { content: [ { type: 'text', text: `Open simulator operation failed: ${result.error}`, }, ], }; } return { content: [ { type: 'text', text: `Simulator app opened successfully`, }, { type: 'text', text: `Next Steps: 1. Boot a simulator if needed: boot_sim({ simulatorUuid: 'UUID_FROM_LIST_SIMULATORS' }) 2. Launch your app and interact with it 3. Log capture options: - Option 1: Capture structured logs only (app continues running): start_sim_log_cap({ simulatorUuid: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID' }) - Option 2: Capture both console and structured logs (app will restart): start_sim_log_cap({ simulatorUuid: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID', captureConsole: true }) - Option 3: Launch app with logs in one step: launch_app_logs_sim({ simulatorUuid: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID' })`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); log('error', `Error during open simulator operation: ${errorMessage}`); return { content: [ { type: 'text', text: `Open simulator operation failed: ${errorMessage}`, }, ], }; } } export default { name: 'open_sim', description: 'Opens the iOS Simulator app.', schema: openSimSchema.shape, // MCP SDK compatibility handler: createTypedTool(openSimSchema, open_simLogic, getDefaultCommandExecutor), }; ``` -------------------------------------------------------------------------------- /src/mcp/tools/macos/stop_mac_app.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { log } from '../../../utils/logging/index.ts'; import { ToolResponse } from '../../../types/common.ts'; import type { CommandExecutor } from '../../../utils/execution/index.ts'; import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const stopMacAppSchema = z.object({ appName: z .string() .optional() .describe('Name of the application to stop (e.g., "Calculator" or "MyApp")'), processId: z.number().optional().describe('Process ID (PID) of the application to stop'), }); // Use z.infer for type safety type StopMacAppParams = z.infer<typeof stopMacAppSchema>; export async function stop_mac_appLogic( params: StopMacAppParams, executor: CommandExecutor, ): Promise<ToolResponse> { if (!params.appName && !params.processId) { return { content: [ { type: 'text', text: 'Either appName or processId must be provided.', }, ], isError: true, }; } log( 'info', `Stopping macOS app: ${params.processId ? `PID ${params.processId}` : params.appName}`, ); try { let command: string[]; if (params.processId) { // Stop by process ID command = ['kill', String(params.processId)]; } else { // Stop by app name - use shell command with fallback for complex logic command = [ 'sh', '-c', `pkill -f "${params.appName}" || osascript -e 'tell application "${params.appName}" to quit'`, ]; } await executor(command, 'Stop macOS App'); return { content: [ { type: 'text', text: `✅ macOS app stopped successfully: ${params.processId ? `PID ${params.processId}` : params.appName}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); log('error', `Error stopping macOS app: ${errorMessage}`); return { content: [ { type: 'text', text: `❌ Stop macOS app operation failed: ${errorMessage}`, }, ], isError: true, }; } } export default { name: 'stop_mac_app', description: 'Stops a running macOS application. Can stop by app name or process ID.', schema: stopMacAppSchema.shape, // MCP SDK compatibility handler: createTypedTool(stopMacAppSchema, stop_mac_appLogic, getDefaultCommandExecutor), }; ``` -------------------------------------------------------------------------------- /src/mcp/tools/doctor/__tests__/index.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for doctor workflow metadata */ import { describe, it, expect } from 'vitest'; import { workflow } from '../index.ts'; describe('doctor workflow metadata', () => { describe('Workflow Structure', () => { it('should export workflow object with required properties', () => { expect(workflow).toHaveProperty('name'); expect(workflow).toHaveProperty('description'); expect(workflow).toHaveProperty('platforms'); expect(workflow).toHaveProperty('capabilities'); }); it('should have correct workflow name', () => { expect(workflow.name).toBe('System Doctor'); }); it('should have correct description', () => { expect(workflow.description).toBe( 'Debug tools and system doctor for troubleshooting XcodeBuildMCP server, development environment, and tool availability.', ); }); it('should have correct platforms array', () => { expect(workflow.platforms).toEqual(['system']); }); it('should have correct capabilities array', () => { expect(workflow.capabilities).toEqual([ 'doctor', 'server-diagnostics', 'troubleshooting', 'system-analysis', 'environment-validation', ]); }); }); describe('Workflow Validation', () => { it('should have valid string properties', () => { expect(typeof workflow.name).toBe('string'); expect(typeof workflow.description).toBe('string'); expect(workflow.name.length).toBeGreaterThan(0); expect(workflow.description.length).toBeGreaterThan(0); }); it('should have valid array properties', () => { expect(Array.isArray(workflow.platforms)).toBe(true); expect(Array.isArray(workflow.capabilities)).toBe(true); expect(workflow.platforms.length).toBeGreaterThan(0); expect(workflow.capabilities.length).toBeGreaterThan(0); }); it('should contain expected platform values', () => { expect(workflow.platforms).toContain('system'); }); it('should contain expected capability values', () => { expect(workflow.capabilities).toContain('doctor'); expect(workflow.capabilities).toContain('server-diagnostics'); expect(workflow.capabilities).toContain('troubleshooting'); expect(workflow.capabilities).toContain('system-analysis'); expect(workflow.capabilities).toContain('environment-validation'); }); it('should not have targets or projectTypes properties', () => { expect(workflow).not.toHaveProperty('targets'); expect(workflow).not.toHaveProperty('projectTypes'); }); }); }); ``` -------------------------------------------------------------------------------- /src/utils/environment.ts: -------------------------------------------------------------------------------- ```typescript /** * Environment Detection Utilities * * Provides abstraction for environment detection to enable testability * while maintaining production functionality. */ import { execSync } from 'child_process'; import { log } from './logger.ts'; /** * Interface for environment detection abstraction */ export interface EnvironmentDetector { /** * Detects if the MCP server is running under Claude Code * @returns true if Claude Code is detected, false otherwise */ isRunningUnderClaudeCode(): boolean; } /** * Production implementation of environment detection */ export class ProductionEnvironmentDetector implements EnvironmentDetector { isRunningUnderClaudeCode(): boolean { // Disable Claude Code detection during tests for environment-agnostic testing if (process.env.NODE_ENV === 'test' || process.env.VITEST === 'true') { return false; } // Method 1: Check for Claude Code environment variables if (process.env.CLAUDECODE === '1' || process.env.CLAUDE_CODE_ENTRYPOINT === 'cli') { return true; } // Method 2: Check parent process name try { const parentPid = process.ppid; if (parentPid) { const parentCommand = execSync(`ps -o command= -p ${parentPid}`, { encoding: 'utf8', timeout: 1000, }).trim(); if (parentCommand.includes('claude')) { return true; } } } catch (error) { // If process detection fails, fall back to environment variables only log('debug', `Failed to detect parent process: ${error}`); } return false; } } /** * Default environment detector instance for production use */ export const defaultEnvironmentDetector = new ProductionEnvironmentDetector(); /** * Gets the default environment detector for production use */ export function getDefaultEnvironmentDetector(): EnvironmentDetector { return defaultEnvironmentDetector; } /** * Normalizes a set of user-provided environment variables by ensuring they are * prefixed with TEST_RUNNER_. Variables already prefixed are preserved. * * Example: * normalizeTestRunnerEnv({ FOO: '1', TEST_RUNNER_BAR: '2' }) * => { TEST_RUNNER_FOO: '1', TEST_RUNNER_BAR: '2' } */ export function normalizeTestRunnerEnv(vars: Record<string, string>): Record<string, string> { const normalized: Record<string, string> = {}; for (const [key, value] of Object.entries(vars ?? {})) { if (value == null) continue; const prefixedKey = key.startsWith('TEST_RUNNER_') ? key : `TEST_RUNNER_${key}`; normalized[prefixedKey] = value; } return normalized; } ``` -------------------------------------------------------------------------------- /src/mcp/tools/device/stop_app_device.ts: -------------------------------------------------------------------------------- ```typescript /** * Device Workspace Plugin: Stop App Device * * Stops an app running on a physical Apple device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). * Requires deviceId and processId. */ import { z } from 'zod'; import { ToolResponse } from '../../../types/common.ts'; import { log } from '../../../utils/logging/index.ts'; import type { CommandExecutor } from '../../../utils/execution/index.ts'; import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const stopAppDeviceSchema = z.object({ deviceId: z.string().describe('UDID of the device (obtained from list_devices)'), processId: z.number().describe('Process ID (PID) of the app to stop'), }); // Use z.infer for type safety type StopAppDeviceParams = z.infer<typeof stopAppDeviceSchema>; export async function stop_app_deviceLogic( params: StopAppDeviceParams, executor: CommandExecutor, ): Promise<ToolResponse> { const { deviceId, processId } = params; log('info', `Stopping app with PID ${processId} on device ${deviceId}`); try { const result = await executor( [ 'xcrun', 'devicectl', 'device', 'process', 'terminate', '--device', deviceId, '--pid', processId.toString(), ], 'Stop app on device', true, // useShell undefined, // env ); if (!result.success) { return { content: [ { type: 'text', text: `Failed to stop app: ${result.error}`, }, ], isError: true, }; } return { content: [ { type: 'text', text: `✅ App stopped successfully\n\n${result.output}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); log('error', `Error stopping app on device: ${errorMessage}`); return { content: [ { type: 'text', text: `Failed to stop app on device: ${errorMessage}`, }, ], isError: true, }; } } export default { name: 'stop_app_device', description: 'Stops a running app on a connected device.', schema: stopAppDeviceSchema.omit({ deviceId: true } as const).shape, handler: createSessionAwareTool<StopAppDeviceParams>({ internalSchema: stopAppDeviceSchema as unknown as z.ZodType<StopAppDeviceParams>, logicFunction: stop_app_deviceLogic, getExecutor: getDefaultCommandExecutor, requirements: [{ allOf: ['deviceId'], message: 'deviceId is required' }], }), }; ``` -------------------------------------------------------------------------------- /.cursor/BUGBOT.md: -------------------------------------------------------------------------------- ```markdown # Bugbot Review Guide for XcodeBuildMCP ## Project Snapshot XcodeBuildMCP is an MCP server exposing Xcode / Swift workflows as **tools** and **resources**. Stack: TypeScript · Node.js · plugin-based auto-discovery (`src/mcp/tools`, `src/mcp/resources`). For full details see [README.md](README.md) and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). --- ## 1. Security Checklist — Critical * No hard-coded secrets, tokens or DSNs. * All shell commands must flow through `CommandExecutor` with validated arguments (no direct `child_process` calls). * Paths must be sanitised via helpers in `src/utils/validation.ts`. * Sentry breadcrumbs / logs must **NOT** include user PII. --- ## 2. Architecture Checklist — Critical | Rule | Quick diff heuristic | |------|----------------------| | Dependency injection only | New `child_process` \| `fs` import ⇒ **critical** | | Handler / Logic split | `handler` > 20 LOC or contains branching ⇒ **critical** | | Plugin auto-registration | Manual `registerTool(...)` / `registerResource(...)` ⇒ **critical** | Positive pattern skeleton: ```ts // src/mcp/tools/foo-bar.ts export async function fooBarLogic( params: FooBarParams, exec: CommandExecutor = getDefaultCommandExecutor(), fs: FileSystemExecutor = getDefaultFileSystemExecutor(), ) { // ... } export const handler = (p: FooBarParams) => fooBarLogic(p); ``` --- ## 3. Testing Checklist * **Ban on Vitest mocking** (`vi.mock`, `vi.fn`, `vi.spyOn`, `.mock*`) ⇒ critical. Use `createMockExecutor` / `createMockFileSystemExecutor`. * Each tool must have tests covering happy-path **and** at least one failure path. * Avoid the `any` type unless justified with an inline comment. --- ## 4. Documentation Checklist * `docs/TOOLS.md` must exactly mirror the structure of `src/mcp/tools/**` (exclude `__tests__` and `*-shared`). *Diff heuristic*: if a PR adds/removes a tool but does **not** change `docs/TOOLS.md` ⇒ **warning**. * Update public docs when CLI parameters or tool names change. --- ## 5. Common Anti-Patterns (and fixes) | Anti-pattern | Preferred approach | |--------------|--------------------| | Complex logic in `handler` | Move to `*Logic` function | | Re-implementing logging | Use `src/utils/logger.ts` | | Direct `fs` / `child_process` usage | Inject `FileSystemExecutor` / `CommandExecutor` | | Chained re-exports | Export directly from source | --- ### How Bugbot Can Verify Rules 1. **Mocking violations**: search `*.test.ts` for `vi.` → critical. 2. **DI compliance**: search for direct `child_process` / `fs` imports outside executors. 3. **Docs accuracy**: compare `docs/TOOLS.md` against `src/mcp/tools/**`. 4. **Style**: ensure ESLint and Prettier pass (`npm run lint`, `npm run format:check`). --- Happy reviewing 🚀 ``` -------------------------------------------------------------------------------- /src/mcp/tools/session-management/__tests__/session_clear_defaults.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { sessionStore } from '../../../../utils/session-store.ts'; import plugin, { sessionClearDefaultsLogic } from '../session_clear_defaults.ts'; describe('session-clear-defaults tool', () => { beforeEach(() => { sessionStore.clear(); sessionStore.setDefaults({ scheme: 'MyScheme', projectPath: '/path/to/proj.xcodeproj', simulatorName: 'iPhone 16', deviceId: 'DEVICE-123', useLatestOS: true, arch: 'arm64', }); }); afterEach(() => { sessionStore.clear(); }); describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(plugin.name).toBe('session-clear-defaults'); }); it('should have correct description', () => { expect(plugin.description).toBe('Clear selected or all session defaults.'); }); it('should have handler function', () => { expect(typeof plugin.handler).toBe('function'); }); it('should have schema object', () => { expect(plugin.schema).toBeDefined(); expect(typeof plugin.schema).toBe('object'); }); }); describe('Handler Behavior', () => { it('should clear specific keys when provided', async () => { const result = await sessionClearDefaultsLogic({ keys: ['scheme', 'deviceId'] }); expect(result.isError).toBe(false); expect(result.content[0].text).toContain('Session defaults cleared'); const current = sessionStore.getAll(); expect(current.scheme).toBeUndefined(); expect(current.deviceId).toBeUndefined(); expect(current.projectPath).toBe('/path/to/proj.xcodeproj'); expect(current.simulatorName).toBe('iPhone 16'); expect(current.useLatestOS).toBe(true); expect(current.arch).toBe('arm64'); }); it('should clear all when all=true', async () => { const result = await sessionClearDefaultsLogic({ all: true }); expect(result.isError).toBe(false); expect(result.content[0].text).toBe('Session defaults cleared'); const current = sessionStore.getAll(); expect(Object.keys(current).length).toBe(0); }); it('should clear all when no params provided', async () => { const result = await sessionClearDefaultsLogic({}); expect(result.isError).toBe(false); const current = sessionStore.getAll(); expect(Object.keys(current).length).toBe(0); }); it('should validate keys enum', async () => { const result = (await plugin.handler({ keys: ['invalid' as any] })) as any; expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Parameter validation failed'); expect(result.content[0].text).toContain('keys'); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/device/install_app_device.ts: -------------------------------------------------------------------------------- ```typescript /** * Device Workspace Plugin: Install App Device * * Installs an app on a physical Apple device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). * Requires deviceId and appPath. */ import { z } from 'zod'; import { ToolResponse } from '../../../types/common.ts'; import { log } from '../../../utils/logging/index.ts'; import type { CommandExecutor } from '../../../utils/execution/index.ts'; import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const installAppDeviceSchema = z.object({ deviceId: z .string() .min(1, 'Device ID cannot be empty') .describe('UDID of the device (obtained from list_devices)'), appPath: z .string() .describe('Path to the .app bundle to install (full path to the .app directory)'), }); // Use z.infer for type safety type InstallAppDeviceParams = z.infer<typeof installAppDeviceSchema>; /** * Business logic for installing an app on a physical Apple device */ export async function install_app_deviceLogic( params: InstallAppDeviceParams, executor: CommandExecutor, ): Promise<ToolResponse> { const { deviceId, appPath } = params; log('info', `Installing app on device ${deviceId}`); try { const result = await executor( ['xcrun', 'devicectl', 'device', 'install', 'app', '--device', deviceId, appPath], 'Install app on device', true, // useShell undefined, // env ); if (!result.success) { return { content: [ { type: 'text', text: `Failed to install app: ${result.error}`, }, ], isError: true, }; } return { content: [ { type: 'text', text: `✅ App installed successfully on device ${deviceId}\n\n${result.output}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); log('error', `Error installing app on device: ${errorMessage}`); return { content: [ { type: 'text', text: `Failed to install app on device: ${errorMessage}`, }, ], isError: true, }; } } export default { name: 'install_app_device', description: 'Installs an app on a connected device.', schema: installAppDeviceSchema.omit({ deviceId: true } as const).shape, handler: createSessionAwareTool<InstallAppDeviceParams>({ internalSchema: installAppDeviceSchema as unknown as z.ZodType<InstallAppDeviceParams>, logicFunction: install_app_deviceLogic, getExecutor: getDefaultCommandExecutor, requirements: [{ allOf: ['deviceId'], message: 'deviceId is required' }], }), }; ``` -------------------------------------------------------------------------------- /src/mcp/tools/project-discovery/__tests__/index.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for project-discovery workflow metadata */ import { describe, it, expect } from 'vitest'; import { workflow } from '../index.ts'; describe('project-discovery workflow metadata', () => { describe('Workflow Structure', () => { it('should export workflow object with required properties', () => { expect(workflow).toHaveProperty('name'); expect(workflow).toHaveProperty('description'); expect(workflow).toHaveProperty('platforms'); expect(workflow).toHaveProperty('capabilities'); }); it('should have correct workflow name', () => { expect(workflow.name).toBe('Project Discovery'); }); it('should have correct description', () => { expect(workflow.description).toBe( 'Discover and examine Xcode projects, workspaces, and Swift packages. Analyze project structure, schemes, build settings, and bundle information.', ); }); it('should have correct platforms array', () => { expect(workflow.platforms).toEqual(['iOS', 'macOS', 'watchOS', 'tvOS', 'visionOS']); }); it('should have correct capabilities array', () => { expect(workflow.capabilities).toEqual([ 'project-analysis', 'scheme-discovery', 'build-settings', 'bundle-inspection', ]); }); }); describe('Workflow Validation', () => { it('should have valid string properties', () => { expect(typeof workflow.name).toBe('string'); expect(typeof workflow.description).toBe('string'); expect(workflow.name.length).toBeGreaterThan(0); expect(workflow.description.length).toBeGreaterThan(0); }); it('should have valid array properties', () => { expect(Array.isArray(workflow.platforms)).toBe(true); expect(Array.isArray(workflow.capabilities)).toBe(true); expect(workflow.platforms.length).toBeGreaterThan(0); expect(workflow.capabilities.length).toBeGreaterThan(0); }); it('should contain expected platform values', () => { expect(workflow.platforms).toContain('iOS'); expect(workflow.platforms).toContain('macOS'); expect(workflow.platforms).toContain('watchOS'); expect(workflow.platforms).toContain('tvOS'); expect(workflow.platforms).toContain('visionOS'); }); it('should contain expected capability values', () => { expect(workflow.capabilities).toContain('project-analysis'); expect(workflow.capabilities).toContain('scheme-discovery'); expect(workflow.capabilities).toContain('build-settings'); expect(workflow.capabilities).toContain('bundle-inspection'); }); it('should not have targets or projectTypes properties', () => { expect(workflow).not.toHaveProperty('targets'); expect(workflow).not.toHaveProperty('projectTypes'); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/launch_app_logs_sim.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { ToolResponse, createTextContent } from '../../../types/common.ts'; import { log } from '../../../utils/logging/index.ts'; import { startLogCapture } from '../../../utils/log-capture/index.ts'; import type { CommandExecutor } from '../../../utils/execution/index.ts'; import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; export type LogCaptureFunction = ( params: { simulatorUuid: string; bundleId: string; captureConsole?: boolean; args?: string[]; }, executor: CommandExecutor, ) => Promise<{ sessionId: string; logFilePath: string; processes: unknown[]; error?: string }>; const launchAppLogsSimSchemaObject = z.object({ simulatorId: z.string().describe('UUID of the simulator to target'), bundleId: z .string() .describe("Bundle identifier of the app to launch (e.g., 'com.example.MyApp')"), args: z.array(z.string()).optional().describe('Additional arguments to pass to the app'), }); type LaunchAppLogsSimParams = z.infer<typeof launchAppLogsSimSchemaObject>; const publicSchemaObject = launchAppLogsSimSchemaObject.omit({ simulatorId: true, } as const); export async function launch_app_logs_simLogic( params: LaunchAppLogsSimParams, executor: CommandExecutor = getDefaultCommandExecutor(), logCaptureFunction: LogCaptureFunction = startLogCapture, ): Promise<ToolResponse> { log('info', `Starting app launch with logs for simulator ${params.simulatorId}`); const captureParams = { simulatorUuid: params.simulatorId, bundleId: params.bundleId, captureConsole: true, ...(params.args && params.args.length > 0 ? { args: params.args } : {}), } as const; const { sessionId, error } = await logCaptureFunction(captureParams, executor); if (error) { return { content: [createTextContent(`App was launched but log capture failed: ${error}`)], isError: true, }; } return { content: [ createTextContent( `App launched successfully in simulator ${params.simulatorId} with log capture enabled.\n\nLog capture session ID: ${sessionId}\n\nNext Steps:\n1. Interact with your app in the simulator.\n2. Use 'stop_and_get_simulator_log({ logSessionId: "${sessionId}" })' to stop capture and retrieve logs.`, ), ], isError: false, }; } export default { name: 'launch_app_logs_sim', description: 'Launches an app in an iOS simulator and captures its logs.', schema: publicSchemaObject.shape, handler: createSessionAwareTool<LaunchAppLogsSimParams>({ internalSchema: launchAppLogsSimSchemaObject, logicFunction: launch_app_logs_simLogic, getExecutor: getDefaultCommandExecutor, requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }], }), }; ``` -------------------------------------------------------------------------------- /src/mcp/tools/macos/launch_mac_app.ts: -------------------------------------------------------------------------------- ```typescript /** * macOS Workspace Plugin: Launch macOS App * * Launches a macOS application using the 'open' command. * IMPORTANT: You MUST provide the appPath parameter. */ import { z } from 'zod'; import { log } from '../../../utils/logging/index.ts'; import { validateFileExists } from '../../../utils/validation/index.ts'; import { ToolResponse } from '../../../types/common.ts'; import type { CommandExecutor, FileSystemExecutor } from '../../../utils/execution/index.ts'; import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const launchMacAppSchema = z.object({ appPath: z .string() .describe('Path to the macOS .app bundle to launch (full path to the .app directory)'), args: z.array(z.string()).optional().describe('Additional arguments to pass to the app'), }); // Use z.infer for type safety type LaunchMacAppParams = z.infer<typeof launchMacAppSchema>; export async function launch_mac_appLogic( params: LaunchMacAppParams, executor: CommandExecutor, fileSystem?: FileSystemExecutor, ): Promise<ToolResponse> { // Validate that the app file exists const fileExistsValidation = validateFileExists(params.appPath, fileSystem); if (!fileExistsValidation.isValid) { return fileExistsValidation.errorResponse!; } log('info', `Starting launch macOS app request for ${params.appPath}`); try { // Construct the command as string array for CommandExecutor const command = ['open', params.appPath]; // Add any additional arguments if provided if (params.args && Array.isArray(params.args) && params.args.length > 0) { command.push('--args', ...params.args); } // Execute the command using CommandExecutor await executor(command, 'Launch macOS App'); // Return success response return { content: [ { type: 'text', text: `✅ macOS app launched successfully: ${params.appPath}`, }, ], }; } catch (error) { // Handle errors const errorMessage = error instanceof Error ? error.message : String(error); log('error', `Error during launch macOS app operation: ${errorMessage}`); return { content: [ { type: 'text', text: `❌ Launch macOS app operation failed: ${errorMessage}`, }, ], isError: true, }; } } export default { name: 'launch_mac_app', description: "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.", schema: launchMacAppSchema.shape, // MCP SDK compatibility handler: createTypedTool(launchMacAppSchema, launch_mac_appLogic, getDefaultCommandExecutor), }; ``` -------------------------------------------------------------------------------- /src/types/common.ts: -------------------------------------------------------------------------------- ```typescript /** * Common type definitions used across the server * * This module provides core type definitions and interfaces used throughout the codebase. * It establishes a consistent type system for platform identification, tool responses, * and other shared concepts. * * Responsibilities: * - Defining the XcodePlatform enum for platform identification * - Establishing the ToolResponse interface for standardized tool outputs * - Providing ToolResponseContent types for different response formats * - Supporting error handling with standardized error response types */ /** * Enum representing Xcode build platforms. */ export enum XcodePlatform { macOS = 'macOS', iOS = 'iOS', iOSSimulator = 'iOS Simulator', watchOS = 'watchOS', watchOSSimulator = 'watchOS Simulator', tvOS = 'tvOS', tvOSSimulator = 'tvOS Simulator', visionOS = 'visionOS', visionOSSimulator = 'visionOS Simulator', } /** * ToolResponse - Standard response format for tools * Compatible with MCP CallToolResult interface from the SDK */ export interface ToolResponse { content: ToolResponseContent[]; isError?: boolean; _meta?: Record<string, unknown>; [key: string]: unknown; // Index signature to match CallToolResult } /** * Contents that can be included in a tool response */ export type ToolResponseContent = | { type: 'text'; text: string; [key: string]: unknown; // Index signature to match ContentItem } | { type: 'image'; data: string; // Base64-encoded image data (without URI scheme prefix) mimeType: string; // e.g., 'image/png', 'image/jpeg' [key: string]: unknown; // Index signature to match ContentItem }; export function createTextContent(text: string): { type: 'text'; text: string } { return { type: 'text', text }; } export function createImageContent( data: string, mimeType: string, ): { type: 'image'; data: string; mimeType: string } { return { type: 'image', data, mimeType }; } /** * ValidationResult - Result of parameter validation operations */ export interface ValidationResult { isValid: boolean; errorResponse?: ToolResponse; warningResponse?: ToolResponse; } /** * CommandResponse - Generic result of command execution */ export interface CommandResponse { success: boolean; output: string; error?: string; process?: unknown; // ChildProcess from node:child_process } /** * Interface for shared build parameters */ export interface SharedBuildParams { workspacePath?: string; projectPath?: string; scheme: string; configuration: string; derivedDataPath?: string; extraArgs?: string[]; } /** * Interface for platform-specific build options */ export interface PlatformBuildOptions { platform: XcodePlatform; simulatorName?: string; simulatorId?: string; deviceId?: string; useLatestOS?: boolean; arch?: string; logPrefix: string; } ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- ```yaml name: Bug Report description: Report a bug or issue with XcodeBuildMCP title: "[Bug]: " labels: ["bug"] body: - type: markdown attributes: value: | Thanks for taking the time to report an issue with XcodeBuildMCP! - type: textarea id: description attributes: label: Bug Description description: A description of the bug or issue you're experiencing. placeholder: When trying to build my iOS app using the AI assistant... validations: required: true - type: textarea id: debug attributes: label: Debug Output description: Ask your agent "Run the XcodeBuildMCP `doctor` tool and return the output as markdown verbatim" and then copy paste it here. placeholder: | ``` XcodeBuildMCP Doctor Generated: 2025-08-11T17:42:29.812Z Server Version: 1.11.2 ## System Information - platform: darwin - release: 25.0.0 - arch: arm64 ... ``` validations: required: true - type: input id: editor-client attributes: label: Editor/Client description: The editor or MCP client you're using placeholder: Cursor 0.49.1 validations: required: true - type: input id: mcp-server-version attributes: label: MCP Server Version description: The version of XcodeBuildMCP you're using placeholder: 1.2.2 validations: required: true - type: input id: llm attributes: label: LLM description: The AI model you're using placeholder: Claude 3.5 Sonnet validations: required: true - type: textarea id: mcp-config attributes: label: MCP Configuration description: Your MCP configuration file (if applicable) placeholder: | ```json { "mcpServers": { "XcodeBuildMCP": {...} } } ``` render: json - type: textarea id: steps attributes: label: Steps to Reproduce description: Steps to reproduce the behavior placeholder: | 1. What you asked the AI agent to do 2. What the AI agent attempted to do 3. What failed or didn't work as expected validations: required: true - type: textarea id: expected attributes: label: Expected Behavior description: What you expected to happen placeholder: The AI should have been able to... validations: required: true - type: textarea id: actual attributes: label: Actual Behavior description: What actually happened placeholder: Instead, the AI... validations: required: true - type: textarea id: error attributes: label: Error Messages description: Any error messages or unexpected output placeholder: Error message or output from the AI render: shell ``` -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- ```json { "$schema": "https://raw.githubusercontent.com/microsoft/vscode/master/extensions/npm/schemas/v1.1.1/tasks.schema.json", "version": "2.0.0", "tasks": [ { "label": "build", "type": "npm", "script": "build", "group": { "kind": "build", "isDefault": true }, "presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "shared", "showReuseMessage": true, "clear": false }, "problemMatcher": [ "$tsc" ] }, { "label": "run", "type": "npm", "script": "inspect", "group": "build", "presentation": { "echo": true, "reveal": "always", "focus": true, "panel": "new" } }, { "label": "test", "type": "npm", "script": "test", "group": { "kind": "test", "isDefault": true }, "presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "shared" } }, { "label": "lint", "type": "npm", "script": "lint", "group": "build", "presentation": { "echo": true, "reveal": "silent", "focus": false, "panel": "shared" }, "problemMatcher": [ "$eslint-stylish" ] }, { "label": "lint:fix", "type": "npm", "script": "lint:fix", "group": "build" }, { "label": "format", "type": "npm", "script": "format", "group": "build" }, { "label": "typecheck (watch)", "type": "shell", "command": "npx tsc --noEmit --watch", "isBackground": true, "problemMatcher": [ "$tsc-watch" ], "group": "build" }, { "label": "dev (watch)", "type": "npm", "script": "dev", "isBackground": true, "group": "build", "presentation": { "panel": "dedicated", "reveal": "always" } }, { "label": "build: dev doctor", "dependsOn": [ "lint", "typecheck (watch)" ], "group": { "kind": "build", "isDefault": false } } ] } ``` -------------------------------------------------------------------------------- /src/mcp/tools/ui-testing/__tests__/index.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for ui-testing workflow metadata */ import { describe, it, expect } from 'vitest'; import { workflow } from '../index.ts'; describe('ui-testing workflow metadata', () => { describe('Workflow Structure', () => { it('should export workflow object with required properties', () => { expect(workflow).toHaveProperty('name'); expect(workflow).toHaveProperty('description'); expect(workflow).toHaveProperty('platforms'); expect(workflow).toHaveProperty('targets'); expect(workflow).toHaveProperty('capabilities'); }); it('should have correct workflow name', () => { expect(workflow.name).toBe('UI Testing & Automation'); }); it('should have correct description', () => { expect(workflow.description).toBe( 'UI automation and accessibility testing tools for iOS simulators. Perform gestures, interactions, screenshots, and UI analysis for automated testing workflows.', ); }); it('should have correct platforms array', () => { expect(workflow.platforms).toEqual(['iOS']); }); it('should have correct targets array', () => { expect(workflow.targets).toEqual(['simulator']); }); it('should have correct capabilities array', () => { expect(workflow.capabilities).toEqual([ 'ui-automation', 'gesture-simulation', 'screenshot-capture', 'accessibility-testing', 'ui-analysis', ]); }); }); describe('Workflow Validation', () => { it('should have valid string properties', () => { expect(typeof workflow.name).toBe('string'); expect(typeof workflow.description).toBe('string'); expect(workflow.name.length).toBeGreaterThan(0); expect(workflow.description.length).toBeGreaterThan(0); }); it('should have valid array properties', () => { expect(Array.isArray(workflow.platforms)).toBe(true); expect(Array.isArray(workflow.targets)).toBe(true); expect(Array.isArray(workflow.capabilities)).toBe(true); expect(workflow.platforms.length).toBeGreaterThan(0); expect(workflow.targets.length).toBeGreaterThan(0); expect(workflow.capabilities.length).toBeGreaterThan(0); }); it('should contain expected platform values', () => { expect(workflow.platforms).toContain('iOS'); }); it('should contain expected target values', () => { expect(workflow.targets).toContain('simulator'); }); it('should contain expected capability values', () => { expect(workflow.capabilities).toContain('ui-automation'); expect(workflow.capabilities).toContain('gesture-simulation'); expect(workflow.capabilities).toContain('screenshot-capture'); expect(workflow.capabilities).toContain('accessibility-testing'); expect(workflow.capabilities).toContain('ui-analysis'); }); it('should not have projectTypes property', () => { expect(workflow).not.toHaveProperty('projectTypes'); }); }); }); ``` -------------------------------------------------------------------------------- /.github/workflows/claude-code-review.yml: -------------------------------------------------------------------------------- ```yaml name: Claude Code Review on: pull_request: types: [opened, synchronize] # Optional: Only run on specific file changes # paths: # - "src/**/*.ts" # - "src/**/*.tsx" # - "src/**/*.js" # - "src/**/*.jsx" jobs: claude-review: # Optional: Filter by PR author # if: | # github.event.pull_request.user.login == 'external-contributor' || # github.event.pull_request.user.login == 'new-developer' || # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' runs-on: ubuntu-latest permissions: contents: read pull-requests: read issues: read id-token: write steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 1 - name: Run Claude Code Review id: claude-review uses: anthropics/claude-code-action@beta with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) # model: "claude-opus-4-20250514" # Direct prompt for automated review (no @claude mention needed) direct_prompt: | Please review this pull request and provide feedback on: - Code quality and best practices - Potential bugs or issues - Performance considerations - Security concerns - Test coverage Be constructive and helpful in your feedback. # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR # use_sticky_comment: true # Optional: Customize review based on file types # direct_prompt: | # Review this PR focusing on: # - For TypeScript files: Type safety and proper interface usage # - For API endpoints: Security, input validation, and error handling # - For React components: Performance, accessibility, and best practices # - For tests: Coverage, edge cases, and test quality # Optional: Different prompts for different authors # direct_prompt: | # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' || 'Please provide a thorough code review focusing on our coding standards and best practices.' }} # Optional: Add specific tools for running tests or linting # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)" # Optional: Skip review for certain conditions # if: | # !contains(github.event.pull_request.title, '[skip-review]') && # !contains(github.event.pull_request.title, '[WIP]') ``` -------------------------------------------------------------------------------- /src/mcp/tools/swift-package/swift_package_build.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import path from 'node:path'; import { createErrorResponse } from '../../../utils/responses/index.ts'; import { log } from '../../../utils/logging/index.ts'; import type { CommandExecutor } from '../../../utils/execution/index.ts'; import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; import { ToolResponse } from '../../../types/common.ts'; import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const swiftPackageBuildSchema = z.object({ packagePath: z.string().describe('Path to the Swift package root (Required)'), targetName: z.string().optional().describe('Optional target to build'), configuration: z .enum(['debug', 'release']) .optional() .describe('Swift package configuration (debug, release)'), architectures: z.array(z.string()).optional().describe('Target architectures to build for'), parseAsLibrary: z.boolean().optional().describe('Build as library instead of executable'), }); // Use z.infer for type safety type SwiftPackageBuildParams = z.infer<typeof swiftPackageBuildSchema>; export async function swift_package_buildLogic( params: SwiftPackageBuildParams, executor: CommandExecutor, ): Promise<ToolResponse> { const resolvedPath = path.resolve(params.packagePath); const swiftArgs = ['build', '--package-path', resolvedPath]; if (params.configuration && params.configuration.toLowerCase() === 'release') { swiftArgs.push('-c', 'release'); } if (params.targetName) { swiftArgs.push('--target', params.targetName); } if (params.architectures) { for (const arch of params.architectures) { swiftArgs.push('--arch', arch); } } if (params.parseAsLibrary) { swiftArgs.push('-Xswiftc', '-parse-as-library'); } log('info', `Running swift ${swiftArgs.join(' ')}`); try { const result = await executor(['swift', ...swiftArgs], 'Swift Package Build', true, undefined); if (!result.success) { const errorMessage = result.error ?? result.output ?? 'Unknown error'; return createErrorResponse('Swift package build failed', errorMessage); } return { content: [ { type: 'text', text: '✅ Swift package build succeeded.' }, { type: 'text', text: '💡 Next: Run tests with swift_package_test or execute with swift_package_run', }, { type: 'text', text: result.output }, ], isError: false, }; } catch (error) { const message = error instanceof Error ? error.message : String(error); log('error', `Swift package build failed: ${message}`); return createErrorResponse('Failed to execute swift build', message); } } export default { name: 'swift_package_build', description: 'Builds a Swift Package with swift build', schema: swiftPackageBuildSchema.shape, // MCP SDK compatibility handler: createTypedTool( swiftPackageBuildSchema, swift_package_buildLogic, getDefaultCommandExecutor, ), }; ```