This is page 8 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 -------------------------------------------------------------------------------- /docs/RELOADEROO_FOR_XCODEBUILDMCP.md: -------------------------------------------------------------------------------- ```markdown # Reloaderoo Usage Guide for XcodeBuildMCP This guide explains how to use Reloaderoo for interacting with XcodeBuildMCP as a CLI to save context window space. You can use this guide to prompt your agent, but providing the entire document will give you no actual benefits. You will end up using more context than just using MCP server directly. So it's recommended that you curate this document by removing the example commands that you don't need and just keeping the ones that are right for your project. You'll then want to keep this file within your project workspace and then include it in the context window when you need to interact your agent to use XcodeBuildMCP tools. > [!IMPORTANT] > Please remove this introduction before you prompt your agent with this file or any derrived version of it. ## Installation Reloaderoo is available via npm and can be used with npx for universal compatibility. ```bash # Use npx to run reloaderoo npx reloaderoo@latest --help ``` **Example Tool Calls:** ### Dynamic Tool Discovery - **`discover_tools`**: Analyzes a task description to enable relevant tools. ```bash npx reloaderoo@latest inspect call-tool discover_tools --params '{"task_description": "I want to build and run my iOS app on a simulator."}' -- node build/index.js ``` ### iOS Device Development - **`build_device`**: Builds an app for a physical device. ```bash npx reloaderoo@latest inspect call-tool build_device --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js ``` - **`get_device_app_path`**: Gets the `.app` bundle path for a device build. ```bash npx reloaderoo@latest inspect call-tool get_device_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js ``` - **`install_app_device`**: Installs an app on a physical device. ```bash npx reloaderoo@latest inspect call-tool install_app_device --params '{"deviceId": "DEVICE_UDID", "appPath": "/path/to/MyApp.app"}' -- node build/index.js ``` - **`launch_app_device`**: Launches an app on a physical device. ```bash npx reloaderoo@latest inspect call-tool launch_app_device --params '{"deviceId": "DEVICE_UDID", "bundleId": "com.example.MyApp"}' -- node build/index.js ``` - **`list_devices`**: Lists connected physical devices. ```bash npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/index.js ``` - **`stop_app_device`**: Stops an app on a physical device. ```bash npx reloaderoo@latest inspect call-tool stop_app_device --params '{"deviceId": "DEVICE_UDID", "processId": 12345}' -- node build/index.js ``` - **`test_device`**: Runs tests on a physical device. ```bash npx reloaderoo@latest inspect call-tool test_device --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "deviceId": "DEVICE_UDID"}' -- node build/index.js ``` ### iOS Simulator Development - **`boot_sim`**: Boots a simulator. ```bash npx reloaderoo@latest inspect call-tool boot_sim --params '{"simulatorId": "SIMULATOR_UUID"}' -- node build/index.js ``` - **`build_run_sim`**: Builds and runs an app on a simulator. ```bash npx reloaderoo@latest inspect call-tool build_run_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -- node build/index.js ``` - **`build_sim`**: Builds an app for a simulator. ```bash npx reloaderoo@latest inspect call-tool build_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -- node build/index.js ``` - **`get_sim_app_path`**: Gets the `.app` bundle path for a simulator build. ```bash npx reloaderoo@latest inspect call-tool get_sim_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "platform": "iOS Simulator", "simulatorName": "iPhone 16"}' -- node build/index.js ``` - **`install_app_sim`**: Installs an app on a simulator. ```bash npx reloaderoo@latest inspect call-tool install_app_sim --params '{"simulatorId": "SIMULATOR_UUID", "appPath": "/path/to/MyApp.app"}' -- node build/index.js ``` - **`launch_app_logs_sim`**: Launches an app on a simulator with log capture. ```bash npx reloaderoo@latest inspect call-tool launch_app_logs_sim --params '{"simulatorId": "SIMULATOR_UUID", "bundleId": "com.example.MyApp"}' -- node build/index.js ``` - **`launch_app_sim`**: Launches an app on a simulator. ```bash npx reloaderoo@latest inspect call-tool launch_app_sim --params '{"simulatorName": "iPhone 16", "bundleId": "com.example.MyApp"}' -- node build/index.js ``` - **`list_sims`**: Lists available simulators. ```bash npx reloaderoo@latest inspect call-tool list_sims --params '{}' -- node build/index.js ``` - **`open_sim`**: Opens the Simulator application. ```bash npx reloaderoo@latest inspect call-tool open_sim --params '{}' -- node build/index.js ``` - **`stop_app_sim`**: Stops an app on a simulator. ```bash npx reloaderoo@latest inspect call-tool stop_app_sim --params '{"simulatorName": "iPhone 16", "bundleId": "com.example.MyApp"}' -- node build/index.js ``` - **`test_sim`**: Runs tests on a simulator. ```bash npx reloaderoo@latest inspect call-tool test_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -- node build/index.js ``` ### Log Capture & Management - **`start_device_log_cap`**: Starts log capture for a physical device. ```bash npx reloaderoo@latest inspect call-tool start_device_log_cap --params '{"deviceId": "DEVICE_UDID", "bundleId": "com.example.MyApp"}' -- node build/index.js ``` - **`start_sim_log_cap`**: Starts log capture for a simulator. ```bash npx reloaderoo@latest inspect call-tool start_sim_log_cap --params '{"simulatorUuid": "SIMULATOR_UUID", "bundleId": "com.example.MyApp"}' -- node build/index.js ``` - **`stop_device_log_cap`**: Stops log capture for a physical device. ```bash npx reloaderoo@latest inspect call-tool stop_device_log_cap --params '{"logSessionId": "SESSION_ID"}' -- node build/index.js ``` - **`stop_sim_log_cap`**: Stops log capture for a simulator. ```bash npx reloaderoo@latest inspect call-tool stop_sim_log_cap --params '{"logSessionId": "SESSION_ID"}' -- node build/index.js ``` ### macOS Development - **`build_macos`**: Builds a macOS app. ```bash npx reloaderoo@latest inspect call-tool build_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js ``` - **`build_run_macos`**: Builds and runs a macOS app. ```bash npx reloaderoo@latest inspect call-tool build_run_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js ``` - **`get_mac_app_path`**: Gets the `.app` bundle path for a macOS build. ```bash npx reloaderoo@latest inspect call-tool get_mac_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js ``` - **`launch_mac_app`**: Launches a macOS app. ```bash npx reloaderoo@latest inspect call-tool launch_mac_app --params '{"appPath": "/Applications/Calculator.app"}' -- node build/index.js ``` - **`stop_mac_app`**: Stops a macOS app. ```bash npx reloaderoo@latest inspect call-tool stop_mac_app --params '{"appName": "Calculator"}' -- node build/index.js ``` - **`test_macos`**: Runs tests for a macOS project. ```bash npx reloaderoo@latest inspect call-tool test_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js ``` ### Project Discovery - **`discover_projs`**: Discovers Xcode projects and workspaces. ```bash npx reloaderoo@latest inspect call-tool discover_projs --params '{"workspaceRoot": "/path/to/workspace"}' -- node build/index.js ``` - **`get_app_bundle_id`**: Gets an app's bundle identifier. ```bash npx reloaderoo@latest inspect call-tool get_app_bundle_id --params '{"appPath": "/path/to/MyApp.app"}' -- node build/index.js ``` - **`get_mac_bundle_id`**: Gets a macOS app's bundle identifier. ```bash npx reloaderoo@latest inspect call-tool get_mac_bundle_id --params '{"appPath": "/Applications/Calculator.app"}' -- node build/index.js ``` - **`list_schemes`**: Lists schemes in a project or workspace. ```bash npx reloaderoo@latest inspect call-tool list_schemes --params '{"projectPath": "/path/to/MyProject.xcodeproj"}' -- node build/index.js ``` - **`show_build_settings`**: Shows build settings for a scheme. ```bash npx reloaderoo@latest inspect call-tool show_build_settings --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js ``` ### Project Scaffolding - **`scaffold_ios_project`**: Scaffolds a new iOS project. ```bash npx reloaderoo@latest inspect call-tool scaffold_ios_project --params '{"projectName": "MyNewApp", "outputPath": "/path/to/projects"}' -- node build/index.js ``` - **`scaffold_macos_project`**: Scaffolds a new macOS project. ```bash npx reloaderoo@latest inspect call-tool scaffold_macos_project --params '{"projectName": "MyNewMacApp", "outputPath": "/path/to/projects"}' -- node build/index.js ``` ### Project Utilities - **`clean`**: Cleans build artifacts. ```bash # For a project npx reloaderoo@latest inspect call-tool clean --params '{"projectPath": "/path/to/MyProject.xcodeproj"}' -- node build/index.js # For a workspace npx reloaderoo@latest inspect call-tool clean --params '{"workspacePath": "/path/to/MyWorkspace.xcworkspace", "scheme": "MyScheme"}' -- node build/index.js ``` ### Simulator Management - **`reset_sim_location`**: Resets a simulator's location. ```bash npx reloaderoo@latest inspect call-tool reset_sim_location --params '{"simulatorUuid": "SIMULATOR_UUID"}' -- node build/index.js ``` - **`set_sim_appearance`**: Sets a simulator's appearance (dark/light mode). ```bash npx reloaderoo@latest inspect call-tool set_sim_appearance --params '{"simulatorUuid": "SIMULATOR_UUID", "mode": "dark"}' -- node build/index.js ``` - **`set_sim_location`**: Sets a simulator's GPS location. ```bash npx reloaderoo@latest inspect call-tool set_sim_location --params '{"simulatorUuid": "SIMULATOR_UUID", "latitude": 37.7749, "longitude": -122.4194}' -- node build/index.js ``` - **`sim_statusbar`**: Overrides a simulator's status bar. ```bash npx reloaderoo@latest inspect call-tool sim_statusbar --params '{"simulatorUuid": "SIMULATOR_UUID", "dataNetwork": "wifi"}' -- node build/index.js ``` ### Swift Package Manager - **`swift_package_build`**: Builds a Swift package. ```bash npx reloaderoo@latest inspect call-tool swift_package_build --params '{"packagePath": "/path/to/package"}' -- node build/index.js ``` - **`swift_package_clean`**: Cleans a Swift package. ```bash npx reloaderoo@latest inspect call-tool swift_package_clean --params '{"packagePath": "/path/to/package"}' -- node build/index.js ``` - **`swift_package_list`**: Lists running Swift package processes. ```bash npx reloaderoo@latest inspect call-tool swift_package_list --params '{}' -- node build/index.js ``` - **`swift_package_run`**: Runs a Swift package executable. ```bash npx reloaderoo@latest inspect call-tool swift_package_run --params '{"packagePath": "/path/to/package"}' -- node build/index.js ``` - **`swift_package_stop`**: Stops a running Swift package process. ```bash npx reloaderoo@latest inspect call-tool swift_package_stop --params '{"pid": 12345}' -- node build/index.js ``` - **`swift_package_test`**: Tests a Swift package. ```bash npx reloaderoo@latest inspect call-tool swift_package_test --params '{"packagePath": "/path/to/package"}' -- node build/index.js ``` ### System Doctor - **`doctor`**: Runs system diagnostics. ```bash npx reloaderoo@latest inspect call-tool doctor --params '{}' -- node build/index.js ``` ### UI Testing & Automation - **`button`**: Simulates a hardware button press. ```bash npx reloaderoo@latest inspect call-tool button --params '{"simulatorUuid": "SIMULATOR_UUID", "buttonType": "home"}' -- node build/index.js ``` - **`describe_ui`**: Gets the UI hierarchy of the current screen. ```bash npx reloaderoo@latest inspect call-tool describe_ui --params '{"simulatorUuid": "SIMULATOR_UUID"}' -- node build/index.js ``` - **`gesture`**: Performs a pre-defined gesture. ```bash npx reloaderoo@latest inspect call-tool gesture --params '{"simulatorUuid": "SIMULATOR_UUID", "preset": "scroll-up"}' -- node build/index.js ``` - **`key_press`**: Simulates a key press. ```bash npx reloaderoo@latest inspect call-tool key_press --params '{"simulatorUuid": "SIMULATOR_UUID", "keyCode": 40}' -- node build/index.js ``` - **`key_sequence`**: Simulates a sequence of key presses. ```bash npx reloaderoo@latest inspect call-tool key_sequence --params '{"simulatorUuid": "SIMULATOR_UUID", "keyCodes": [40, 42, 44]}' -- node build/index.js ``` - **`long_press`**: Performs a long press at coordinates. ```bash npx reloaderoo@latest inspect call-tool long_press --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200, "duration": 1500}' -- node build/index.js ``` - **`screenshot`**: Takes a screenshot. ```bash npx reloaderoo@latest inspect call-tool screenshot --params '{"simulatorUuid": "SIMULATOR_UUID"}' -- node build/index.js ``` - **`swipe`**: Performs a swipe gesture. ```bash npx reloaderoo@latest inspect call-tool swipe --params '{"simulatorUuid": "SIMULATOR_UUID", "x1": 100, "y1": 200, "x2": 100, "y2": 400}' -- node build/index.js ``` - **`tap`**: Performs a tap at coordinates. ```bash npx reloaderoo@latest inspect call-tool tap --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200}' -- node build/index.js ``` - **`touch`**: Simulates a touch down or up event. ```bash npx reloaderoo@latest inspect call-tool touch --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200, "down": true}' -- node build/index.js ``` - **`type_text`**: Types text into the focused element. ```bash npx reloaderoo@latest inspect call-tool type_text --params '{"simulatorUuid": "SIMULATOR_UUID", "text": "Hello, World!"}' -- node build/index.js ``` ### Resources - **Read devices resource**: ```bash npx reloaderoo@latest inspect read-resource "xcodebuildmcp://devices" -- node build/index.js ``` - **Read simulators resource**: ```bash npx reloaderoo@latest inspect read-resource "xcodebuildmcp://simulators" -- node build/index.js ``` - **Read doctor resource**: ```bash npx reloaderoo@latest inspect read-resource "xcodebuildmcp://doctor" -- node build/index.js ``` ``` -------------------------------------------------------------------------------- /src/mcp/tools/macos/__tests__/build_macos.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for build_macos plugin (unified) * Following CLAUDE.md testing standards with literal validation * Using pure dependency injection for deterministic testing * NO VITEST MOCKING ALLOWED - Only createMockExecutor and createMockFileSystemExecutor */ import { describe, it, expect, beforeEach } from 'vitest'; import { z } from 'zod'; import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import buildMacOS, { buildMacOSLogic } from '../build_macos.ts'; describe('build_macos plugin', () => { beforeEach(() => { sessionStore.clear(); }); describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(buildMacOS.name).toBe('build_macos'); }); it('should have correct description', () => { expect(buildMacOS.description).toBe('Builds a macOS app.'); }); it('should have handler function', () => { expect(typeof buildMacOS.handler).toBe('function'); }); it('should validate schema correctly', () => { const schema = z.object(buildMacOS.schema); expect(schema.safeParse({}).success).toBe(true); expect( schema.safeParse({ derivedDataPath: '/path/to/derived-data', extraArgs: ['--arg1', '--arg2'], preferXcodebuild: true, }).success, ).toBe(true); expect(schema.safeParse({ derivedDataPath: 42 }).success).toBe(false); expect(schema.safeParse({ extraArgs: ['--ok', 1] }).success).toBe(false); expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false); const schemaKeys = Object.keys(buildMacOS.schema).sort(); expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs', 'preferXcodebuild'].sort()); }); }); describe('Handler Requirements', () => { it('should require scheme when no defaults provided', async () => { const result = await buildMacOS.handler({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('scheme is required'); expect(result.content[0].text).toContain('session-set-defaults'); }); it('should require project or workspace once scheme default exists', async () => { sessionStore.setDefaults({ scheme: 'MyScheme' }); const result = await buildMacOS.handler({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Provide a project or workspace'); }); it('should reject when both projectPath and workspacePath provided explicitly', async () => { sessionStore.setDefaults({ scheme: 'MyScheme' }); const result = await buildMacOS.handler({ projectPath: '/path/to/project.xcodeproj', workspacePath: '/path/to/workspace.xcworkspace', }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Mutually exclusive parameters provided'); expect(result.content[0].text).toContain('projectPath'); expect(result.content[0].text).toContain('workspacePath'); }); }); describe('Handler Behavior (Complete Literal Returns)', () => { it('should return exact successful build response', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED', }); const result = await buildMacOSLogic( { projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ macOS Build build succeeded for scheme MyScheme.', }, { type: 'text', text: "Next Steps:\n1. Get app path: get_mac_app_path({ scheme: 'MyScheme' })\n2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })\n3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })", }, ], }); }); it('should return exact build failure response', async () => { const mockExecutor = createMockExecutor({ success: false, error: 'error: Compilation error in main.swift', }); const result = await buildMacOSLogic( { projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: '❌ [stderr] error: Compilation error in main.swift', }, { type: 'text', text: '❌ macOS Build build failed for scheme MyScheme.', }, ], isError: true, }); }); it('should return exact successful build response with optional parameters', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED', }); const result = await buildMacOSLogic( { projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme', configuration: 'Release', arch: 'arm64', derivedDataPath: '/path/to/derived-data', extraArgs: ['--verbose'], preferXcodebuild: true, }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ macOS Build build succeeded for scheme MyScheme.', }, { type: 'text', text: "Next Steps:\n1. Get app path: get_mac_app_path({ scheme: 'MyScheme' })\n2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })\n3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })", }, ], }); }); it('should return exact exception handling response', async () => { // Create executor that throws error during command execution // This will be caught by executeXcodeBuildCommand's try-catch block const mockExecutor = async () => { throw new Error('Network error'); }; const result = await buildMacOSLogic( { projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error during macOS Build build: Network error', }, ], isError: true, }); }); it('should return exact spawn error handling response', async () => { // Create executor that throws spawn error during command execution // This will be caught by executeXcodeBuildCommand's try-catch block const mockExecutor = async () => { throw new Error('Spawn error'); }; const result = await buildMacOSLogic( { projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error during macOS Build build: Spawn error', }, ], isError: true, }); }); }); describe('Command Generation', () => { it('should generate correct xcodebuild command with minimal parameters', async () => { let capturedCommand: string[] = []; const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' }); // Override the executor to capture the command const spyExecutor = async (command: string[]) => { capturedCommand = command; return mockExecutor(command); }; const result = await buildMacOSLogic( { projectPath: '/path/to/project.xcodeproj', scheme: 'MyScheme', }, spyExecutor, ); expect(capturedCommand).toEqual([ 'xcodebuild', '-project', '/path/to/project.xcodeproj', '-scheme', 'MyScheme', '-configuration', 'Debug', '-skipMacroValidation', '-destination', 'platform=macOS', 'build', ]); }); it('should generate correct xcodebuild command with all parameters', async () => { let capturedCommand: string[] = []; const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' }); // Override the executor to capture the command const spyExecutor = async (command: string[]) => { capturedCommand = command; return mockExecutor(command); }; const result = await buildMacOSLogic( { projectPath: '/path/to/project.xcodeproj', scheme: 'MyScheme', configuration: 'Release', arch: 'x86_64', derivedDataPath: '/custom/derived', extraArgs: ['--verbose'], preferXcodebuild: true, }, spyExecutor, ); expect(capturedCommand).toEqual([ 'xcodebuild', '-project', '/path/to/project.xcodeproj', '-scheme', 'MyScheme', '-configuration', 'Release', '-skipMacroValidation', '-destination', 'platform=macOS,arch=x86_64', '-derivedDataPath', '/custom/derived', '--verbose', 'build', ]); }); it('should generate correct xcodebuild command with only derivedDataPath', async () => { let capturedCommand: string[] = []; const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' }); // Override the executor to capture the command const spyExecutor = async (command: string[]) => { capturedCommand = command; return mockExecutor(command); }; const result = await buildMacOSLogic( { projectPath: '/path/to/project.xcodeproj', scheme: 'MyScheme', derivedDataPath: '/custom/derived/data', }, spyExecutor, ); expect(capturedCommand).toEqual([ 'xcodebuild', '-project', '/path/to/project.xcodeproj', '-scheme', 'MyScheme', '-configuration', 'Debug', '-skipMacroValidation', '-destination', 'platform=macOS', '-derivedDataPath', '/custom/derived/data', 'build', ]); }); it('should generate correct xcodebuild command with arm64 architecture only', async () => { let capturedCommand: string[] = []; const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' }); // Override the executor to capture the command const spyExecutor = async (command: string[]) => { capturedCommand = command; return mockExecutor(command); }; const result = await buildMacOSLogic( { projectPath: '/path/to/project.xcodeproj', scheme: 'MyScheme', arch: 'arm64', }, spyExecutor, ); expect(capturedCommand).toEqual([ 'xcodebuild', '-project', '/path/to/project.xcodeproj', '-scheme', 'MyScheme', '-configuration', 'Debug', '-skipMacroValidation', '-destination', 'platform=macOS,arch=arm64', 'build', ]); }); it('should handle paths with spaces in command generation', async () => { let capturedCommand: string[] = []; const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' }); // Override the executor to capture the command const spyExecutor = async (command: string[]) => { capturedCommand = command; return mockExecutor(command); }; const result = await buildMacOSLogic( { projectPath: '/Users/dev/My Project/MyProject.xcodeproj', scheme: 'MyScheme', }, spyExecutor, ); expect(capturedCommand).toEqual([ 'xcodebuild', '-project', '/Users/dev/My Project/MyProject.xcodeproj', '-scheme', 'MyScheme', '-configuration', 'Debug', '-skipMacroValidation', '-destination', 'platform=macOS', 'build', ]); }); it('should generate correct xcodebuild workspace command with minimal parameters', async () => { let capturedCommand: string[] = []; const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' }); // Override the executor to capture the command const spyExecutor = async (command: string[]) => { capturedCommand = command; return mockExecutor(command); }; const result = await buildMacOSLogic( { workspacePath: '/path/to/workspace.xcworkspace', scheme: 'MyScheme', }, spyExecutor, ); expect(capturedCommand).toEqual([ 'xcodebuild', '-workspace', '/path/to/workspace.xcworkspace', '-scheme', 'MyScheme', '-configuration', 'Debug', '-skipMacroValidation', '-destination', 'platform=macOS', 'build', ]); }); }); describe('XOR Validation', () => { it('should error when neither projectPath nor workspacePath provided', async () => { const result = await buildMacOS.handler({ scheme: 'MyScheme' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Provide a project or workspace'); }); it('should error when both projectPath and workspacePath provided', async () => { const result = await buildMacOS.handler({ projectPath: '/path/to/project.xcodeproj', workspacePath: '/path/to/workspace.xcworkspace', scheme: 'MyScheme', }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Mutually exclusive parameters provided'); }); it('should succeed with valid projectPath', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED', }); const result = await buildMacOSLogic( { projectPath: '/path/to/project.xcodeproj', scheme: 'MyScheme', }, mockExecutor, ); expect(result.isError).toBeUndefined(); }); it('should succeed with valid workspacePath', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED', }); const result = await buildMacOSLogic( { workspacePath: '/path/to/workspace.xcworkspace', scheme: 'MyScheme', }, mockExecutor, ); expect(result.isError).toBeUndefined(); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/discovery/discover_tools.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { createTextResponse } from '../../../utils/responses/index.ts'; import { log } from '../../../utils/logging/index.ts'; // Removed CreateMessageResultSchema import as it's no longer used import { ToolResponse } from '../../../types/common.ts'; import { enableWorkflows, getAvailableWorkflows, generateWorkflowDescriptions, } from '../../../core/dynamic-tools.ts'; import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; import { McpServer } from '@camsoft/mcp-sdk/server/mcp.js'; // Using McpServer type from SDK instead of custom interface // Configuration for LLM parameters - made configurable instead of hardcoded interface LLMConfig { maxTokens: number; temperature?: number; } // Default LLM configuration with environment variable overrides const getLLMConfig = (): LLMConfig => { let maxTokens = 200; // default if (process.env.XCODEBUILDMCP_LLM_MAX_TOKENS) { const parsed = parseInt(process.env.XCODEBUILDMCP_LLM_MAX_TOKENS, 10); if (!isNaN(parsed) && parsed > 0) { maxTokens = parsed; } } let temperature: number | undefined; if (process.env.XCODEBUILDMCP_LLM_TEMPERATURE) { const parsed = parseFloat(process.env.XCODEBUILDMCP_LLM_TEMPERATURE); if (!isNaN(parsed) && parsed >= 0 && parsed <= 2) { temperature = parsed; } } return { maxTokens, temperature, }; }; /** * Sanitizes user input to prevent injection attacks and ensure safe LLM usage * @param input The raw user input to sanitize * @returns Sanitized input safe for LLM processing */ function sanitizeTaskDescription(input: string): string { if (!input || typeof input !== 'string') { throw new Error('Task description must be a non-empty string'); } // Remove control characters and normalize whitespace let sanitized = input // eslint-disable-next-line no-control-regex -- Intentional control character removal for security .replace(/[\x00-\x1F\x7F-\x9F]/g, '') // Remove control characters .replace(/\s+/g, ' ') // Normalize whitespace .trim(); // Length validation - prevent excessively long inputs if (sanitized.length === 0) { throw new Error('Task description cannot be empty after sanitization'); } if (sanitized.length > 2000) { sanitized = sanitized.substring(0, 2000); log('warn', 'Task description truncated to 2000 characters for safety'); } // Basic injection prevention - remove potential prompt injection patterns const suspiciousPatterns = [ /ignore\s+previous\s+instructions/gi, /forget\s+everything/gi, /system\s*:/gi, /assistant\s*:/gi, /you\s+are\s+now/gi, /act\s+as/gi, ]; for (const pattern of suspiciousPatterns) { if (pattern.test(sanitized)) { log('warn', 'Potentially suspicious pattern detected in task description'); sanitized = sanitized.replace(pattern, '[filtered]'); } } return sanitized; } // Define schema as ZodObject const discoverToolsSchema = z.object({ task_description: z .string() .describe( 'A detailed description of the development task you want to accomplish. ' + "For example: 'I need to build my iOS app and run it on the iPhone 16 simulator.' " + 'If working with Xcode projects, explicitly state whether you are using a .xcworkspace (workspace) or a .xcodeproj (project).', ), additive: z .boolean() .optional() .describe( 'If true, add the discovered tools to existing enabled workflows. ' + 'If false (default), replace all existing workflows with the newly discovered one. ' + 'Use additive mode when you need tools from multiple workflows simultaneously.', ), }); // Use z.infer for type safety type DiscoverToolsParams = z.infer<typeof discoverToolsSchema>; // Dependencies interface for dependency injection interface Dependencies { getAvailableWorkflows?: () => string[]; generateWorkflowDescriptions?: () => string; enableWorkflows?: (server: McpServer, workflows: string[], additive?: boolean) => Promise<void>; } export async function discover_toolsLogic( args: DiscoverToolsParams, _executor?: unknown, deps?: Dependencies, ): Promise<ToolResponse> { // Enhanced null safety checks if (!args || typeof args !== 'object') { return createTextResponse('Invalid arguments provided to discover_tools', true); } const { task_description, additive } = args; // Sanitize the task description to prevent injection attacks let sanitizedTaskDescription: string; try { sanitizedTaskDescription = sanitizeTaskDescription(task_description); log('info', `Discovering tools for task: ${sanitizedTaskDescription}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Invalid task description'; log('error', `Task description sanitization failed: ${errorMessage}`); return createTextResponse(`Invalid task description: ${errorMessage}`, true); } try { // Get the server instance from the global context const server = (globalThis as { mcpServer?: McpServer }).mcpServer; if (!server) { throw new Error('Server instance not available'); } // 1. Check for sampling capability const clientCapabilities = server.server?.getClientCapabilities?.(); if (!clientCapabilities?.sampling) { log('warn', 'Client does not support sampling capability'); return createTextResponse( 'Your client does not support the sampling feature required for dynamic tool discovery. ' + 'Please use XCODEBUILDMCP_DYNAMIC_TOOLS=false to use the standard tool set.', true, ); } // 2. Get available workflows using generated metadata const workflowNames = (deps?.getAvailableWorkflows ?? getAvailableWorkflows)(); const workflowDescriptions = ( deps?.generateWorkflowDescriptions ?? generateWorkflowDescriptions )(); // 3. Construct the prompt for the LLM using sanitized input const userPrompt = `You are an expert assistant for the XcodeBuildMCP server. Your task is to select the most relevant workflow for a user's Apple development request. The user wants to perform the following task: "${sanitizedTaskDescription}" IMPORTANT: Select EXACTLY ONE workflow that best matches the user's task. In most cases, users are working with a project or workspace. Use this selection guide: Primary (project/workspace-based) workflows: - iOS simulator (supports both .xcworkspace and .xcodeproj): choose "simulator" - iOS physical device (supports both .xcworkspace and .xcodeproj): choose "device" - macOS (supports both .xcworkspace and .xcodeproj): choose "macos" - Swift Package Manager (no Xcode project): choose "swift-package" Secondary (task-based, no project/workspace needed): - Simulator management (boot, list, open, status bar, appearance, GPS/location): choose "simulator-management" - Logging or log capture (simulator or device): choose "logging" - UI automation/gestures/screenshots on a simulator app: choose "ui-testing" - System/environment diagnostics or validation: choose "doctor" - Create new iOS/macOS projects from templates: choose "project-scaffolding" - Project discovery and analysis: choose "project-discovery" - General utilities: choose "utilities" All available workflows: ${workflowDescriptions} Respond with ONLY a JSON array containing ONE workflow name that best matches the task (e.g., ["simulator"]).`; // 4. Send sampling request with configurable parameters const llmConfig = getLLMConfig(); log('debug', `Sending sampling request to client LLM with maxTokens: ${llmConfig.maxTokens}`); if (!server.server?.createMessage) { throw new Error('Server does not support message creation'); } const samplingOptions: { messages: Array<{ role: 'user'; content: { type: 'text'; text: string } }>; maxTokens: number; temperature?: number; } = { messages: [{ role: 'user', content: { type: 'text', text: userPrompt } }], maxTokens: llmConfig.maxTokens, }; // Only add temperature if configured if (llmConfig.temperature !== undefined) { samplingOptions.temperature = llmConfig.temperature; } const samplingResult = await server.server.createMessage(samplingOptions); // 5. Parse the response with enhanced null safety checks let selectedWorkflows: string[] = []; try { // Enhanced null safety - check if samplingResult exists and has expected structure if (!samplingResult || typeof samplingResult !== 'object') { throw new Error('Invalid sampling result: null or not an object'); } const content = ( samplingResult as { content?: Array<{ type: 'text'; text: string }> | { type: 'text'; text: string } | null; } ).content; if (!content) { throw new Error('No content in sampling response'); } let responseText = ''; // Handle both array and single object content formats with enhanced null checks if (Array.isArray(content)) { if (content.length === 0) { throw new Error('Empty content array in sampling response'); } const firstItem = content[0]; if (!firstItem || typeof firstItem !== 'object' || firstItem.type !== 'text') { throw new Error('Invalid first content item in array'); } if (!firstItem.text || typeof firstItem.text !== 'string') { throw new Error('Invalid text content in first array item'); } responseText = firstItem.text.trim(); } else if ( content && typeof content === 'object' && 'type' in content && content.type === 'text' && 'text' in content && typeof content.text === 'string' ) { responseText = content.text.trim(); } else { throw new Error('Invalid content format in sampling response'); } if (!responseText) { throw new Error('Empty response text after parsing'); } log('debug', `LLM response: ${responseText}`); const parsedResponse: unknown = JSON.parse(responseText); if (!Array.isArray(parsedResponse)) { throw new Error('Response is not an array'); } // Validate that all items are strings if (!parsedResponse.every((item): item is string => typeof item === 'string')) { throw new Error('Response array contains non-string items'); } selectedWorkflows = parsedResponse; // Validate that all selected workflows are valid const validWorkflows = selectedWorkflows.filter((workflow) => workflowNames.includes(workflow), ); if (validWorkflows.length !== selectedWorkflows.length) { const invalidWorkflows = selectedWorkflows.filter( (workflow) => !workflowNames.includes(workflow), ); log('warn', `LLM selected invalid workflows: ${invalidWorkflows.join(', ')}`); selectedWorkflows = validWorkflows; } } catch (error) { log('error', `Failed to parse LLM response: ${error}`); // Extract the response text for error reporting with enhanced null safety let errorResponseText = 'Unknown response format'; try { if (samplingResult && typeof samplingResult === 'object') { const content = ( samplingResult as { content?: | Array<{ type: 'text'; text: string }> | { type: 'text'; text: string } | null; } ).content; if (content && Array.isArray(content) && content.length > 0) { const firstItem = content[0]; if ( firstItem && typeof firstItem === 'object' && firstItem.type === 'text' && typeof firstItem.text === 'string' ) { errorResponseText = firstItem.text; } } else if ( content && typeof content === 'object' && 'type' in content && content.type === 'text' && 'text' in content && typeof content.text === 'string' ) { errorResponseText = content.text; } } } catch { // Keep default error message } return createTextResponse( `I was unable to determine the right tools for your task. The AI model returned: "${errorResponseText}". ` + `Could you please rephrase your request or try a more specific description?`, true, ); } // 6. Handle empty selection if (selectedWorkflows.length === 0) { log('info', 'LLM returned empty workflow selection'); return createTextResponse( "No specific Xcode tools seem necessary for that task. Could you provide more details about what you'd like to accomplish with Xcode?", ); } // 7. Enable the selected workflows const isAdditive = Boolean(additive); log( 'info', `${isAdditive ? 'Adding' : 'Replacing with'} workflows: ${selectedWorkflows.join(', ')}`, ); await (deps?.enableWorkflows ?? enableWorkflows)(server, selectedWorkflows, isAdditive); // 8. Return success response - we can't easily get tool count ahead of time with dynamic loading // but that's okay since the user will see the tools when they're loaded const actionWord = isAdditive ? 'Added' : 'Enabled'; const modeDescription = isAdditive ? `Added tools from ${selectedWorkflows.join(', ')} to your existing workflow tools.` : `Replaced previous tools with ${selectedWorkflows.join(', ')} workflow tools.`; return createTextResponse( `✅ ${actionWord} XcodeBuildMCP tools for: ${selectedWorkflows.join(', ')}.\n\n` + `${modeDescription}\n\n` + `Use XcodeBuildMCP tools for all Apple platform development tasks from now on. ` + `Call tools/list to see all available tools for your workflow.`, ); } catch (error) { log('error', `Error in discoverTools: ${error}`); return createTextResponse( `An error occurred while discovering tools: ${error instanceof Error ? error.message : 'Unknown error'}`, true, ); } } export default { name: 'discover_tools', description: 'Analyzes a natural language task description and enables the most relevant development workflow. Prioritizes project/workspace workflows (simulator/device/macOS) and also supports task-based workflows (simulator-management, logging) and Swift packages.', schema: discoverToolsSchema.shape, // MCP SDK compatibility handler: createTypedTool( discoverToolsSchema, (params: DiscoverToolsParams, executor) => { return discover_toolsLogic(params, executor); }, getDefaultCommandExecutor, ), }; ``` -------------------------------------------------------------------------------- /src/mcp/tools/macos/__tests__/build_run_macos.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect, beforeEach } from 'vitest'; import { z } from 'zod'; import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import tool, { buildRunMacOSLogic } from '../build_run_macos.ts'; describe('build_run_macos', () => { beforeEach(() => { sessionStore.clear(); }); describe('Export Field Validation (Literal)', () => { it('should export the correct name', () => { expect(tool.name).toBe('build_run_macos'); }); it('should export the correct description', () => { expect(tool.description).toBe('Builds and runs a macOS app.'); }); it('should export a handler function', () => { expect(typeof tool.handler).toBe('function'); }); it('should expose only non-session fields in schema', () => { const schema = z.object(tool.schema); expect(schema.safeParse({}).success).toBe(true); expect( schema.safeParse({ derivedDataPath: '/tmp/derived', extraArgs: ['--verbose'], preferXcodebuild: true, }).success, ).toBe(true); expect(schema.safeParse({ derivedDataPath: 1 }).success).toBe(false); expect(schema.safeParse({ extraArgs: ['--ok', 2] }).success).toBe(false); expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false); const schemaKeys = Object.keys(tool.schema).sort(); expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs', 'preferXcodebuild'].sort()); }); }); describe('Handler Requirements', () => { it('should require scheme before executing', async () => { const result = await tool.handler({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('scheme is required'); }); it('should require project or workspace once scheme is set', async () => { sessionStore.setDefaults({ scheme: 'MyApp' }); const result = await tool.handler({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Provide a project or workspace'); }); it('should fail when both project and workspace provided explicitly', async () => { sessionStore.setDefaults({ scheme: 'MyApp' }); const result = await tool.handler({ projectPath: '/path/to/project.xcodeproj', workspacePath: '/path/to/workspace.xcworkspace', }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Mutually exclusive parameters provided'); }); }); describe('Command Generation and Response Logic', () => { it('should successfully build and run macOS app from project', async () => { // Track executor calls manually let callCount = 0; const executorCalls: any[] = []; const mockExecutor = ( command: string[], description: string, logOutput: boolean, timeout?: number, ) => { callCount++; executorCalls.push({ command, description, logOutput, timeout }); if (callCount === 1) { // First call for build return Promise.resolve({ success: true, output: 'BUILD SUCCEEDED', error: '', }); } else if (callCount === 2) { // Second call for build settings return Promise.resolve({ success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: '', }); } return Promise.resolve({ success: true, output: '', error: '' }); }; const args = { projectPath: '/path/to/project.xcodeproj', scheme: 'MyApp', configuration: 'Debug', preferXcodebuild: false, }; const result = await buildRunMacOSLogic(args, mockExecutor); // Verify build command was called expect(executorCalls[0]).toEqual({ command: [ 'xcodebuild', '-project', '/path/to/project.xcodeproj', '-scheme', 'MyApp', '-configuration', 'Debug', '-skipMacroValidation', '-destination', 'platform=macOS', 'build', ], description: 'macOS Build', logOutput: true, timeout: undefined, }); // Verify build settings command was called expect(executorCalls[1]).toEqual({ command: [ 'xcodebuild', '-showBuildSettings', '-project', '/path/to/project.xcodeproj', '-scheme', 'MyApp', '-configuration', 'Debug', ], description: 'Get Build Settings for Launch', logOutput: true, timeout: undefined, }); expect(result).toEqual({ content: [ { type: 'text', text: '✅ macOS Build build succeeded for scheme MyApp.', }, { type: 'text', text: "Next Steps:\n1. Get app path: get_mac_app_path({ scheme: 'MyApp' })\n2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })\n3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })", }, { type: 'text', text: '✅ macOS build and run succeeded for scheme MyApp. App launched: /path/to/build/MyApp.app', }, ], isError: false, }); }); it('should successfully build and run macOS app from workspace', async () => { // Track executor calls manually let callCount = 0; const executorCalls: any[] = []; const mockExecutor = ( command: string[], description: string, logOutput: boolean, timeout?: number, ) => { callCount++; executorCalls.push({ command, description, logOutput, timeout }); if (callCount === 1) { // First call for build return Promise.resolve({ success: true, output: 'BUILD SUCCEEDED', error: '', }); } else if (callCount === 2) { // Second call for build settings return Promise.resolve({ success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: '', }); } return Promise.resolve({ success: true, output: '', error: '' }); }; const args = { workspacePath: '/path/to/workspace.xcworkspace', scheme: 'MyApp', configuration: 'Debug', preferXcodebuild: false, }; const result = await buildRunMacOSLogic(args, mockExecutor); // Verify build command was called expect(executorCalls[0]).toEqual({ command: [ 'xcodebuild', '-workspace', '/path/to/workspace.xcworkspace', '-scheme', 'MyApp', '-configuration', 'Debug', '-skipMacroValidation', '-destination', 'platform=macOS', 'build', ], description: 'macOS Build', logOutput: true, timeout: undefined, }); // Verify build settings command was called expect(executorCalls[1]).toEqual({ command: [ 'xcodebuild', '-showBuildSettings', '-workspace', '/path/to/workspace.xcworkspace', '-scheme', 'MyApp', '-configuration', 'Debug', ], description: 'Get Build Settings for Launch', logOutput: true, timeout: undefined, }); expect(result).toEqual({ content: [ { type: 'text', text: '✅ macOS Build build succeeded for scheme MyApp.', }, { type: 'text', text: "Next Steps:\n1. Get app path: get_mac_app_path({ scheme: 'MyApp' })\n2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })\n3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })", }, { type: 'text', text: '✅ macOS build and run succeeded for scheme MyApp. App launched: /path/to/build/MyApp.app', }, ], isError: false, }); }); it('should handle build failure', async () => { const mockExecutor = createMockExecutor({ success: false, output: '', error: 'error: Build failed', }); const args = { projectPath: '/path/to/project.xcodeproj', scheme: 'MyApp', configuration: 'Debug', preferXcodebuild: false, }; const result = await buildRunMacOSLogic(args, mockExecutor); expect(result).toEqual({ content: [ { type: 'text', text: '❌ [stderr] error: Build failed' }, { type: 'text', text: '❌ macOS Build build failed for scheme MyApp.' }, ], isError: true, }); }); it('should handle build settings failure', async () => { // Track executor calls manually let callCount = 0; const mockExecutor = ( command: string[], description: string, logOutput: boolean, timeout?: number, ) => { callCount++; if (callCount === 1) { // First call for build succeeds return Promise.resolve({ success: true, output: 'BUILD SUCCEEDED', error: '', }); } else if (callCount === 2) { // Second call for build settings fails return Promise.resolve({ success: false, output: '', error: 'error: Failed to get settings', }); } return Promise.resolve({ success: true, output: '', error: '' }); }; const args = { projectPath: '/path/to/project.xcodeproj', scheme: 'MyApp', configuration: 'Debug', preferXcodebuild: false, }; const result = await buildRunMacOSLogic(args, mockExecutor); expect(result).toEqual({ content: [ { type: 'text', text: '✅ macOS Build build succeeded for scheme MyApp.', }, { type: 'text', text: "Next Steps:\n1. Get app path: get_mac_app_path({ scheme: 'MyApp' })\n2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })\n3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })", }, { type: 'text', text: '✅ Build succeeded, but failed to get app path to launch: error: Failed to get settings', }, ], isError: false, }); }); it('should handle app launch failure', async () => { // Track executor calls manually let callCount = 0; const mockExecutor = ( command: string[], description: string, logOutput: boolean, timeout?: number, ) => { callCount++; if (callCount === 1) { // First call for build succeeds return Promise.resolve({ success: true, output: 'BUILD SUCCEEDED', error: '', }); } else if (callCount === 2) { // Second call for build settings succeeds return Promise.resolve({ success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: '', }); } else if (callCount === 3) { // Third call for open command fails return Promise.resolve({ success: false, output: '', error: 'Failed to launch', }); } return Promise.resolve({ success: true, output: '', error: '' }); }; const args = { projectPath: '/path/to/project.xcodeproj', scheme: 'MyApp', configuration: 'Debug', preferXcodebuild: false, }; const result = await buildRunMacOSLogic(args, mockExecutor); expect(result).toEqual({ content: [ { type: 'text', text: '✅ macOS Build build succeeded for scheme MyApp.', }, { type: 'text', text: "Next Steps:\n1. Get app path: get_mac_app_path({ scheme: 'MyApp' })\n2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })\n3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })", }, { type: 'text', text: '✅ Build succeeded, but failed to launch app /path/to/build/MyApp.app. Error: Failed to launch', }, ], isError: false, }); }); it('should handle spawn error', async () => { const mockExecutor = ( command: string[], description: string, logOutput: boolean, timeout?: number, ) => { return Promise.reject(new Error('spawn xcodebuild ENOENT')); }; const args = { projectPath: '/path/to/project.xcodeproj', scheme: 'MyApp', configuration: 'Debug', preferXcodebuild: false, }; const result = await buildRunMacOSLogic(args, mockExecutor); expect(result).toEqual({ content: [ { type: 'text', text: 'Error during macOS Build build: spawn xcodebuild ENOENT' }, ], isError: true, }); }); it('should use default configuration when not provided', async () => { // Track executor calls manually let callCount = 0; const executorCalls: any[] = []; const mockExecutor = ( command: string[], description: string, logOutput: boolean, timeout?: number, ) => { callCount++; executorCalls.push({ command, description, logOutput, timeout }); if (callCount === 1) { // First call for build return Promise.resolve({ success: true, output: 'BUILD SUCCEEDED', error: '', }); } else if (callCount === 2) { // Second call for build settings return Promise.resolve({ success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: '', }); } return Promise.resolve({ success: true, output: '', error: '' }); }; const args = { projectPath: '/path/to/project.xcodeproj', scheme: 'MyApp', configuration: 'Debug', preferXcodebuild: false, }; await buildRunMacOSLogic(args, mockExecutor); expect(executorCalls[0]).toEqual({ command: [ 'xcodebuild', '-project', '/path/to/project.xcodeproj', '-scheme', 'MyApp', '-configuration', 'Debug', '-skipMacroValidation', '-destination', 'platform=macOS', 'build', ], description: 'macOS Build', logOutput: true, timeout: undefined, }); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/macos/__tests__/get_mac_app_path.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for get_mac_app_path plugin (unified project/workspace) * Following CLAUDE.md testing standards with literal validation * Using dependency injection for deterministic testing */ import { describe, it, expect, beforeEach } from 'vitest'; import { z } from 'zod'; import { createMockExecutor, type CommandExecutor } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import getMacAppPath, { get_mac_app_pathLogic } from '../get_mac_app_path.ts'; describe('get_mac_app_path plugin', () => { beforeEach(() => { sessionStore.clear(); }); describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(getMacAppPath.name).toBe('get_mac_app_path'); }); it('should have correct description', () => { expect(getMacAppPath.description).toBe('Retrieves the built macOS app bundle path.'); }); it('should have handler function', () => { expect(typeof getMacAppPath.handler).toBe('function'); }); it('should validate schema correctly', () => { const schema = z.object(getMacAppPath.schema); expect(schema.safeParse({}).success).toBe(true); expect( schema.safeParse({ derivedDataPath: '/path/to/derived', extraArgs: ['--verbose'], }).success, ).toBe(true); expect(schema.safeParse({ derivedDataPath: 7 }).success).toBe(false); expect(schema.safeParse({ extraArgs: ['--bad', 1] }).success).toBe(false); const schemaKeys = Object.keys(getMacAppPath.schema).sort(); expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs'].sort()); }); }); describe('Handler Requirements', () => { it('should require scheme before running', async () => { const result = await getMacAppPath.handler({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('scheme is required'); }); it('should require project or workspace when scheme default exists', async () => { sessionStore.setDefaults({ scheme: 'MyScheme' }); const result = await getMacAppPath.handler({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Provide a project or workspace'); }); it('should reject when both projectPath and workspacePath provided explicitly', async () => { sessionStore.setDefaults({ scheme: 'MyScheme' }); const result = await getMacAppPath.handler({ projectPath: '/path/to/project.xcodeproj', workspacePath: '/path/to/workspace.xcworkspace', }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Mutually exclusive parameters provided'); }); }); describe('XOR Validation', () => { it('should error when neither projectPath nor workspacePath provided', async () => { const result = await getMacAppPath.handler({ scheme: 'MyScheme', }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Provide a project or workspace'); }); it('should error when both projectPath and workspacePath provided', async () => { const result = await getMacAppPath.handler({ projectPath: '/path/to/project.xcodeproj', workspacePath: '/path/to/workspace.xcworkspace', scheme: 'MyScheme', }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Mutually exclusive parameters provided'); }); }); describe('Command Generation', () => { it('should generate correct command with workspace minimal parameters', async () => { // Manual call tracking for command verification const calls: any[] = []; const mockExecutor: CommandExecutor = async (...args) => { calls.push(args); return { success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: undefined, process: { pid: 12345 }, }; }; const args = { workspacePath: '/path/to/MyProject.xcworkspace', scheme: 'MyScheme', }; await get_mac_app_pathLogic(args, mockExecutor); // Verify command generation with manual call tracking expect(calls).toHaveLength(1); expect(calls[0]).toEqual([ [ 'xcodebuild', '-showBuildSettings', '-workspace', '/path/to/MyProject.xcworkspace', '-scheme', 'MyScheme', '-configuration', 'Debug', ], 'Get App Path', true, undefined, ]); }); it('should generate correct command with project minimal parameters', async () => { // Manual call tracking for command verification const calls: any[] = []; const mockExecutor: CommandExecutor = async (...args) => { calls.push(args); return { success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: undefined, process: { pid: 12345 }, }; }; const args = { projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme', }; await get_mac_app_pathLogic(args, mockExecutor); // Verify command generation with manual call tracking expect(calls).toHaveLength(1); expect(calls[0]).toEqual([ [ 'xcodebuild', '-showBuildSettings', '-project', '/path/to/MyProject.xcodeproj', '-scheme', 'MyScheme', '-configuration', 'Debug', ], 'Get App Path', true, undefined, ]); }); it('should generate correct command with workspace all parameters', async () => { // Manual call tracking for command verification const calls: any[] = []; const mockExecutor: CommandExecutor = async (...args) => { calls.push(args); return { success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: undefined, process: { pid: 12345 }, }; }; const args = { workspacePath: '/path/to/MyProject.xcworkspace', scheme: 'MyScheme', configuration: 'Release', arch: 'arm64', }; await get_mac_app_pathLogic(args, mockExecutor); // Verify command generation with manual call tracking expect(calls).toHaveLength(1); expect(calls[0]).toEqual([ [ 'xcodebuild', '-showBuildSettings', '-workspace', '/path/to/MyProject.xcworkspace', '-scheme', 'MyScheme', '-configuration', 'Release', '-destination', 'platform=macOS,arch=arm64', ], 'Get App Path', true, undefined, ]); }); it('should generate correct command with x86_64 architecture', async () => { // Manual call tracking for command verification const calls: any[] = []; const mockExecutor: CommandExecutor = async (...args) => { calls.push(args); return { success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: undefined, process: { pid: 12345 }, }; }; const args = { workspacePath: '/path/to/MyProject.xcworkspace', scheme: 'MyScheme', configuration: 'Debug', arch: 'x86_64', }; await get_mac_app_pathLogic(args, mockExecutor); // Verify command generation with manual call tracking expect(calls).toHaveLength(1); expect(calls[0]).toEqual([ [ 'xcodebuild', '-showBuildSettings', '-workspace', '/path/to/MyProject.xcworkspace', '-scheme', 'MyScheme', '-configuration', 'Debug', '-destination', 'platform=macOS,arch=x86_64', ], 'Get App Path', true, undefined, ]); }); it('should generate correct command with project all parameters', async () => { // Manual call tracking for command verification const calls: any[] = []; const mockExecutor: CommandExecutor = async (...args) => { calls.push(args); return { success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: undefined, process: { pid: 12345 }, }; }; const args = { projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme', configuration: 'Release', derivedDataPath: '/path/to/derived', extraArgs: ['--verbose'], }; await get_mac_app_pathLogic(args, mockExecutor); // Verify command generation with manual call tracking expect(calls).toHaveLength(1); expect(calls[0]).toEqual([ [ 'xcodebuild', '-showBuildSettings', '-project', '/path/to/MyProject.xcodeproj', '-scheme', 'MyScheme', '-configuration', 'Release', '-derivedDataPath', '/path/to/derived', '--verbose', ], 'Get App Path', true, undefined, ]); }); it('should use default configuration when not provided', async () => { // Manual call tracking for command verification const calls: any[] = []; const mockExecutor: CommandExecutor = async (...args) => { calls.push(args); return { success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: undefined, process: { pid: 12345 }, }; }; const args = { workspacePath: '/path/to/MyProject.xcworkspace', scheme: 'MyScheme', arch: 'arm64', }; await get_mac_app_pathLogic(args, mockExecutor); // Verify command generation with manual call tracking expect(calls).toHaveLength(1); expect(calls[0]).toEqual([ [ 'xcodebuild', '-showBuildSettings', '-workspace', '/path/to/MyProject.xcworkspace', '-scheme', 'MyScheme', '-configuration', 'Debug', '-destination', 'platform=macOS,arch=arm64', ], 'Get App Path', true, undefined, ]); }); }); describe('Handler Behavior (Complete Literal Returns)', () => { it('should return Zod validation error for missing scheme', async () => { const result = await getMacAppPath.handler({ workspacePath: '/path/to/MyProject.xcworkspace', }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('scheme is required'); expect(result.content[0].text).toContain('session-set-defaults'); }); it('should return exact successful app path response with workspace', async () => { const mockExecutor = createMockExecutor({ success: true, output: ` BUILT_PRODUCTS_DIR = /Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug FULL_PRODUCT_NAME = MyApp.app `, }); const result = await get_mac_app_pathLogic( { workspacePath: '/path/to/MyProject.xcworkspace', scheme: 'MyScheme', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ App path retrieved successfully: /Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug/MyApp.app', }, { type: 'text', text: 'Next Steps:\n1. Get bundle ID: get_app_bundle_id({ appPath: "/Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug/MyApp.app" })\n2. Launch app: launch_mac_app({ appPath: "/Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug/MyApp.app" })', }, ], }); }); it('should return exact successful app path response with project', async () => { const mockExecutor = createMockExecutor({ success: true, output: ` BUILT_PRODUCTS_DIR = /Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug FULL_PRODUCT_NAME = MyApp.app `, }); const result = await get_mac_app_pathLogic( { projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ App path retrieved successfully: /Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug/MyApp.app', }, { type: 'text', text: 'Next Steps:\n1. Get bundle ID: get_app_bundle_id({ appPath: "/Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug/MyApp.app" })\n2. Launch app: launch_mac_app({ appPath: "/Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug/MyApp.app" })', }, ], }); }); it('should return exact build settings failure response', async () => { const mockExecutor = createMockExecutor({ success: false, error: 'error: No such scheme', }); const result = await get_mac_app_pathLogic( { workspacePath: '/path/to/MyProject.xcworkspace', scheme: 'MyScheme', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: Failed to get macOS app path\nDetails: error: No such scheme', }, ], isError: true, }); }); it('should return exact missing build settings response', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'OTHER_SETTING = value', }); const result = await get_mac_app_pathLogic( { workspacePath: '/path/to/MyProject.xcworkspace', scheme: 'MyScheme', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: Failed to get macOS app path\nDetails: Could not extract app path from build settings', }, ], isError: true, }); }); it('should return exact exception handling response', async () => { const mockExecutor = async () => { throw new Error('Network error'); }; const result = await get_mac_app_pathLogic( { workspacePath: '/path/to/MyProject.xcworkspace', scheme: 'MyScheme', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: Failed to get macOS app path\nDetails: Network error', }, ], isError: true, }); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/ui-testing/__tests__/swipe.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for swipe tool plugin */ import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts'; import { SystemError, DependencyError } from '../../../../utils/responses/index.ts'; // Import the plugin module to test import swipePlugin, { AxeHelpers, swipeLogic, SwipeParams } from '../swipe.ts'; // Helper function to create mock axe helpers function createMockAxeHelpers(): AxeHelpers { return { getAxePath: () => '/mocked/axe/path', getBundledAxeEnvironment: () => ({ SOME_ENV: 'value' }), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; } // Helper function to create mock axe helpers with null path (for dependency error tests) function createMockAxeHelpersWithNullPath(): AxeHelpers { return { getAxePath: () => null, getBundledAxeEnvironment: () => ({ SOME_ENV: 'value' }), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; } describe('Swipe Plugin', () => { describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(swipePlugin.name).toBe('swipe'); }); it('should have correct description', () => { expect(swipePlugin.description).toBe( "Swipe from one point to another. Use describe_ui for precise coordinates (don't guess from screenshots). Supports configurable timing.", ); }); it('should have handler function', () => { expect(typeof swipePlugin.handler).toBe('function'); }); it('should validate schema fields with safeParse', () => { const schema = z.object(swipePlugin.schema); // Valid case expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, y2: 400, }).success, ).toBe(true); // Invalid simulatorUuid expect( schema.safeParse({ simulatorUuid: 'invalid-uuid', x1: 100, y1: 200, x2: 300, y2: 400, }).success, ).toBe(false); // Invalid x1 (not integer) expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', x1: 100.5, y1: 200, x2: 300, y2: 400, }).success, ).toBe(false); // Valid with optional parameters expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, y2: 400, duration: 1.5, delta: 10, preDelay: 0.5, postDelay: 0.2, }).success, ).toBe(true); // Invalid duration (negative) expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, y2: 400, duration: -1, }).success, ).toBe(false); }); }); describe('Command Generation', () => { it('should generate correct axe command for basic swipe', async () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; return { success: true, output: 'swipe completed', error: undefined, process: { pid: 12345 }, }; }; const mockAxeHelpers = createMockAxeHelpers(); await swipeLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, y2: 400, }, trackingExecutor, mockAxeHelpers, ); expect(capturedCommand).toEqual([ '/mocked/axe/path', 'swipe', '--start-x', '100', '--start-y', '200', '--end-x', '300', '--end-y', '400', '--udid', '12345678-1234-1234-1234-123456789012', ]); }); it('should generate correct axe command for swipe with duration', async () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; return { success: true, output: 'swipe completed', error: undefined, process: { pid: 12345 }, }; }; const mockAxeHelpers = createMockAxeHelpers(); await swipeLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', x1: 50, y1: 75, x2: 250, y2: 350, duration: 1.5, }, trackingExecutor, mockAxeHelpers, ); expect(capturedCommand).toEqual([ '/mocked/axe/path', 'swipe', '--start-x', '50', '--start-y', '75', '--end-x', '250', '--end-y', '350', '--duration', '1.5', '--udid', '12345678-1234-1234-1234-123456789012', ]); }); it('should generate correct axe command for swipe with all optional parameters', async () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; return { success: true, output: 'swipe completed', error: undefined, process: { pid: 12345 }, }; }; const mockAxeHelpers = createMockAxeHelpers(); await swipeLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', x1: 0, y1: 0, x2: 500, y2: 800, duration: 2.0, delta: 10, preDelay: 0.5, postDelay: 0.3, }, trackingExecutor, mockAxeHelpers, ); expect(capturedCommand).toEqual([ '/mocked/axe/path', 'swipe', '--start-x', '0', '--start-y', '0', '--end-x', '500', '--end-y', '800', '--duration', '2', '--delta', '10', '--pre-delay', '0.5', '--post-delay', '0.3', '--udid', '12345678-1234-1234-1234-123456789012', ]); }); it('should generate correct axe command with bundled axe path', async () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; return { success: true, output: 'swipe completed', error: undefined, process: { pid: 12345 }, }; }; const mockAxeHelpers = { getAxePath: () => '/path/to/bundled/axe', getBundledAxeEnvironment: () => ({ AXE_PATH: '/some/path' }), createAxeNotAvailableResponse: () => ({ content: [{ type: 'text', text: 'AXe tools not available' }], isError: true, }), }; await swipeLogic( { simulatorUuid: 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF', x1: 150, y1: 250, x2: 400, y2: 600, delta: 5, }, trackingExecutor, mockAxeHelpers, ); expect(capturedCommand).toEqual([ '/path/to/bundled/axe', 'swipe', '--start-x', '150', '--start-y', '250', '--end-x', '400', '--end-y', '600', '--delta', '5', '--udid', 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF', ]); }); }); describe('Handler Behavior (Complete Literal Returns)', () => { it('should return error for missing simulatorUuid via handler', async () => { const result = await swipePlugin.handler({ x1: 100, y1: 200, x2: 300, y2: 400 }); expect(result.isError).toBe(true); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Parameter validation failed'); expect(result.content[0].text).toContain('simulatorUuid'); }); it('should return error for missing x1 via handler', async () => { const result = await swipePlugin.handler({ simulatorUuid: '12345678-1234-1234-1234-123456789012', y1: 200, x2: 300, y2: 400, }); expect(result.isError).toBe(true); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Parameter validation failed'); expect(result.content[0].text).toContain('x1'); }); it('should return success for valid swipe execution', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'swipe completed', error: '', }); const mockAxeHelpers = createMockAxeHelpers(); const result = await swipeLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, y2: 400, }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Swipe from (100, 200) to (300, 400) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], isError: false, }); }); it('should return success for swipe with duration', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'swipe completed', error: '', }); const mockAxeHelpers = createMockAxeHelpers(); const result = await swipeLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, y2: 400, duration: 1.5, }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Swipe from (100, 200) to (300, 400) duration=1.5s simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], isError: false, }); }); it('should handle DependencyError when axe is not available', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'swipe completed', error: '', }); const mockAxeHelpers = createMockAxeHelpersWithNullPath(); const result = await swipeLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, y2: 400, }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }); }); it('should handle AxeError from failed command execution', async () => { const mockExecutor = createMockExecutor({ success: false, output: '', error: 'axe command failed', }); const mockAxeHelpers = createMockAxeHelpers(); const result = await swipeLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, y2: 400, }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: "Error: Failed to simulate swipe: axe command 'swipe' failed.\nDetails: axe command failed", }, ], isError: true, }); }); it('should handle SystemError from command execution', async () => { // Override the executor to throw SystemError for this test const systemErrorExecutor = async () => { throw new SystemError('System error occurred'); }; const mockAxeHelpers = createMockAxeHelpers(); const result = await swipeLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, y2: 400, }, systemErrorExecutor, mockAxeHelpers, ); expect(result.isError).toBe(true); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain( 'Error: System error executing axe: Failed to execute axe command: System error occurred', ); expect(result.content[0].text).toContain('Details: SystemError: System error occurred'); }); it('should handle unexpected Error objects', async () => { // Override the executor to throw an unexpected Error for this test const unexpectedErrorExecutor = async () => { throw new Error('Unexpected error'); }; const mockAxeHelpers = createMockAxeHelpers(); const result = await swipeLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, y2: 400, }, unexpectedErrorExecutor, mockAxeHelpers, ); expect(result.isError).toBe(true); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain( 'Error: System error executing axe: Failed to execute axe command: Unexpected error', ); expect(result.content[0].text).toContain('Details: Error: Unexpected error'); }); it('should handle unexpected string errors', async () => { // Override the executor to throw a string error for this test const stringErrorExecutor = async () => { throw 'String error'; }; const mockAxeHelpers = createMockAxeHelpers(); const result = await swipeLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, y2: 400, }, stringErrorExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: System error executing axe: Failed to execute axe command: String error', }, ], isError: true, }); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/ui-testing/__tests__/button.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for button tool plugin */ import { describe, it, expect, beforeEach } from 'vitest'; import { z } from 'zod'; import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts'; import buttonPlugin, { buttonLogic } from '../button.ts'; describe('Button Plugin', () => { describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(buttonPlugin.name).toBe('button'); }); it('should have correct description', () => { expect(buttonPlugin.description).toBe( 'Press hardware button on iOS simulator. Supported buttons: apple-pay, home, lock, side-button, siri', ); }); it('should have handler function', () => { expect(typeof buttonPlugin.handler).toBe('function'); }); it('should validate schema fields with safeParse', () => { const schema = z.object(buttonPlugin.schema); // Valid case expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', buttonType: 'home', }).success, ).toBe(true); // Invalid simulatorUuid expect( schema.safeParse({ simulatorUuid: 'invalid-uuid', buttonType: 'home', }).success, ).toBe(false); // Invalid buttonType expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', buttonType: 'invalid-button', }).success, ).toBe(false); // Valid with duration expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', buttonType: 'home', duration: 2.5, }).success, ).toBe(true); // Invalid duration (negative) expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', buttonType: 'home', duration: -1, }).success, ).toBe(false); // Test all valid button types const validButtons = ['apple-pay', 'home', 'lock', 'side-button', 'siri']; validButtons.forEach((buttonType) => { expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', buttonType, }).success, ).toBe(true); }); }); }); describe('Command Generation', () => { it('should generate correct axe command for basic button press', async () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; return { success: true, output: 'button press completed', error: undefined, process: { pid: 12345 }, }; }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [{ type: 'text', text: 'axe not available' }], isError: true, }), }; await buttonLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', buttonType: 'home', }, trackingExecutor, mockAxeHelpers, ); expect(capturedCommand).toEqual([ '/usr/local/bin/axe', 'button', 'home', '--udid', '12345678-1234-1234-1234-123456789012', ]); }); it('should generate correct axe command for button press with duration', async () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; return { success: true, output: 'button press completed', error: undefined, process: { pid: 12345 }, }; }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [{ type: 'text', text: 'axe not available' }], isError: true, }), }; await buttonLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', buttonType: 'side-button', duration: 2.5, }, trackingExecutor, mockAxeHelpers, ); expect(capturedCommand).toEqual([ '/usr/local/bin/axe', 'button', 'side-button', '--duration', '2.5', '--udid', '12345678-1234-1234-1234-123456789012', ]); }); it('should generate correct axe command for different button types', async () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; return { success: true, output: 'button press completed', error: undefined, process: { pid: 12345 }, }; }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [{ type: 'text', text: 'axe not available' }], isError: true, }), }; await buttonLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', buttonType: 'apple-pay', }, trackingExecutor, mockAxeHelpers, ); expect(capturedCommand).toEqual([ '/usr/local/bin/axe', 'button', 'apple-pay', '--udid', '12345678-1234-1234-1234-123456789012', ]); }); it('should generate correct axe command with bundled axe path', async () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; return { success: true, output: 'button press completed', error: undefined, process: { pid: 12345 }, }; }; const mockAxeHelpers = { getAxePath: () => '/path/to/bundled/axe', getBundledAxeEnvironment: () => ({ AXE_PATH: '/some/path' }), }; await buttonLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', buttonType: 'siri', }, trackingExecutor, mockAxeHelpers, ); expect(capturedCommand).toEqual([ '/path/to/bundled/axe', 'button', 'siri', '--udid', '12345678-1234-1234-1234-123456789012', ]); }); }); describe('Handler Behavior (Complete Literal Returns)', () => { it('should return error for missing simulatorUuid', async () => { const result = await buttonPlugin.handler({ buttonType: 'home' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Parameter validation failed'); expect(result.content[0].text).toContain('simulatorUuid: Required'); }); it('should return error for missing buttonType', async () => { const result = await buttonPlugin.handler({ simulatorUuid: '12345678-1234-1234-1234-123456789012', }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Parameter validation failed'); expect(result.content[0].text).toContain('buttonType: Required'); }); it('should return error for invalid simulatorUuid format', async () => { const result = await buttonPlugin.handler({ simulatorUuid: 'invalid-uuid-format', buttonType: 'home', }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Parameter validation failed'); expect(result.content[0].text).toContain('Invalid Simulator UUID format'); }); it('should return error for invalid buttonType', async () => { const result = await buttonPlugin.handler({ simulatorUuid: '12345678-1234-1234-1234-123456789012', buttonType: 'invalid-button', }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Parameter validation failed'); }); it('should return error for negative duration', async () => { const result = await buttonPlugin.handler({ simulatorUuid: '12345678-1234-1234-1234-123456789012', buttonType: 'home', duration: -1, }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Parameter validation failed'); expect(result.content[0].text).toContain('Duration must be non-negative'); }); it('should return success for valid button press', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'button press completed', error: undefined, process: { pid: 12345 }, }); const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [{ type: 'text', text: 'axe not available' }], isError: true, }), }; const result = await buttonLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', buttonType: 'home', }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [{ type: 'text', text: "Hardware button 'home' pressed successfully." }], isError: false, }); }); it('should return success for button press with duration', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'button press completed', error: undefined, process: { pid: 12345 }, }); const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [{ type: 'text', text: 'axe not available' }], isError: true, }), }; const result = await buttonLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', buttonType: 'side-button', duration: 2.5, }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [{ type: 'text', text: "Hardware button 'side-button' pressed successfully." }], isError: false, }); }); it('should handle DependencyError when axe is not available', async () => { const mockAxeHelpers = { getAxePath: () => null, getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; const result = await buttonLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', buttonType: 'home', }, createNoopExecutor(), mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }); }); it('should handle AxeError from failed command execution', async () => { const mockExecutor = createMockExecutor({ success: false, output: '', error: 'axe command failed', process: { pid: 12345 }, }); const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [{ type: 'text', text: 'axe not available' }], isError: true, }), }; const result = await buttonLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', buttonType: 'home', }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: "Error: Failed to press button 'home': axe command 'button' failed.\nDetails: axe command failed", }, ], isError: true, }); }); it('should handle SystemError from command execution', async () => { const mockExecutor = async () => { throw new Error('ENOENT: no such file or directory'); }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [{ type: 'text', text: 'axe not available' }], isError: true, }), }; const result = await buttonLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', buttonType: 'home', }, mockExecutor, mockAxeHelpers, ); expect(result.content[0].text).toMatch( /^Error: System error executing axe: Failed to execute axe command: ENOENT: no such file or directory/, ); expect(result.isError).toBe(true); }); it('should handle unexpected Error objects', async () => { const mockExecutor = async () => { throw new Error('Unexpected error'); }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [{ type: 'text', text: 'axe not available' }], isError: true, }), }; const result = await buttonLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', buttonType: 'home', }, mockExecutor, mockAxeHelpers, ); expect(result.content[0].text).toMatch( /^Error: System error executing axe: Failed to execute axe command: Unexpected error/, ); expect(result.isError).toBe(true); }); it('should handle unexpected string errors', async () => { const mockExecutor = async () => { throw 'String error'; }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [{ type: 'text', text: 'axe not available' }], isError: true, }), }; const result = await buttonLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', buttonType: 'home', }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: System error executing axe: Failed to execute axe command: String error', }, ], isError: true, }); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/ui-testing/__tests__/key_press.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for key_press tool plugin */ import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import { createMockExecutor, createMockFileSystemExecutor, createNoopExecutor, } from '../../../../test-utils/mock-executors.ts'; import keyPressPlugin, { key_pressLogic } from '../key_press.ts'; describe('Key Press Plugin', () => { describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(keyPressPlugin.name).toBe('key_press'); }); it('should have correct description', () => { expect(keyPressPlugin.description).toBe( 'Press a single key by keycode on the simulator. Common keycodes: 40=Return, 42=Backspace, 43=Tab, 44=Space, 58-67=F1-F10.', ); }); it('should have handler function', () => { expect(typeof keyPressPlugin.handler).toBe('function'); }); it('should validate schema fields with safeParse', () => { const schema = z.object(keyPressPlugin.schema); // Valid case expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCode: 40, }).success, ).toBe(true); // Invalid simulatorUuid expect( schema.safeParse({ simulatorUuid: 'invalid-uuid', keyCode: 40, }).success, ).toBe(false); // Invalid keyCode (string) expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCode: 'invalid', }).success, ).toBe(false); // Invalid keyCode (below range) expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCode: -1, }).success, ).toBe(false); // Invalid keyCode (above range) expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCode: 256, }).success, ).toBe(false); // Valid with duration expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCode: 40, duration: 1.5, }).success, ).toBe(true); // Invalid duration (negative) expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCode: 40, duration: -1, }).success, ).toBe(false); }); }); describe('Command Generation', () => { it('should generate correct axe command for basic key press', async () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; return { success: true, output: 'key press completed', error: undefined, process: { pid: 12345 }, }; }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; await key_pressLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCode: 40, }, trackingExecutor, mockAxeHelpers, ); expect(capturedCommand).toEqual([ '/usr/local/bin/axe', 'key', '40', '--udid', '12345678-1234-1234-1234-123456789012', ]); }); it('should generate correct axe command for key press with duration', async () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; return { success: true, output: 'key press completed', error: undefined, process: { pid: 12345 }, }; }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; await key_pressLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCode: 42, duration: 1.5, }, trackingExecutor, mockAxeHelpers, ); expect(capturedCommand).toEqual([ '/usr/local/bin/axe', 'key', '42', '--duration', '1.5', '--udid', '12345678-1234-1234-1234-123456789012', ]); }); it('should generate correct axe command for different key codes', async () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; return { success: true, output: 'key press completed', error: undefined, process: { pid: 12345 }, }; }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; await key_pressLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCode: 255, }, trackingExecutor, mockAxeHelpers, ); expect(capturedCommand).toEqual([ '/usr/local/bin/axe', 'key', '255', '--udid', '12345678-1234-1234-1234-123456789012', ]); }); it('should generate correct axe command with bundled axe path', async () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; return { success: true, output: 'key press completed', error: undefined, process: { pid: 12345 }, }; }; const mockAxeHelpers = { getAxePath: () => '/path/to/bundled/axe', getBundledAxeEnvironment: () => ({ AXE_PATH: '/some/path' }), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; await key_pressLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCode: 44, }, trackingExecutor, mockAxeHelpers, ); expect(capturedCommand).toEqual([ '/path/to/bundled/axe', 'key', '44', '--udid', '12345678-1234-1234-1234-123456789012', ]); }); }); describe('Handler Behavior (Complete Literal Returns)', () => { // Note: Parameter validation is now handled by Zod schema validation in createTypedTool wrapper. // The key_pressLogic function expects valid parameters and focuses on business logic testing. it('should return success for valid key press execution', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'key press completed', error: '', }); const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; const result = await key_pressLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCode: 40, }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [{ type: 'text', text: 'Key press (code: 40) simulated successfully.' }], isError: false, }); }); it('should return success for key press with duration', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'key press completed', error: '', }); const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; const result = await key_pressLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCode: 42, duration: 1.5, }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [{ type: 'text', text: 'Key press (code: 42) simulated successfully.' }], isError: false, }); }); it('should handle DependencyError when axe is not available', async () => { const mockAxeHelpers = { getAxePath: () => null, getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; const result = await key_pressLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCode: 40, }, createNoopExecutor(), mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\n' + 'This is likely an installation issue with the npm package.\n' + 'Please reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }); }); it('should handle AxeError from failed command execution', async () => { const mockExecutor = createMockExecutor({ success: false, output: '', error: 'axe command failed', }); const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; const result = await key_pressLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCode: 40, }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: "Error: Failed to simulate key press (code: 40): axe command 'key' failed.\nDetails: axe command failed", }, ], isError: true, }); }); it('should handle SystemError from command execution', async () => { const mockExecutor = () => { throw new Error('System error occurred'); }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; const result = await key_pressLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCode: 40, }, mockExecutor, mockAxeHelpers, ); expect(result.isError).toBe(true); expect(result.content[0].text).toContain( 'Error: System error executing axe: Failed to execute axe command: System error occurred', ); }); it('should handle unexpected Error objects', async () => { const mockExecutor = () => { throw new Error('Unexpected error'); }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; const result = await key_pressLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCode: 40, }, mockExecutor, mockAxeHelpers, ); expect(result.isError).toBe(true); expect(result.content[0].text).toContain( 'Error: System error executing axe: Failed to execute axe command: Unexpected error', ); }); it('should handle unexpected string errors', async () => { const mockExecutor = () => { throw 'String error'; }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; const result = await key_pressLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCode: 40, }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: System error executing axe: Failed to execute axe command: String error', }, ], isError: true, }); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/__tests__/screenshot.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for screenshot plugin * Following CLAUDE.md testing standards with literal validation * Using pure dependency injection for deterministic testing */ import { describe, it, expect, beforeEach } from 'vitest'; import { z } from 'zod'; import { createMockExecutor, createMockFileSystemExecutor, createCommandMatchingMockExecutor, } from '../../../../test-utils/mock-executors.ts'; import { SystemError } from '../../../../utils/responses/index.ts'; import screenshotPlugin, { screenshotLogic } from '../../ui-testing/screenshot.ts'; describe('screenshot plugin', () => { // No mocks to clear since we use pure dependency injection describe('Export Field Validation (Literal)', () => { it('should have correct name field', () => { expect(screenshotPlugin.name).toBe('screenshot'); }); it('should have correct description field', () => { expect(screenshotPlugin.description).toBe( "Captures screenshot for visual verification. For UI coordinates, use describe_ui instead (don't determine coordinates from screenshots).", ); }); it('should have handler function', () => { expect(typeof screenshotPlugin.handler).toBe('function'); }); it('should have correct schema validation', () => { const schema = z.object(screenshotPlugin.schema); expect( schema.safeParse({ simulatorUuid: '550e8400-e29b-41d4-a716-446655440000', }).success, ).toBe(true); expect( schema.safeParse({ simulatorUuid: 123, }).success, ).toBe(false); expect(schema.safeParse({}).success).toBe(false); }); }); describe('Command Generation', () => { it('should generate correct simctl and sips commands', async () => { const capturedCommands: string[][] = []; const mockExecutor = createCommandMatchingMockExecutor({ 'xcrun simctl': { success: true, output: 'Screenshot saved' }, sips: { success: true, output: 'Image optimized' }, }); // Wrap to capture both commands const capturingExecutor = async (command: string[], ...args: any[]) => { capturedCommands.push(command); return mockExecutor(command, ...args); }; const mockFileSystemExecutor = createMockFileSystemExecutor({ readFile: async () => 'fake-image-data', }); const mockPathDeps = { tmpdir: () => '/tmp', join: (...paths: string[]) => paths.join('/'), }; const mockUuidDeps = { v4: () => 'mock-uuid-123', }; await screenshotLogic( { simulatorUuid: 'test-uuid', }, capturingExecutor, mockFileSystemExecutor, mockPathDeps, mockUuidDeps, ); // Should execute both commands in sequence expect(capturedCommands).toHaveLength(2); // First command: xcrun simctl screenshot expect(capturedCommands[0]).toEqual([ 'xcrun', 'simctl', 'io', 'test-uuid', 'screenshot', '/tmp/screenshot_mock-uuid-123.png', ]); // Second command: sips optimization expect(capturedCommands[1]).toEqual([ 'sips', '-Z', '800', '-s', 'format', 'jpeg', '-s', 'formatOptions', '75', '/tmp/screenshot_mock-uuid-123.png', '--out', '/tmp/screenshot_optimized_mock-uuid-123.jpg', ]); }); it('should generate correct path with different uuid', async () => { const capturedCommands: string[][] = []; const mockExecutor = createCommandMatchingMockExecutor({ 'xcrun simctl': { success: true, output: 'Screenshot saved' }, sips: { success: true, output: 'Image optimized' }, }); // Wrap to capture both commands const capturingExecutor = async (command: string[], ...args: any[]) => { capturedCommands.push(command); return mockExecutor(command, ...args); }; const mockFileSystemExecutor = createMockFileSystemExecutor({ readFile: async () => 'fake-image-data', }); const mockPathDeps = { tmpdir: () => '/tmp', join: (...paths: string[]) => paths.join('/'), }; const mockUuidDeps = { v4: () => 'different-uuid-456', }; await screenshotLogic( { simulatorUuid: 'another-uuid', }, capturingExecutor, mockFileSystemExecutor, mockPathDeps, mockUuidDeps, ); // Should execute both commands in sequence expect(capturedCommands).toHaveLength(2); // First command: xcrun simctl screenshot expect(capturedCommands[0]).toEqual([ 'xcrun', 'simctl', 'io', 'another-uuid', 'screenshot', '/tmp/screenshot_different-uuid-456.png', ]); // Second command: sips optimization expect(capturedCommands[1]).toEqual([ 'sips', '-Z', '800', '-s', 'format', 'jpeg', '-s', 'formatOptions', '75', '/tmp/screenshot_different-uuid-456.png', '--out', '/tmp/screenshot_optimized_different-uuid-456.jpg', ]); }); it('should use default dependencies when not provided', async () => { const capturedCommands: string[][] = []; const mockExecutor = createCommandMatchingMockExecutor({ 'xcrun simctl': { success: true, output: 'Screenshot saved' }, sips: { success: true, output: 'Image optimized' }, }); // Wrap to capture both commands const capturingExecutor = async (command: string[], ...args: any[]) => { capturedCommands.push(command); return mockExecutor(command, ...args); }; const mockFileSystemExecutor = createMockFileSystemExecutor({ readFile: async () => 'fake-image-data', }); await screenshotLogic( { simulatorUuid: 'test-uuid', }, capturingExecutor, mockFileSystemExecutor, ); // Should execute both commands in sequence expect(capturedCommands).toHaveLength(2); // First command should be generated with real os.tmpdir, path.join, and uuidv4 const firstCommand = capturedCommands[0]; expect(firstCommand).toHaveLength(6); expect(firstCommand[0]).toBe('xcrun'); expect(firstCommand[1]).toBe('simctl'); expect(firstCommand[2]).toBe('io'); expect(firstCommand[3]).toBe('test-uuid'); expect(firstCommand[4]).toBe('screenshot'); expect(firstCommand[5]).toMatch(/\/.*\/screenshot_.*\.png/); // Second command should be sips optimization const secondCommand = capturedCommands[1]; expect(secondCommand[0]).toBe('sips'); expect(secondCommand[1]).toBe('-Z'); expect(secondCommand[2]).toBe('800'); // Should have proper PNG input and JPG output paths expect(secondCommand[secondCommand.length - 3]).toMatch(/\/.*\/screenshot_.*\.png/); expect(secondCommand[secondCommand.length - 1]).toMatch(/\/.*\/screenshot_optimized_.*\.jpg/); }); }); describe('Response Processing', () => { it('should capture screenshot successfully', async () => { const mockImageBuffer = Buffer.from('fake-image-data'); // Mock both commands: screenshot + optimization const mockExecutor = createCommandMatchingMockExecutor({ 'xcrun simctl': { success: true, output: 'Screenshot saved' }, sips: { success: true, output: 'Image optimized' }, }); const mockFileSystemExecutor = createMockFileSystemExecutor({ readFile: async () => mockImageBuffer.toString('base64'), // Return base64 directly }); const mockPathDeps = { tmpdir: () => '/tmp', join: (...paths: string[]) => paths.join('/'), }; const mockUuidDeps = { v4: () => 'mock-uuid-123', }; const result = await screenshotLogic( { simulatorUuid: 'test-uuid', }, mockExecutor, mockFileSystemExecutor, mockPathDeps, mockUuidDeps, ); expect(result).toEqual({ content: [ { type: 'image', data: mockImageBuffer.toString('base64'), mimeType: 'image/jpeg', // Now JPEG after optimization }, ], isError: false, }); }); it('should handle missing simulatorUuid via handler', async () => { // Test Zod validation by calling the handler with invalid params const result = await screenshotPlugin.handler({}); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorUuid: Required', }, ], isError: true, }); }); it('should handle command failure', async () => { const mockExecutor = createMockExecutor({ success: false, output: '', error: 'Command failed', }); const mockPathDeps = { tmpdir: () => '/tmp', join: (...paths: string[]) => paths.join('/'), }; const mockUuidDeps = { v4: () => 'mock-uuid-123', }; const result = await screenshotLogic( { simulatorUuid: 'test-uuid', }, mockExecutor, createMockFileSystemExecutor(), mockPathDeps, mockUuidDeps, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: System error executing screenshot: Failed to capture screenshot: Command failed', }, ], isError: true, }); }); it('should handle file read failure', async () => { const mockExecutor = createMockExecutor({ success: true, output: '', error: undefined, }); const mockFileSystemExecutor = createMockFileSystemExecutor({ readFile: async () => { throw new Error('File not found'); }, }); const mockPathDeps = { tmpdir: () => '/tmp', join: (...paths: string[]) => paths.join('/'), }; const mockUuidDeps = { v4: () => 'mock-uuid-123', }; const result = await screenshotLogic( { simulatorUuid: 'test-uuid', }, mockExecutor, mockFileSystemExecutor, mockPathDeps, mockUuidDeps, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: Screenshot captured but failed to process image file: File not found', }, ], isError: true, }); }); it('should call correct command with direct execution', async () => { const capturedArgs: any[][] = []; const mockExecutor = createCommandMatchingMockExecutor({ 'xcrun simctl': { success: true, output: 'Screenshot saved' }, sips: { success: true, output: 'Image optimized' }, }); // Wrap to capture both command executions const capturingExecutor = async (...args: any[]) => { capturedArgs.push(args); return mockExecutor(...args); }; const mockFileSystemExecutor = createMockFileSystemExecutor({ readFile: async () => 'fake-image-data', }); const mockPathDeps = { tmpdir: () => '/tmp', join: (...paths: string[]) => paths.join('/'), }; const mockUuidDeps = { v4: () => 'mock-uuid-123', }; await screenshotLogic( { simulatorUuid: 'test-uuid', }, capturingExecutor, mockFileSystemExecutor, mockPathDeps, mockUuidDeps, ); // Should capture both command executions expect(capturedArgs).toHaveLength(2); // First call: xcrun simctl screenshot (3 args: command, logPrefix, useShell) expect(capturedArgs[0]).toEqual([ ['xcrun', 'simctl', 'io', 'test-uuid', 'screenshot', '/tmp/screenshot_mock-uuid-123.png'], '[Screenshot]: screenshot', false, ]); // Second call: sips optimization (3 args: command, logPrefix, useShell) expect(capturedArgs[1]).toEqual([ [ 'sips', '-Z', '800', '-s', 'format', 'jpeg', '-s', 'formatOptions', '75', '/tmp/screenshot_mock-uuid-123.png', '--out', '/tmp/screenshot_optimized_mock-uuid-123.jpg', ], '[Screenshot]: optimize image', false, ]); }); it('should handle SystemError exceptions', async () => { const mockExecutor = createMockExecutor(new SystemError('System error occurred')); const mockPathDeps = { tmpdir: () => '/tmp', join: (...paths: string[]) => paths.join('/'), }; const mockUuidDeps = { v4: () => 'mock-uuid-123', }; const result = await screenshotLogic( { simulatorUuid: 'test-uuid', }, mockExecutor, createMockFileSystemExecutor(), mockPathDeps, mockUuidDeps, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: System error executing screenshot: System error occurred', }, ], isError: true, }); }); it('should handle unexpected Error objects', async () => { const mockExecutor = createMockExecutor(new Error('Unexpected error')); const mockPathDeps = { tmpdir: () => '/tmp', join: (...paths: string[]) => paths.join('/'), }; const mockUuidDeps = { v4: () => 'mock-uuid-123', }; const result = await screenshotLogic( { simulatorUuid: 'test-uuid', }, mockExecutor, createMockFileSystemExecutor(), mockPathDeps, mockUuidDeps, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: An unexpected error occurred: Unexpected error', }, ], isError: true, }); }); it('should handle unexpected string errors', async () => { const mockExecutor = createMockExecutor('String error'); const mockPathDeps = { tmpdir: () => '/tmp', join: (...paths: string[]) => paths.join('/'), }; const mockUuidDeps = { v4: () => 'mock-uuid-123', }; const result = await screenshotLogic( { simulatorUuid: 'test-uuid', }, mockExecutor, createMockFileSystemExecutor(), mockPathDeps, mockUuidDeps, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: An unexpected error occurred: String error', }, ], isError: true, }); }); it('should handle file read error with fileSystemExecutor', async () => { const mockExecutor = createMockExecutor({ success: true, output: '', error: undefined, }); const mockFileSystemExecutor = createMockFileSystemExecutor({ readFile: async () => { throw 'File system error'; }, }); const mockPathDeps = { tmpdir: () => '/tmp', join: (...paths: string[]) => paths.join('/'), }; const mockUuidDeps = { v4: () => 'mock-uuid-123', }; const result = await screenshotLogic( { simulatorUuid: 'test-uuid', }, mockExecutor, mockFileSystemExecutor, mockPathDeps, mockUuidDeps, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: Screenshot captured but failed to process image file: File system error', }, ], isError: true, }); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/ui-testing/__tests__/key_sequence.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for key_sequence plugin */ import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts'; import keySequencePlugin, { key_sequenceLogic } from '../key_sequence.ts'; describe('Key Sequence Plugin', () => { describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(keySequencePlugin.name).toBe('key_sequence'); }); it('should have correct description', () => { expect(keySequencePlugin.description).toBe( 'Press key sequence using HID keycodes on iOS simulator with configurable delay', ); }); it('should have handler function', () => { expect(typeof keySequencePlugin.handler).toBe('function'); }); it('should validate schema fields with safeParse', () => { const schema = z.object(keySequencePlugin.schema); // Valid case expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCodes: [40, 42, 44], }).success, ).toBe(true); // Invalid simulatorUuid expect( schema.safeParse({ simulatorUuid: 'invalid-uuid', keyCodes: [40], }).success, ).toBe(false); // Invalid keyCodes - empty array expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCodes: [], }).success, ).toBe(false); // Invalid keyCodes - out of range expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCodes: [-1], }).success, ).toBe(false); expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCodes: [256], }).success, ).toBe(false); // Invalid delay - negative expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCodes: [40], delay: -0.1, }).success, ).toBe(false); // Valid with optional delay expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCodes: [40], delay: 0.1, }).success, ).toBe(true); // Missing required fields expect(schema.safeParse({}).success).toBe(false); }); }); describe('Command Generation', () => { it('should generate correct axe command for basic key sequence', async () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; return { success: true, output: 'key sequence completed', error: undefined, process: { pid: 12345 }, }; }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; await key_sequenceLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCodes: [40, 42, 44], }, trackingExecutor, mockAxeHelpers, ); expect(capturedCommand).toEqual([ '/usr/local/bin/axe', 'key-sequence', '--keycodes', '40,42,44', '--udid', '12345678-1234-1234-1234-123456789012', ]); }); it('should generate correct axe command for key sequence with delay', async () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; return { success: true, output: 'key sequence completed', error: undefined, process: { pid: 12345 }, }; }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; await key_sequenceLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCodes: [58, 59, 60], delay: 0.5, }, trackingExecutor, mockAxeHelpers, ); expect(capturedCommand).toEqual([ '/usr/local/bin/axe', 'key-sequence', '--keycodes', '58,59,60', '--delay', '0.5', '--udid', '12345678-1234-1234-1234-123456789012', ]); }); it('should generate correct axe command for single key in sequence', async () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; return { success: true, output: 'key sequence completed', error: undefined, process: { pid: 12345 }, }; }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; await key_sequenceLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCodes: [255], }, trackingExecutor, mockAxeHelpers, ); expect(capturedCommand).toEqual([ '/usr/local/bin/axe', 'key-sequence', '--keycodes', '255', '--udid', '12345678-1234-1234-1234-123456789012', ]); }); it('should generate correct axe command with bundled axe path', async () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; return { success: true, output: 'key sequence completed', error: undefined, process: { pid: 12345 }, }; }; const mockAxeHelpers = { getAxePath: () => '/path/to/bundled/axe', getBundledAxeEnvironment: () => ({ AXE_PATH: '/some/path' }), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; await key_sequenceLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCodes: [0, 1, 2, 3, 4], delay: 1.0, }, trackingExecutor, mockAxeHelpers, ); expect(capturedCommand).toEqual([ '/path/to/bundled/axe', 'key-sequence', '--keycodes', '0,1,2,3,4', '--delay', '1', '--udid', '12345678-1234-1234-1234-123456789012', ]); }); }); describe('Handler Behavior (Complete Literal Returns)', () => { it('should return success for valid key sequence execution', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'Key sequence executed', error: undefined, }); const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; const result = await key_sequenceLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCodes: [40, 42, 44], delay: 0.1, }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [{ type: 'text', text: 'Key sequence [40,42,44] executed successfully.' }], isError: false, }); }); it('should return success for key sequence without delay', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'Key sequence executed', error: undefined, }); const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; const result = await key_sequenceLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCodes: [40], }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [{ type: 'text', text: 'Key sequence [40] executed successfully.' }], isError: false, }); }); it('should handle DependencyError when axe binary not found', async () => { const mockAxeHelpers = { getAxePath: () => null, getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; const result = await key_sequenceLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCodes: [40], }, createNoopExecutor(), mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }); }); it('should handle AxeError from command execution', async () => { const mockExecutor = createMockExecutor({ success: false, output: '', error: 'Simulator not found', }); const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; const result = await key_sequenceLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCodes: [40], }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: "Error: Failed to execute key sequence: axe command 'key-sequence' failed.\nDetails: Simulator not found", }, ], isError: true, }); }); it('should handle SystemError from command execution', async () => { const mockExecutor = () => { throw new Error('ENOENT: no such file or directory'); }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; const result = await key_sequenceLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCodes: [40], }, mockExecutor, mockAxeHelpers, ); expect(result.content[0].text).toMatch( /^Error: System error executing axe: Failed to execute axe command: ENOENT: no such file or directory/, ); expect(result.isError).toBe(true); }); it('should handle unexpected Error objects', async () => { const mockExecutor = () => { throw new Error('Unexpected error'); }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; const result = await key_sequenceLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCodes: [40], }, mockExecutor, mockAxeHelpers, ); expect(result.content[0].text).toMatch( /^Error: System error executing axe: Failed to execute axe command: Unexpected error/, ); expect(result.isError).toBe(true); }); it('should handle unexpected string errors', async () => { const mockExecutor = () => { throw 'String error'; }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; const result = await key_sequenceLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', keyCodes: [40], }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: System error executing axe: Failed to execute axe command: String error', }, ], isError: true, }); }); }); }); ``` -------------------------------------------------------------------------------- /docs/RELOADEROO_XCODEBUILDMCP_PRIMER.md: -------------------------------------------------------------------------------- ```markdown # Reloaderoo + XcodeBuildMCP: Curated CLI Primer Use this primer to drive XcodeBuildMCP entirely through Reloaderoo—treating it like a CLI. It is designed to be included in your agent’s context to show exactly how to invoke the specific tools your project needs. Why this file: - XcodeBuildMCP exposes many tools. Dumping the full tool surface into the context wastes tokens. - Instead, copy this file into your project and delete everything you don’t need. Keep only the commands relevant to your workflow (e.g., just Simulator tools). - Your trimmed version becomes a small, project‑specific reference that tells your agent precisely which Reloaderoo tool calls to make. How to use this primer: 1. Copy this file into your repo (e.g., docs/xcodebuildmcp_primer.md or AGENTS.md). 2. Remove all sections and commands you don’t use. Keep it minimal. 3. Replace placeholders with your real values (paths, schemes, simulator UUIDs/Names, bundle IDs, etc.). 4. Use the quiet (-q) examples to reduce noise; pipe output to jq when you only need the content. 5. Include your curated file in the agent context whenever you want it to call XcodeBuildMCP via Reloaderoo. Conventions in the examples: - Calls use: npx reloaderoo@latest inspect … -q -- npx xcodebuildmcp@latest - Parameters are passed as JSON via --params. - Resources are read with read-resource (e.g., xcodebuildmcp://simulators). - Use jq -r '.contents[].text' to extract the textual results when needed. Keep it small. The smaller your curated primer, the less context your agent needs—and the cheaper, faster, and more reliable your interactions will be. ## Installation Reloaderoo is available via npm and can be used with npx for universal compatibility. ```bash # Use npx to run reloaderoo npx reloaderoo@latest --help ``` ## Hint Use jq to parse the output to get just the content response: ```bash npx reloaderoo@latest inspect read-resource "xcodebuildmcp://simulators" -q -- npx xcodebuildmcp@latest | jq -r '.contents[].text' ``` **Example Tool Calls:** ## Dynamic Tool Discovery - **`discover_tools`**: Analyzes a task description to enable relevant tools. ```bash npx reloaderoo@latest inspect call-tool discover_tools --params '{"task_description": "I want to build and run my iOS app on a simulator."}' -q -- npx xcodebuildmcp@latest ``` ## iOS Device Development - **`build_device`**: Builds an app for a physical device. ```bash npx reloaderoo@latest inspect -q call-tool build_device --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest ``` - **`get_device_app_path`**: Gets the `.app` bundle path for a device build. ```bash npx reloaderoo@latest inspect call-tool get_device_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest ``` - **`install_app_device`**: Installs an app on a physical device. ```bash npx reloaderoo@latest inspect call-tool install_app_device --params '{"deviceId": "DEVICE_UDID", "appPath": "/path/to/MyApp.app"}' -q -- npx xcodebuildmcp@latest ``` - **`launch_app_device`**: Launches an app on a physical device. ```bash npx reloaderoo@latest inspect call-tool launch_app_device --params '{"deviceId": "DEVICE_UDID", "bundleId": "com.example.MyApp"}' -q -- npx xcodebuildmcp@latest ``` - **`list_devices`**: Lists connected physical devices. ```bash npx reloaderoo@latest inspect call-tool list_devices --params '{}' -q -- npx xcodebuildmcp@latest ``` - **`stop_app_device`**: Stops an app on a physical device. ```bash npx reloaderoo@latest inspect call-tool stop_app_device --params '{"deviceId": "DEVICE_UDID", "processId": 12345}' -q -- npx xcodebuildmcp@latest ``` - **`test_device`**: Runs tests on a physical device. ```bash npx reloaderoo@latest inspect call-tool test_device --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "deviceId": "DEVICE_UDID"}' -q -- npx xcodebuildmcp@latest ``` ## iOS Simulator Development - **`boot_sim`**: Boots a simulator. ```bash npx reloaderoo@latest inspect call-tool boot_sim --params '{"simulatorUuid": "SIMULATOR_UUID"}' -q -- npx xcodebuildmcp@latest ``` - **`build_run_sim`**: Builds and runs an app on a simulator. ```bash npx reloaderoo@latest inspect call-tool build_run_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -q -- npx xcodebuildmcp@latest ``` - **`build_sim`**: Builds an app for a simulator. ```bash npx reloaderoo@latest inspect call-tool build_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -q -- npx xcodebuildmcp@latest ``` - **`get_sim_app_path`**: Gets the `.app` bundle path for a simulator build. ```bash npx reloaderoo@latest inspect call-tool get_sim_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "platform": "iOS Simulator", "simulatorName": "iPhone 16"}' -q -- npx xcodebuildmcp@latest ``` - **`install_app_sim`**: Installs an app on a simulator. ```bash npx reloaderoo@latest inspect call-tool install_app_sim --params '{"simulatorUuid": "SIMULATOR_UUID", "appPath": "/path/to/MyApp.app"}' -q -- npx xcodebuildmcp@latest ``` - **`launch_app_logs_sim`**: Launches an app on a simulator with log capture. ```bash npx reloaderoo@latest inspect call-tool launch_app_logs_sim --params '{"simulatorUuid": "SIMULATOR_UUID", "bundleId": "com.example.MyApp"}' -q -- npx xcodebuildmcp@latest ``` - **`launch_app_sim`**: Launches an app on a simulator. ```bash npx reloaderoo@latest inspect call-tool launch_app_sim --params '{"simulatorName": "iPhone 16", "bundleId": "com.example.MyApp"}' -q -- npx xcodebuildmcp@latest ``` - **`list_sims`**: Lists available simulators. ```bash npx reloaderoo@latest inspect call-tool list_sims --params '{}' -q -- npx xcodebuildmcp@latest ``` - **`open_sim`**: Opens the Simulator application. ```bash npx reloaderoo@latest inspect call-tool open_sim --params '{}' -q -- npx xcodebuildmcp@latest ``` - **`stop_app_sim`**: Stops an app on a simulator. ```bash npx reloaderoo@latest inspect call-tool stop_app_sim --params '{"simulatorName": "iPhone 16", "bundleId": "com.example.MyApp"}' -q -- npx xcodebuildmcp@latest ``` - **`test_sim`**: Runs tests on a simulator. ```bash npx reloaderoo@latest inspect call-tool test_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -q -- npx xcodebuildmcp@latest ``` ## Log Capture & Management - **`start_device_log_cap`**: Starts log capture for a physical device. ```bash npx reloaderoo@latest inspect call-tool start_device_log_cap --params '{"deviceId": "DEVICE_UDID", "bundleId": "com.example.MyApp"}' -q -- npx xcodebuildmcp@latest ``` - **`start_sim_log_cap`**: Starts log capture for a simulator. ```bash npx reloaderoo@latest inspect call-tool start_sim_log_cap --params '{"simulatorUuid": "SIMULATOR_UUID", "bundleId": "com.example.MyApp"}' -q -- npx xcodebuildmcp@latest ``` - **`stop_device_log_cap`**: Stops log capture for a physical device. ```bash npx reloaderoo@latest inspect call-tool stop_device_log_cap --params '{"logSessionId": "SESSION_ID"}' -q -- npx xcodebuildmcp@latest ``` - **`stop_sim_log_cap`**: Stops log capture for a simulator. ```bash npx reloaderoo@latest inspect call-tool stop_sim_log_cap --params '{"logSessionId": "SESSION_ID"}' -q -- npx xcodebuildmcp@latest ``` ## macOS Development - **`build_macos`**: Builds a macOS app. ```bash npx reloaderoo@latest inspect call-tool build_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest ``` - **`build_run_macos`**: Builds and runs a macOS app. ```bash npx reloaderoo@latest inspect call-tool build_run_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest ``` - **`get_mac_app_path`**: Gets the `.app` bundle path for a macOS build. ```bash npx reloaderoo@latest inspect call-tool get_mac_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest ``` - **`launch_mac_app`**: Launches a macOS app. ```bash npx reloaderoo@latest inspect call-tool launch_mac_app --params '{"appPath": "/Applications/Calculator.app"}' -q -- npx xcodebuildmcp@latest ``` - **`stop_mac_app`**: Stops a macOS app. ```bash npx reloaderoo@latest inspect call-tool stop_mac_app --params '{"appName": "Calculator"}' -q -- npx xcodebuildmcp@latest ``` - **`test_macos`**: Runs tests for a macOS project. ```bash npx reloaderoo@latest inspect call-tool test_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest ``` ## Project Discovery - **`discover_projs`**: Discovers Xcode projects and workspaces. ```bash npx reloaderoo@latest inspect call-tool discover_projs --params '{"workspaceRoot": "/path/to/workspace"}' -q -- npx xcodebuildmcp@latest ``` - **`get_app_bundle_id`**: Gets an app's bundle identifier. ```bash npx reloaderoo@latest inspect call-tool get_app_bundle_id --params '{"appPath": "/path/to/MyApp.app"}' -q -- npx xcodebuildmcp@latest ``` - **`get_mac_bundle_id`**: Gets a macOS app's bundle identifier. ```bash npx reloaderoo@latest inspect call-tool get_mac_bundle_id --params '{"appPath": "/Applications/Calculator.app"}' -q -- npx xcodebuildmcp@latest ``` - **`list_schemes`**: Lists schemes in a project or workspace. ```bash npx reloaderoo@latest inspect call-tool list_schemes --params '{"projectPath": "/path/to/MyProject.xcodeproj"}' -q -- npx xcodebuildmcp@latest ``` - **`show_build_settings`**: Shows build settings for a scheme. ```bash npx reloaderoo@latest inspect call-tool show_build_settings --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest ``` ## Project Scaffolding - **`scaffold_ios_project`**: Scaffolds a new iOS project. ```bash npx reloaderoo@latest inspect call-tool scaffold_ios_project --params '{"projectName": "MyNewApp", "outputPath": "/path/to/projects"}' -q -- npx xcodebuildmcp@latest ``` - **`scaffold_macos_project`**: Scaffolds a new macOS project. ```bash npx reloaderoo@latest inspect call-tool scaffold_macos_project --params '{"projectName": "MyNewMacApp", "outputPath": "/path/to/projects"}' -q -- npx xcodebuildmcp@latest ``` ## Project Utilities - **`clean`**: Cleans build artifacts. ```bash # For a project npx reloaderoo@latest inspect call-tool clean --params '{"projectPath": "/path/to/MyProject.xcodeproj"}' -q -- npx xcodebuildmcp@latest # For a workspace npx reloaderoo@latest inspect call-tool clean --params '{"workspacePath": "/path/to/MyWorkspace.xcworkspace", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest ``` ## Simulator Management - **`reset_sim_location`**: Resets a simulator's location. ```bash npx reloaderoo@latest inspect call-tool reset_sim_location --params '{"simulatorUuid": "SIMULATOR_UUID"}' -q -- npx xcodebuildmcp@latest ``` - **`set_sim_appearance`**: Sets a simulator's appearance (dark/light mode). ```bash npx reloaderoo@latest inspect call-tool set_sim_appearance --params '{"simulatorUuid": "SIMULATOR_UUID", "mode": "dark"}' -q -- npx xcodebuildmcp@latest ``` - **`set_sim_location`**: Sets a simulator's GPS location. ```bash npx reloaderoo@latest inspect call-tool set_sim_location --params '{"simulatorUuid": "SIMULATOR_UUID", "latitude": 37.7749, "longitude": -122.4194}' -q -- npx xcodebuildmcp@latest ``` - **`sim_statusbar`**: Overrides a simulator's status bar. ```bash npx reloaderoo@latest inspect call-tool sim_statusbar --params '{"simulatorUuid": "SIMULATOR_UUID", "dataNetwork": "wifi"}' -q -- npx xcodebuildmcp@latest ``` ## Swift Package Manager - **`swift_package_build`**: Builds a Swift package. ```bash npx reloaderoo@latest inspect call-tool swift_package_build --params '{"packagePath": "/path/to/package"}' -q -- npx xcodebuildmcp@latest ``` - **`swift_package_clean`**: Cleans a Swift package. ```bash npx reloaderoo@latest inspect call-tool swift_package_clean --params '{"packagePath": "/path/to/package"}' -q -- npx xcodebuildmcp@latest ``` - **`swift_package_list`**: Lists running Swift package processes. ```bash npx reloaderoo@latest inspect call-tool swift_package_list --params '{}' -q -- npx xcodebuildmcp@latest ``` - **`swift_package_run`**: Runs a Swift package executable. ```bash npx reloaderoo@latest inspect call-tool swift_package_run --params '{"packagePath": "/path/to/package"}' -q -- npx xcodebuildmcp@latest ``` - **`swift_package_stop`**: Stops a running Swift package process. ```bash npx reloaderoo@latest inspect call-tool swift_package_stop --params '{"pid": 12345}' -q -- npx xcodebuildmcp@latest ``` - **`swift_package_test`**: Tests a Swift package. ```bash npx reloaderoo@latest inspect call-tool swift_package_test --params '{"packagePath": "/path/to/package"}' -q -- npx xcodebuildmcp@latest ``` ## System Doctor - **`doctor`**: Runs system diagnostics. ```bash npx reloaderoo@latest inspect call-tool doctor --params '{}' -q -- npx xcodebuildmcp@latest ``` ## UI Testing & Automation - **`button`**: Simulates a hardware button press. ```bash npx reloaderoo@latest inspect call-tool button --params '{"simulatorUuid": "SIMULATOR_UUID", "buttonType": "home"}' -q -- npx xcodebuildmcp@latest ``` - **`describe_ui`**: Gets the UI hierarchy of the current screen. ```bash npx reloaderoo@latest inspect call-tool describe_ui --params '{"simulatorUuid": "SIMULATOR_UUID"}' -q -- npx xcodebuildmcp@latest ``` - **`gesture`**: Performs a pre-defined gesture. ```bash npx reloaderoo@latest inspect call-tool gesture --params '{"simulatorUuid": "SIMULATOR_UUID", "preset": "scroll-up"}' -q -- npx xcodebuildmcp@latest ``` - **`key_press`**: Simulates a key press. ```bash npx reloaderoo@latest inspect call-tool key_press --params '{"simulatorUuid": "SIMULATOR_UUID", "keyCode": 40}' -q -- npx xcodebuildmcp@latest ``` - **`key_sequence`**: Simulates a sequence of key presses. ```bash npx reloaderoo@latest inspect call-tool key_sequence --params '{"simulatorUuid": "SIMULATOR_UUID", "keyCodes": [40, 42, 44]}' -q -- npx xcodebuildmcp@latest ``` - **`long_press`**: Performs a long press at coordinates. ```bash npx reloaderoo@latest inspect call-tool long_press --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200, "duration": 1500}' -q -- npx xcodebuildmcp@latest ``` - **`screenshot`**: Takes a screenshot. ```bash npx reloaderoo@latest inspect call-tool screenshot --params '{"simulatorUuid": "SIMULATOR_UUID"}' -q -- npx xcodebuildmcp@latest ``` - **`swipe`**: Performs a swipe gesture. ```bash npx reloaderoo@latest inspect call-tool swipe --params '{"simulatorUuid": "SIMULATOR_UUID", "x1": 100, "y1": 200, "x2": 100, "y2": 400}' -q -- npx xcodebuildmcp@latest ``` - **`tap`**: Performs a tap at coordinates. ```bash npx reloaderoo@latest inspect call-tool tap --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200}' -q -- npx xcodebuildmcp@latest ``` - **`touch`**: Simulates a touch down or up event. ```bash npx reloaderoo@latest inspect call-tool touch --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200, "down": true}' -q -- npx xcodebuildmcp@latest ``` - **`type_text`**: Types text into the focused element. ```bash npx reloaderoo@latest inspect call-tool type_text --params '{"simulatorUuid": "SIMULATOR_UUID", "text": "Hello, World!"}' -q -- npx xcodebuildmcp@latest ``` ## Resources - **Read devices resource**: ```bash npx reloaderoo@latest inspect read-resource "xcodebuildmcp://devices" -q -- npx xcodebuildmcp@latest ``` - **Read simulators resource**: ```bash npx reloaderoo@latest inspect read-resource "xcodebuildmcp://simulators" -q -- npx xcodebuildmcp@latest ``` - **Read doctor resource**: ```bash npx reloaderoo@latest inspect read-resource "xcodebuildmcp://doctor" -q -- npx xcodebuildmcp@latest ``` ``` -------------------------------------------------------------------------------- /src/mcp/tools/device/list_devices.ts: -------------------------------------------------------------------------------- ```typescript /** * Device Workspace Plugin: List Devices * * Lists connected physical Apple devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro) * with their UUIDs, names, and connection status. Use this to discover physical devices for testing. */ import { z } from 'zod'; import type { 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'; import { promises as fs } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; // Define schema as ZodObject (empty schema since this tool takes no parameters) const listDevicesSchema = z.object({}); // Use z.infer for type safety type ListDevicesParams = z.infer<typeof listDevicesSchema>; /** * Business logic for listing connected devices */ export async function list_devicesLogic( params: ListDevicesParams, executor: CommandExecutor, pathDeps?: { tmpdir?: () => string; join?: (...paths: string[]) => string }, fsDeps?: { readFile?: (path: string, encoding?: string) => Promise<string>; unlink?: (path: string) => Promise<void>; }, ): Promise<ToolResponse> { log('info', 'Starting device discovery'); try { // Try modern devicectl with JSON output first (iOS 17+, Xcode 15+) const tempDir = pathDeps?.tmpdir ? pathDeps.tmpdir() : tmpdir(); const timestamp = pathDeps?.join ? '123' : Date.now(); // Use fixed timestamp for tests const tempJsonPath = pathDeps?.join ? pathDeps.join(tempDir, `devicectl-${timestamp}.json`) : join(tempDir, `devicectl-${timestamp}.json`); const devices = []; let useDevicectl = false; try { const result = await executor( ['xcrun', 'devicectl', 'list', 'devices', '--json-output', tempJsonPath], 'List Devices (devicectl with JSON)', true, undefined, ); if (result.success) { useDevicectl = true; // Read and parse the JSON file const jsonContent = fsDeps?.readFile ? await fsDeps.readFile(tempJsonPath, 'utf8') : await fs.readFile(tempJsonPath, 'utf8'); const deviceCtlData: unknown = JSON.parse(jsonContent); // Type guard to validate the device data structure const isValidDeviceData = (data: unknown): data is { result?: { devices?: unknown[] } } => { return ( typeof data === 'object' && data !== null && 'result' in data && typeof (data as { result?: unknown }).result === 'object' && (data as { result?: unknown }).result !== null && 'devices' in ((data as { result?: unknown }).result as { devices?: unknown }) && Array.isArray( ((data as { result?: unknown }).result as { devices?: unknown[] }).devices, ) ); }; if (isValidDeviceData(deviceCtlData) && deviceCtlData.result?.devices) { for (const deviceRaw of deviceCtlData.result.devices) { // Type guard for device object const isValidDevice = ( device: unknown, ): device is { visibilityClass?: string; connectionProperties?: { pairingState?: string; tunnelState?: string; transportType?: string; }; deviceProperties?: { platformIdentifier?: string; name?: string; osVersionNumber?: string; developerModeStatus?: string; marketingName?: string; }; hardwareProperties?: { productType?: string; cpuType?: { name?: string }; }; identifier?: string; } => { if (typeof device !== 'object' || device === null) { return false; } const dev = device as Record<string, unknown>; // Check if identifier exists and is a string (most critical property) if (typeof dev.identifier !== 'string' && dev.identifier !== undefined) { return false; } // Check visibilityClass if present if (dev.visibilityClass !== undefined && typeof dev.visibilityClass !== 'string') { return false; } // Check connectionProperties structure if present if (dev.connectionProperties !== undefined) { if ( typeof dev.connectionProperties !== 'object' || dev.connectionProperties === null ) { return false; } const connProps = dev.connectionProperties as Record<string, unknown>; if ( connProps.pairingState !== undefined && typeof connProps.pairingState !== 'string' ) { return false; } if ( connProps.tunnelState !== undefined && typeof connProps.tunnelState !== 'string' ) { return false; } if ( connProps.transportType !== undefined && typeof connProps.transportType !== 'string' ) { return false; } } // Check deviceProperties structure if present if (dev.deviceProperties !== undefined) { if (typeof dev.deviceProperties !== 'object' || dev.deviceProperties === null) { return false; } const devProps = dev.deviceProperties as Record<string, unknown>; if ( devProps.platformIdentifier !== undefined && typeof devProps.platformIdentifier !== 'string' ) { return false; } if (devProps.name !== undefined && typeof devProps.name !== 'string') { return false; } if ( devProps.osVersionNumber !== undefined && typeof devProps.osVersionNumber !== 'string' ) { return false; } if ( devProps.developerModeStatus !== undefined && typeof devProps.developerModeStatus !== 'string' ) { return false; } if ( devProps.marketingName !== undefined && typeof devProps.marketingName !== 'string' ) { return false; } } // Check hardwareProperties structure if present if (dev.hardwareProperties !== undefined) { if (typeof dev.hardwareProperties !== 'object' || dev.hardwareProperties === null) { return false; } const hwProps = dev.hardwareProperties as Record<string, unknown>; if (hwProps.productType !== undefined && typeof hwProps.productType !== 'string') { return false; } if (hwProps.cpuType !== undefined) { if (typeof hwProps.cpuType !== 'object' || hwProps.cpuType === null) { return false; } const cpuType = hwProps.cpuType as Record<string, unknown>; if (cpuType.name !== undefined && typeof cpuType.name !== 'string') { return false; } } } return true; }; if (!isValidDevice(deviceRaw)) continue; const device = deviceRaw; // Skip simulators or unavailable devices if ( device.visibilityClass === 'Simulator' || !device.connectionProperties?.pairingState ) { continue; } // Determine platform from platformIdentifier let platform = 'Unknown'; const platformId = device.deviceProperties?.platformIdentifier?.toLowerCase() ?? ''; if (typeof platformId === 'string') { if (platformId.includes('ios') || platformId.includes('iphone')) { platform = 'iOS'; } else if (platformId.includes('ipad')) { platform = 'iPadOS'; } else if (platformId.includes('watch')) { platform = 'watchOS'; } else if (platformId.includes('tv') || platformId.includes('apple tv')) { platform = 'tvOS'; } else if (platformId.includes('vision')) { platform = 'visionOS'; } } // Determine connection state const pairingState = device.connectionProperties?.pairingState ?? ''; const tunnelState = device.connectionProperties?.tunnelState ?? ''; const transportType = device.connectionProperties?.transportType ?? ''; let state = 'Unknown'; // Consider a device available if it's paired, regardless of tunnel state // This allows WiFi-connected devices to be used even if tunnelState isn't "connected" if (pairingState === 'paired') { if (tunnelState === 'connected') { state = 'Available'; } else { // Device is paired but tunnel state may be different for WiFi connections // Still mark as available since devicectl commands can work with paired devices state = 'Available (WiFi)'; } } else { state = 'Unpaired'; } devices.push({ name: device.deviceProperties?.name ?? 'Unknown Device', identifier: device.identifier ?? 'Unknown', platform: platform, model: device.deviceProperties?.marketingName ?? device.hardwareProperties?.productType, osVersion: device.deviceProperties?.osVersionNumber, state: state, connectionType: transportType, trustState: pairingState, developerModeStatus: device.deviceProperties?.developerModeStatus, productType: device.hardwareProperties?.productType, cpuArchitecture: device.hardwareProperties?.cpuType?.name, }); } } } } catch { log('info', 'devicectl with JSON failed, trying xctrace fallback'); } finally { // Clean up temp file try { if (fsDeps?.unlink) { await fsDeps.unlink(tempJsonPath); } else { await fs.unlink(tempJsonPath); } } catch { // Ignore cleanup errors } } // If devicectl failed or returned no devices, fallback to xctrace if (!useDevicectl || devices.length === 0) { const result = await executor( ['xcrun', 'xctrace', 'list', 'devices'], 'List Devices (xctrace)', true, undefined, ); if (!result.success) { return { content: [ { type: 'text', text: `Failed to list devices: ${result.error}\n\nMake sure Xcode is installed and devices are connected and trusted.`, }, ], isError: true, }; } // Return raw xctrace output without parsing return { content: [ { type: 'text', text: `Device listing (xctrace output):\n\n${result.output}\n\nNote: For better device information, please upgrade to Xcode 15 or later which supports the modern devicectl command.`, }, ], }; } // Format the response let responseText = 'Connected Devices:\n\n'; // Filter out duplicates const uniqueDevices = devices.filter( (device, index, self) => index === self.findIndex((d) => d.identifier === device.identifier), ); if (uniqueDevices.length === 0) { responseText += 'No physical Apple devices found.\n\n'; responseText += 'Make sure:\n'; responseText += '1. Devices are connected via USB or WiFi\n'; responseText += '2. Devices are unlocked and trusted\n'; responseText += '3. "Trust this computer" has been accepted on the device\n'; responseText += '4. Developer mode is enabled on the device (iOS 16+)\n'; responseText += '5. Xcode is properly installed\n\n'; responseText += 'For simulators, use the list_sims tool instead.\n'; } else { // Group devices by availability status const availableDevices = uniqueDevices.filter( (d) => d.state === 'Available' || d.state === 'Available (WiFi)' || d.state === 'Connected', ); const pairedDevices = uniqueDevices.filter((d) => d.state === 'Paired (not connected)'); const unpairedDevices = uniqueDevices.filter((d) => d.state === 'Unpaired'); if (availableDevices.length > 0) { responseText += '✅ Available Devices:\n'; for (const device of availableDevices) { responseText += `\n📱 ${device.name}\n`; responseText += ` UDID: ${device.identifier}\n`; responseText += ` Model: ${device.model ?? 'Unknown'}\n`; if (device.productType) { responseText += ` Product Type: ${device.productType}\n`; } responseText += ` Platform: ${device.platform} ${device.osVersion ?? ''}\n`; if (device.cpuArchitecture) { responseText += ` CPU Architecture: ${device.cpuArchitecture}\n`; } responseText += ` Connection: ${device.connectionType ?? 'Unknown'}\n`; if (device.developerModeStatus) { responseText += ` Developer Mode: ${device.developerModeStatus}\n`; } } responseText += '\n'; } if (pairedDevices.length > 0) { responseText += '🔗 Paired but Not Connected:\n'; for (const device of pairedDevices) { responseText += `\n📱 ${device.name}\n`; responseText += ` UDID: ${device.identifier}\n`; responseText += ` Model: ${device.model ?? 'Unknown'}\n`; responseText += ` Platform: ${device.platform} ${device.osVersion ?? ''}\n`; } responseText += '\n'; } if (unpairedDevices.length > 0) { responseText += '❌ Unpaired Devices:\n'; for (const device of unpairedDevices) { responseText += `- ${device.name} (${device.identifier})\n`; } responseText += '\n'; } } // Add next steps const availableDevicesExist = uniqueDevices.some( (d) => d.state === 'Available' || d.state === 'Available (WiFi)' || d.state === 'Connected', ); if (availableDevicesExist) { responseText += 'Next Steps:\n'; responseText += "1. Build for device: build_device({ scheme: 'SCHEME', deviceId: 'DEVICE_UDID' })\n"; responseText += "2. Run tests: test_device({ scheme: 'SCHEME', deviceId: 'DEVICE_UDID' })\n"; responseText += "3. Get app path: get_device_app_path({ scheme: 'SCHEME' })\n\n"; responseText += 'Note: Use the device ID/UDID from above when required by other tools.\n'; } else if (uniqueDevices.length > 0) { responseText += 'Note: No devices are currently available for testing. Make sure devices are:\n'; responseText += '- Connected via USB\n'; responseText += '- Unlocked and trusted\n'; responseText += '- Have developer mode enabled (iOS 16+)\n'; } return { content: [ { type: 'text', text: responseText, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); log('error', `Error listing devices: ${errorMessage}`); return { content: [ { type: 'text', text: `Failed to list devices: ${errorMessage}`, }, ], isError: true, }; } } export default { name: 'list_devices', description: 'Lists connected physical Apple devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro) with their UUIDs, names, and connection status. Use this to discover physical devices for testing.', schema: listDevicesSchema.shape, // MCP SDK compatibility handler: createTypedTool(listDevicesSchema, list_devicesLogic, getDefaultCommandExecutor), }; ```