This is page 2 of 12. 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
│ ├── README.md
│ ├── release.yml
│ ├── sentry.yml
│ └── stale.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
├── docs
│ ├── CONFIGURATION.md
│ ├── DAP_BACKEND_IMPLEMENTATION_PLAN.md
│ ├── DEBUGGING_ARCHITECTURE.md
│ ├── DEMOS.md
│ ├── dev
│ │ ├── ARCHITECTURE.md
│ │ ├── CODE_QUALITY.md
│ │ ├── CONTRIBUTING.md
│ │ ├── ESLINT_TYPE_SAFETY.md
│ │ ├── MANUAL_TESTING.md
│ │ ├── NODEJS_2025.md
│ │ ├── PLUGIN_DEVELOPMENT.md
│ │ ├── README.md
│ │ ├── RELEASE_PROCESS.md
│ │ ├── RELOADEROO_FOR_XCODEBUILDMCP.md
│ │ ├── RELOADEROO_XCODEBUILDMCP_PRIMER.md
│ │ ├── RELOADEROO.md
│ │ ├── session_management_plan.md
│ │ ├── session-aware-migration-todo.md
│ │ ├── SMITHERY.md
│ │ ├── TEST_RUNNER_ENV_IMPLEMENTATION_PLAN.md
│ │ ├── TESTING.md
│ │ └── ZOD_MIGRATION_GUIDE.md
│ ├── DEVICE_CODE_SIGNING.md
│ ├── GETTING_STARTED.md
│ ├── investigations
│ │ ├── issue-154-screenshot-downscaling.md
│ │ ├── issue-163.md
│ │ ├── issue-debugger-attach-stopped.md
│ │ └── issue-describe-ui-empty-after-debugger-resume.md
│ ├── OVERVIEW.md
│ ├── PRIVACY.md
│ ├── README.md
│ ├── SESSION_DEFAULTS.md
│ ├── TOOLS.md
│ └── TROUBLESHOOTING.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
│ │ ├── .gitignore
│ │ ├── 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
│ │ └── MCPTestTests
│ │ └── MCPTestTests.swift
│ └── 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
│ ├── generate-loaders.ts
│ ├── generate-version.ts
│ ├── release.sh
│ ├── tools-cli.ts
│ ├── update-tools-docs.ts
│ └── verify-smithery-bundle.sh
├── server.json
├── smithery.config.js
├── smithery.yaml
├── src
│ ├── core
│ │ ├── __tests__
│ │ │ └── resources.test.ts
│ │ ├── generated-plugins.ts
│ │ ├── generated-resources.ts
│ │ ├── plugin-registry.ts
│ │ ├── plugin-types.ts
│ │ └── resources.ts
│ ├── doctor-cli.ts
│ ├── index.ts
│ ├── mcp
│ │ ├── resources
│ │ │ ├── __tests__
│ │ │ │ ├── devices.test.ts
│ │ │ │ ├── doctor.test.ts
│ │ │ │ ├── session-status.test.ts
│ │ │ │ └── simulators.test.ts
│ │ │ ├── devices.ts
│ │ │ ├── doctor.ts
│ │ │ ├── session-status.ts
│ │ │ └── simulators.ts
│ │ └── tools
│ │ ├── debugging
│ │ │ ├── debug_attach_sim.ts
│ │ │ ├── debug_breakpoint_add.ts
│ │ │ ├── debug_breakpoint_remove.ts
│ │ │ ├── debug_continue.ts
│ │ │ ├── debug_detach.ts
│ │ │ ├── debug_lldb_command.ts
│ │ │ ├── debug_stack.ts
│ │ │ ├── debug_variables.ts
│ │ │ └── index.ts
│ │ ├── 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
│ │ ├── 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
│ │ ├── bootstrap.ts
│ │ └── server.ts
│ ├── smithery.ts
│ ├── test-utils
│ │ └── mock-executors.ts
│ ├── types
│ │ └── common.ts
│ ├── utils
│ │ ├── __tests__
│ │ │ ├── build-utils-suppress-warnings.test.ts
│ │ │ ├── build-utils.test.ts
│ │ │ ├── debugger-simctl.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
│ │ │ └── workflow-selection.test.ts
│ │ ├── axe
│ │ │ └── index.ts
│ │ ├── axe-helpers.ts
│ │ ├── build
│ │ │ └── index.ts
│ │ ├── build-utils.ts
│ │ ├── capabilities.ts
│ │ ├── command.ts
│ │ ├── CommandExecutor.ts
│ │ ├── debugger
│ │ │ ├── __tests__
│ │ │ │ └── debugger-manager-dap.test.ts
│ │ │ ├── backends
│ │ │ │ ├── __tests__
│ │ │ │ │ └── dap-backend.test.ts
│ │ │ │ ├── dap-backend.ts
│ │ │ │ ├── DebuggerBackend.ts
│ │ │ │ └── lldb-cli-backend.ts
│ │ │ ├── dap
│ │ │ │ ├── __tests__
│ │ │ │ │ └── transport-framing.test.ts
│ │ │ │ ├── adapter-discovery.ts
│ │ │ │ ├── transport.ts
│ │ │ │ └── types.ts
│ │ │ ├── debugger-manager.ts
│ │ │ ├── index.ts
│ │ │ ├── simctl.ts
│ │ │ ├── tool-context.ts
│ │ │ ├── types.ts
│ │ │ └── ui-automation-guard.ts
│ │ ├── environment.ts
│ │ ├── errors.ts
│ │ ├── execution
│ │ │ ├── index.ts
│ │ │ └── interactive-process.ts
│ │ ├── FileSystemExecutor.ts
│ │ ├── log_capture.ts
│ │ ├── log-capture
│ │ │ ├── device-log-sessions.ts
│ │ │ └── index.ts
│ │ ├── logger.ts
│ │ ├── logging
│ │ │ └── index.ts
│ │ ├── plugin-registry
│ │ │ └── index.ts
│ │ ├── responses
│ │ │ └── index.ts
│ │ ├── runtime-registry.ts
│ │ ├── schema-helpers.ts
│ │ ├── sentry.ts
│ │ ├── session-status.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
│ │ ├── workflow-selection.ts
│ │ ├── xcode.ts
│ │ ├── xcodemake
│ │ │ └── index.ts
│ │ └── xcodemake.ts
│ └── version.ts
├── tsconfig.json
├── tsconfig.test.json
├── tsconfig.tests.json
├── tsup.config.ts
├── vitest.config.ts
└── XcodeBuildMCP.code-workspace
```
# Files
--------------------------------------------------------------------------------
/docs/investigations/issue-debugger-attach-stopped.md:
--------------------------------------------------------------------------------
```markdown
# Investigation: Debugger attaches in stopped state after launch
## Summary
Reproduced: attaching the debugger leaves the simulator app in a stopped state. UI automation is blocked by the guard because the debugger reports `state=stopped`. The attach flow does not issue any resume/continue, so the process remains paused after attach.
## Symptoms
- After attaching debugger to Calculator, UI automation taps fail because the app is paused.
- UI guard blocks with `state=stopped` immediately after attach.
## Investigation Log
### 2025-02-14 - Repro (CalculatorApp on iPhone 17 simulator)
**Hypothesis:** Attach leaves the process stopped, which triggers the UI automation guard.
**Findings:** `debug_attach_sim` attached to a running CalculatorApp (DAP backend), then `tap` was blocked with `state=stopped`.
**Evidence:** `tap` returned "UI automation blocked: app is paused in debugger" with `state=stopped` and the current debug session ID.
**Conclusion:** Confirmed.
### 2025-02-14 - Code Review (attach flow)
**Hypothesis:** The attach implementation does not resume the process.
**Findings:** The attach flow never calls any resume/continue primitive.
- `debug_attach_sim` creates a session and returns without resuming.
- DAP backend attach flow (`initialize -> attach -> configurationDone`) has no `continue`.
- LLDB CLI backend uses `process attach --pid` and never `process continue`.
- UI automation guard blocks when state is `stopped`.
**Evidence:** `src/mcp/tools/debugging/debug_attach_sim.ts`, `src/utils/debugger/backends/dap-backend.ts`, `src/utils/debugger/backends/lldb-cli-backend.ts`, `src/utils/debugger/ui-automation-guard.ts`.
**Conclusion:** Confirmed. Stopped state originates from debugger attach semantics, and the tool never resumes.
## Root Cause
The debugger attach path halts the target process (standard debugger behavior) and there is no subsequent resume/continue step. This leaves the process in `stopped` state, which causes `guardUiAutomationAgainstStoppedDebugger` to block UI tools like `tap`.
## Recommendations
1. Add a first-class `debug_continue` tool backed by a backend-level `continue()` API to resume without relying on LLDB command evaluation.
2. Add an optional `continueOnAttach` (or `stopOnAttach`) parameter to `debug_attach_sim`, with a default suited for UI automation workflows.
3. Update guard messaging to recommend `debug_continue` (not `debug_lldb_command continue`, which is unreliable on DAP).
## Preventive Measures
- Document that UI tools require the target process to be running, and that debugger attach may pause execution by default.
- Add a state check or auto-resume option when attaching in automation contexts.
```
--------------------------------------------------------------------------------
/src/utils/__tests__/workflow-selection.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { z } from 'zod';
import { resolveSelectedWorkflows } from '../workflow-selection.ts';
import type { WorkflowGroup } from '../../core/plugin-types.ts';
function makeWorkflow(name: string): WorkflowGroup {
return {
directoryName: name,
workflow: {
name,
description: `${name} workflow`,
},
tools: [
{
name: `${name}-tool`,
description: `${name} tool`,
schema: { enabled: z.boolean().optional() },
async handler() {
return { content: [] };
},
},
],
};
}
function makeWorkflowMap(names: string[]): Map<string, WorkflowGroup> {
const map = new Map<string, WorkflowGroup>();
for (const name of names) {
map.set(name, makeWorkflow(name));
}
return map;
}
describe('resolveSelectedWorkflows', () => {
let originalDebug: string | undefined;
beforeEach(() => {
originalDebug = process.env.XCODEBUILDMCP_DEBUG;
});
afterEach(() => {
if (typeof originalDebug === 'undefined') {
delete process.env.XCODEBUILDMCP_DEBUG;
} else {
process.env.XCODEBUILDMCP_DEBUG = originalDebug;
}
});
it('adds doctor when debug is enabled and selection list is provided', () => {
process.env.XCODEBUILDMCP_DEBUG = 'true';
const workflows = makeWorkflowMap(['session-management', 'doctor', 'simulator']);
const result = resolveSelectedWorkflows(workflows, ['simulator']);
expect(result.selectedNames).toEqual(['session-management', 'doctor', 'simulator']);
expect(result.selectedWorkflows.map((workflow) => workflow.directoryName)).toEqual([
'session-management',
'doctor',
'simulator',
]);
});
it('does not add doctor when debug is disabled', () => {
process.env.XCODEBUILDMCP_DEBUG = 'false';
const workflows = makeWorkflowMap(['session-management', 'doctor', 'simulator']);
const result = resolveSelectedWorkflows(workflows, ['simulator']);
expect(result.selectedNames).toEqual(['session-management', 'simulator']);
expect(result.selectedWorkflows.map((workflow) => workflow.directoryName)).toEqual([
'session-management',
'simulator',
]);
});
it('returns all workflows when no selection list is provided', () => {
process.env.XCODEBUILDMCP_DEBUG = 'true';
const workflows = makeWorkflowMap(['session-management', 'doctor', 'simulator']);
const result = resolveSelectedWorkflows(workflows, []);
expect(result.selectedNames).toBeNull();
expect(result.selectedWorkflows.map((workflow) => workflow.directoryName)).toEqual([
'session-management',
'doctor',
'simulator',
]);
});
});
```
--------------------------------------------------------------------------------
/.cursor/BUGBOT.md:
--------------------------------------------------------------------------------
```markdown
# Bugbot Review Guide for XcodeBuildMCP
## Project Snapshot
XcodeBuildMCP is an MCP server exposing Xcode / Swift workflows as **tools** and **resources**.
Stack: TypeScript · Node.js · plugin-based auto-discovery (`src/mcp/tools`, `src/mcp/resources`).
For full details see [README.md](README.md) and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
---
## 1. Security Checklist — Critical
* No hard-coded secrets, tokens or DSNs.
* All shell commands must flow through `CommandExecutor` with validated arguments (no direct `child_process` calls).
* Paths must be sanitised via helpers in `src/utils/validation.ts`.
* Sentry breadcrumbs / logs must **NOT** include user PII.
---
## 2. Architecture Checklist — Critical
| Rule | Quick diff heuristic |
|------|----------------------|
| Dependency injection only | New `child_process` \| `fs` import ⇒ **critical** |
| Handler / Logic split | `handler` > 20 LOC or contains branching ⇒ **critical** |
| Plugin auto-registration | Manual `registerTool(...)` / `registerResource(...)` ⇒ **critical** |
Positive pattern skeleton:
```ts
// src/mcp/tools/foo-bar.ts
export async function fooBarLogic(
params: FooBarParams,
exec: CommandExecutor = getDefaultCommandExecutor(),
fs: FileSystemExecutor = getDefaultFileSystemExecutor(),
) {
// ...
}
export const handler = (p: FooBarParams) => fooBarLogic(p);
```
---
## 3. Testing Checklist
* **Ban on Vitest mocking** (`vi.mock`, `vi.fn`, `vi.spyOn`, `.mock*`) ⇒ critical. Use `createMockExecutor` / `createMockFileSystemExecutor`.
* Each tool must have tests covering happy-path **and** at least one failure path.
* Avoid the `any` type unless justified with an inline comment.
---
## 4. Documentation Checklist
* `docs/TOOLS.md` must exactly mirror the structure of `src/mcp/tools/**` (exclude `__tests__` and `*-shared`).
*Diff heuristic*: if a PR adds/removes a tool but does **not** change `docs/TOOLS.md` ⇒ **warning**.
* Update public docs when CLI parameters or tool names change.
---
## 5. Common Anti-Patterns (and fixes)
| Anti-pattern | Preferred approach |
|--------------|--------------------|
| Complex logic in `handler` | Move to `*Logic` function |
| Re-implementing logging | Use `src/utils/logger.ts` |
| Direct `fs` / `child_process` usage | Inject `FileSystemExecutor` / `CommandExecutor` |
| Chained re-exports | Export directly from source |
---
### How Bugbot Can Verify Rules
1. **Mocking violations**: search `*.test.ts` for `vi.` → critical.
2. **DI compliance**: search for direct `child_process` / `fs` imports outside executors.
3. **Docs accuracy**: compare `docs/TOOLS.md` against `src/mcp/tools/**`.
4. **Style**: ensure ESLint and Prettier pass (`npm run lint`, `npm run format:check`).
---
Happy reviewing 🚀
```
--------------------------------------------------------------------------------
/src/mcp/tools/session-management/__tests__/session_clear_defaults.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { sessionStore } from '../../../../utils/session-store.ts';
import plugin, { sessionClearDefaultsLogic } from '../session_clear_defaults.ts';
describe('session-clear-defaults tool', () => {
beforeEach(() => {
sessionStore.clear();
sessionStore.setDefaults({
scheme: 'MyScheme',
projectPath: '/path/to/proj.xcodeproj',
simulatorName: 'iPhone 16',
deviceId: 'DEVICE-123',
useLatestOS: true,
arch: 'arm64',
});
});
afterEach(() => {
sessionStore.clear();
});
describe('Export Field Validation (Literal)', () => {
it('should have correct name', () => {
expect(plugin.name).toBe('session-clear-defaults');
});
it('should have correct description', () => {
expect(plugin.description).toBe('Clear selected or all session defaults.');
});
it('should have handler function', () => {
expect(typeof plugin.handler).toBe('function');
});
it('should have schema object', () => {
expect(plugin.schema).toBeDefined();
expect(typeof plugin.schema).toBe('object');
});
});
describe('Handler Behavior', () => {
it('should clear specific keys when provided', async () => {
const result = await sessionClearDefaultsLogic({ keys: ['scheme', 'deviceId'] });
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Session defaults cleared');
const current = sessionStore.getAll();
expect(current.scheme).toBeUndefined();
expect(current.deviceId).toBeUndefined();
expect(current.projectPath).toBe('/path/to/proj.xcodeproj');
expect(current.simulatorName).toBe('iPhone 16');
expect(current.useLatestOS).toBe(true);
expect(current.arch).toBe('arm64');
});
it('should clear all when all=true', async () => {
const result = await sessionClearDefaultsLogic({ all: true });
expect(result.isError).toBe(false);
expect(result.content[0].text).toBe('Session defaults cleared');
const current = sessionStore.getAll();
expect(Object.keys(current).length).toBe(0);
});
it('should clear all when no params provided', async () => {
const result = await sessionClearDefaultsLogic({});
expect(result.isError).toBe(false);
const current = sessionStore.getAll();
expect(Object.keys(current).length).toBe(0);
});
it('should validate keys enum', async () => {
const result = (await plugin.handler({ keys: ['invalid' as any] })) as any;
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Parameter validation failed');
expect(result.content[0].text).toContain('keys');
});
});
});
```
--------------------------------------------------------------------------------
/docs/investigations/issue-154-screenshot-downscaling.md:
--------------------------------------------------------------------------------
```markdown
# Investigation: Optional Screenshot Downscaling (Issue #154)
## Summary
Investigation started; initial context gathered from the issue description. Context builder failed (Gemini CLI usage error), so manual exploration is proceeding.
## Symptoms
- Screenshots captured for UI automation are full-resolution by default.
- High-resolution screenshots increase multimodal token usage and cost.
## Investigation Log
### 2026-01-04 - Initial assessment
**Hypothesis:** Screenshot pipeline always emits full-resolution images and lacks an opt-in scaling path.
**Findings:** Issue describes full-res screenshots and requests optional downscaling. No code inspected yet.
**Evidence:** GitHub issue #154 body.
**Conclusion:** Needs codebase investigation.
### 2026-01-04 - Context builder attempt
**Hypothesis:** Use automated context discovery to map screenshot capture flow.
**Findings:** `context_builder` failed due to Gemini CLI usage error in this environment.
**Evidence:** Tool error output in session (Gemini CLI usage/help text).
**Conclusion:** Proceeding with manual code inspection.
### 2026-01-04 - Screenshot capture implementation
**Hypothesis:** Screenshot tool stores and returns full-resolution PNGs.
**Findings:** The `screenshot` tool captures a PNG, then immediately downscales/optimizes via `sips` to max 800px width, JPEG format, quality 75%, and returns the JPEG. Optimization is always attempted; on failure it falls back to original PNG.
**Evidence:** `src/mcp/tools/ui-testing/screenshot.ts` (sips `-Z 800`, `format jpeg`, `formatOptions 75`).
**Conclusion:** The current implementation already downscales by default; the gap is configurability (opt-in/out, size/quality controls) and documentation.
### 2026-01-04 - Git history check
**Hypothesis:** Recent commits might have added/changed screenshot optimization behavior.
**Findings:** Recent history shows tool annotations and session-awareness changes, but no indication of configurable screenshot scaling.
**Evidence:** `git log -n 5 -- src/mcp/tools/ui-testing/screenshot.ts`.
**Conclusion:** No recent change introduces optional scaling controls.
## Root Cause
The issue report assumes full-resolution screenshots, but the current `screenshot` tool already downsamples to 800px max width and JPEG 75% every time. There is no parameter to disable or tune this behavior, and docs do not mention the optimization.
## Recommendations
1. Document existing downscaling behavior and defaults in tool docs (and in the screenshot tool description).
2. Add optional parameters to `screenshot` for max width/quality/format or a boolean to disable optimization, preserving current defaults.
## Preventive Measures
- Add a section in docs/TOOLS.md or tool-specific docs describing image processing defaults and token tradeoffs.
```
--------------------------------------------------------------------------------
/docs/GETTING_STARTED.md:
--------------------------------------------------------------------------------
```markdown
# Getting Started
## Prerequisites
- macOS 14.5 or later
- Xcode 16.x or later
- Node.js 18.x or later
## Install options
### Smithery (recommended)
```bash
npx -y @smithery/cli@latest install cameroncooke/xcodebuildmcp --client client-name
```
### One click install
If you are using Cursor or VS Code you can use the quick install links below.
[](https://cursor.com/en/install-mcp?name=XcodeBuildMCP&config=eyJ0eXBlIjoic3RkaW8iLCJjb21tYW5kIjoibnB4IC15IHhjb2RlYnVpbGRtY3BAbGF0ZXN0IiwiZW52Ijp7IklOQ1JFTUVOVEFMX0JVSUxEU19FTkFCTEVEIjoiZmFsc2UiLCJYQ09ERUJVSUxETUNQX1NFTlRSWV9ESVNBQkxFRCI6ImZhbHNlIn19)
[<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](https://insiders.vscode.dev/redirect/mcp/install?name=XcodeBuildMCP&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22xcodebuildmcp%40latest%22%5D%7D)
[<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect/mcp/install?name=XcodeBuildMCP&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22xcodebuildmcp%40latest%22%5D%7D&quality=insiders)
### Manual installation
Most MCP clients use JSON configuration. Add the following to your client configuration under `mcpServers`:
```json
"XcodeBuildMCP": {
"command": "npx",
"args": [
"-y",
"xcodebuildmcp@latest"
]
}
```
## Client-specific configuration
### OpenAI Codex CLI
Codex uses TOML for MCP configuration. Add this to your Codex CLI config file:
```toml
[mcp_servers.XcodeBuildMCP]
command = "npx"
args = ["-y", "xcodebuildmcp@latest"]
env = { "INCREMENTAL_BUILDS_ENABLED" = "false", "XCODEBUILDMCP_SENTRY_DISABLED" = "false" }
```
If you see tool calls timing out (for example, `timed out awaiting tools/call after 60s`), increase the timeout:
```toml
tool_timeout_sec = 600
```
For more info see the OpenAI Codex configuration docs:
https://github.com/openai/codex/blob/main/docs/config.md#connecting-to-mcp-servers
### Claude Code CLI
```bash
# Add XcodeBuildMCP server to Claude Code
claude mcp add XcodeBuildMCP npx xcodebuildmcp@latest
# Or with environment variables
claude mcp add XcodeBuildMCP npx xcodebuildmcp@latest -e INCREMENTAL_BUILDS_ENABLED=false -e XCODEBUILDMCP_SENTRY_DISABLED=false
```
Note: XcodeBuildMCP requests xcodebuild to skip macro validation to avoid Swift Macro build errors.
## Next steps
- Configuration options: [CONFIGURATION.md](CONFIGURATION.md)
- Session defaults and opt-out: [SESSION_DEFAULTS.md](SESSION_DEFAULTS.md)
- Tools reference: [TOOLS.md](TOOLS.md)
- Troubleshooting: [TROUBLESHOOTING.md](TROUBLESHOOTING.md)
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from 'vitest';
import * as z from 'zod';
import eraseSims, { erase_simsLogic } from '../erase_sims.ts';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
describe('erase_sims tool (single simulator)', () => {
describe('Export Field Validation (Literal)', () => {
it('should have correct name', () => {
expect(eraseSims.name).toBe('erase_sims');
});
it('should have correct description', () => {
expect(eraseSims.description).toBe('Erases a simulator by UDID.');
});
it('should have handler function', () => {
expect(typeof eraseSims.handler).toBe('function');
});
it('should validate schema fields (shape only)', () => {
const schema = z.object(eraseSims.schema);
expect(schema.safeParse({ shutdownFirst: true }).success).toBe(true);
expect(schema.safeParse({}).success).toBe(true);
});
});
describe('Single mode', () => {
it('erases a simulator successfully', async () => {
const mock = createMockExecutor({ success: true, output: 'OK' });
const res = await erase_simsLogic({ simulatorId: 'UD1' }, mock);
expect(res).toEqual({
content: [{ type: 'text', text: 'Successfully erased simulator UD1' }],
});
});
it('returns failure when erase fails', async () => {
const mock = createMockExecutor({ success: false, error: 'Booted device' });
const res = await erase_simsLogic({ simulatorId: 'UD1' }, mock);
expect(res).toEqual({
content: [{ type: 'text', text: 'Failed to erase simulator: Booted device' }],
});
});
it('adds tool hint when booted error occurs without shutdownFirst', async () => {
const bootedError =
'An error was encountered processing the command (domain=com.apple.CoreSimulator.SimError, code=405):\nUnable to erase contents and settings in current state: Booted\n';
const mock = createMockExecutor({ success: false, error: bootedError });
const res = await erase_simsLogic({ simulatorId: 'UD1' }, mock);
expect((res.content?.[1] as any).text).toContain('Tool hint');
expect((res.content?.[1] as any).text).toContain('shutdownFirst: true');
});
it('performs shutdown first when shutdownFirst=true', async () => {
const calls: any[] = [];
const exec = async (cmd: string[]) => {
calls.push(cmd);
return { success: true, output: 'OK', error: '', process: { pid: 1 } as any };
};
const res = await erase_simsLogic({ simulatorId: 'UD1', shutdownFirst: true }, exec as any);
expect(calls).toEqual([
['xcrun', 'simctl', 'shutdown', 'UD1'],
['xcrun', 'simctl', 'erase', 'UD1'],
]);
expect(res).toEqual({
content: [{ type: 'text', text: 'Successfully erased simulator UD1' }],
});
});
});
});
```
--------------------------------------------------------------------------------
/src/mcp/tools/logging/start_sim_log_cap.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Logging Plugin: Start Simulator Log Capture
*
* Starts capturing logs from a specified simulator.
*/
import * as z from 'zod';
import { startLogCapture } from '../../../utils/log-capture/index.ts';
import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/command.ts';
import { ToolResponse, createTextContent } from '../../../types/common.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
// Define schema as ZodObject
const startSimLogCapSchema = z.object({
simulatorId: z
.uuid()
.describe('UUID of the simulator to capture logs from (obtained from list_simulators).'),
bundleId: z.string().describe('Bundle identifier of the app to capture logs for.'),
captureConsole: z
.boolean()
.optional()
.describe('Whether to capture console output (requires app relaunch).'),
});
// Use z.infer for type safety
type StartSimLogCapParams = z.infer<typeof startSimLogCapSchema>;
export async function start_sim_log_capLogic(
params: StartSimLogCapParams,
_executor: CommandExecutor = getDefaultCommandExecutor(),
logCaptureFunction: typeof startLogCapture = startLogCapture,
): Promise<ToolResponse> {
const captureConsole = params.captureConsole ?? false;
const { sessionId, error } = await logCaptureFunction(
{
simulatorUuid: params.simulatorId,
bundleId: params.bundleId,
captureConsole,
},
_executor,
);
if (error) {
return {
content: [createTextContent(`Error starting log capture: ${error}`)],
isError: true,
};
}
return {
content: [
createTextContent(
`Log capture started successfully. Session ID: ${sessionId}.\n\n${captureConsole ? 'Note: Your app was relaunched to capture console output.' : 'Note: Only structured logs are being captured.'}\n\nNext Steps:\n1. Interact with your simulator and app.\n2. Use 'stop_sim_log_cap' with session ID '${sessionId}' to stop capture and retrieve logs.`,
),
],
};
}
const publicSchemaObject = z.strictObject(
startSimLogCapSchema.omit({ simulatorId: true } as const).shape,
);
export default {
name: 'start_sim_log_cap',
description:
'Starts capturing logs from a specified simulator. Returns a session ID. By default, captures only structured logs.',
schema: getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: startSimLogCapSchema,
}),
annotations: {
title: 'Start Simulator Log Capture',
destructiveHint: true,
},
handler: createSessionAwareTool<StartSimLogCapParams>({
internalSchema: startSimLogCapSchema as unknown as z.ZodType<StartSimLogCapParams, unknown>,
logicFunction: start_sim_log_capLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
}),
};
```
--------------------------------------------------------------------------------
/src/types/common.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Common type definitions used across the server
*
* This module provides core type definitions and interfaces used throughout the codebase.
* It establishes a consistent type system for platform identification, tool responses,
* and other shared concepts.
*
* Responsibilities:
* - Defining the XcodePlatform enum for platform identification
* - Establishing the ToolResponse interface for standardized tool outputs
* - Providing ToolResponseContent types for different response formats
* - Supporting error handling with standardized error response types
*/
/**
* Enum representing Xcode build platforms.
*/
export enum XcodePlatform {
macOS = 'macOS',
iOS = 'iOS',
iOSSimulator = 'iOS Simulator',
watchOS = 'watchOS',
watchOSSimulator = 'watchOS Simulator',
tvOS = 'tvOS',
tvOSSimulator = 'tvOS Simulator',
visionOS = 'visionOS',
visionOSSimulator = 'visionOS Simulator',
}
/**
* ToolResponse - Standard response format for tools
* Compatible with MCP CallToolResult interface from the SDK
*/
export interface ToolResponse {
content: ToolResponseContent[];
isError?: boolean;
_meta?: Record<string, unknown>;
[key: string]: unknown; // Index signature to match CallToolResult
}
/**
* Contents that can be included in a tool response
*/
export type ToolResponseContent =
| {
type: 'text';
text: string;
[key: string]: unknown; // Index signature to match ContentItem
}
| {
type: 'image';
data: string; // Base64-encoded image data (without URI scheme prefix)
mimeType: string; // e.g., 'image/png', 'image/jpeg'
[key: string]: unknown; // Index signature to match ContentItem
};
export function createTextContent(text: string): { type: 'text'; text: string } {
return { type: 'text', text };
}
export function createImageContent(
data: string,
mimeType: string,
): { type: 'image'; data: string; mimeType: string } {
return { type: 'image', data, mimeType };
}
/**
* ValidationResult - Result of parameter validation operations
*/
export interface ValidationResult {
isValid: boolean;
errorResponse?: ToolResponse;
warningResponse?: ToolResponse;
}
/**
* CommandResponse - Generic result of command execution
*/
export interface CommandResponse {
success: boolean;
output: string;
error?: string;
process?: unknown; // ChildProcess from node:child_process
}
/**
* Interface for shared build parameters
*/
export interface SharedBuildParams {
workspacePath?: string;
projectPath?: string;
scheme: string;
configuration: string;
derivedDataPath?: string;
extraArgs?: string[];
}
/**
* Interface for platform-specific build options
*/
export interface PlatformBuildOptions {
platform: XcodePlatform;
simulatorName?: string;
simulatorId?: string;
deviceId?: string;
useLatestOS?: boolean;
arch?: string;
logPrefix: string;
}
```
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
```yaml
name: Bug Report
description: Report a bug or issue with XcodeBuildMCP
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report an issue with XcodeBuildMCP!
- type: textarea
id: description
attributes:
label: Bug Description
description: A description of the bug or issue you're experiencing.
placeholder: When trying to build my iOS app using the AI assistant...
validations:
required: true
- type: textarea
id: debug
attributes:
label: Debug Output
description: Ask your agent "Run the XcodeBuildMCP `doctor` tool and return the output as markdown verbatim" and then copy paste it here.
placeholder: |
```
XcodeBuildMCP Doctor
Generated: 2025-08-11T17:42:29.812Z
Server Version: 1.11.2
## System Information
- platform: darwin
- release: 25.0.0
- arch: arm64
...
```
validations:
required: true
- type: input
id: editor-client
attributes:
label: Editor/Client
description: The editor or MCP client you're using
placeholder: Cursor 0.49.1
validations:
required: true
- type: input
id: mcp-server-version
attributes:
label: MCP Server Version
description: The version of XcodeBuildMCP you're using
placeholder: 1.2.2
validations:
required: true
- type: input
id: llm
attributes:
label: LLM
description: The AI model you're using
placeholder: Claude 3.5 Sonnet
validations:
required: true
- type: textarea
id: mcp-config
attributes:
label: MCP Configuration
description: Your MCP configuration file (if applicable)
placeholder: |
```json
{
"mcpServers": {
"XcodeBuildMCP": {...}
}
}
```
render: json
- type: textarea
id: steps
attributes:
label: Steps to Reproduce
description: Steps to reproduce the behavior
placeholder: |
1. What you asked the AI agent to do
2. What the AI agent attempted to do
3. What failed or didn't work as expected
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: What you expected to happen
placeholder: The AI should have been able to...
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Behavior
description: What actually happened
placeholder: Instead, the AI...
validations:
required: true
- type: textarea
id: error
attributes:
label: Error Messages
description: Any error messages or unexpected output
placeholder: Error message or output from the AI
render: shell
```
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
```json
{
"$schema": "https://raw.githubusercontent.com/microsoft/vscode/master/extensions/npm/schemas/v1.1.1/tasks.schema.json",
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "npm",
"script": "build",
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
},
"problemMatcher": [
"$tsc"
]
},
{
"label": "run",
"type": "npm",
"script": "inspect",
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "new"
}
},
{
"label": "test",
"type": "npm",
"script": "test",
"group": {
"kind": "test",
"isDefault": true
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
}
},
{
"label": "lint",
"type": "npm",
"script": "lint",
"group": "build",
"presentation": {
"echo": true,
"reveal": "silent",
"focus": false,
"panel": "shared"
},
"problemMatcher": [
"$eslint-stylish"
]
},
{
"label": "lint:fix",
"type": "npm",
"script": "lint:fix",
"group": "build"
},
{
"label": "format",
"type": "npm",
"script": "format",
"group": "build"
},
{
"label": "typecheck (watch)",
"type": "shell",
"command": "npx tsc --noEmit --watch",
"isBackground": true,
"problemMatcher": [
"$tsc-watch"
],
"group": "build"
},
{
"label": "dev (watch)",
"type": "npm",
"script": "dev",
"isBackground": true,
"group": "build",
"presentation": {
"panel": "dedicated",
"reveal": "always"
}
},
{
"label": "build: dev doctor",
"dependsOn": [
"lint",
"typecheck (watch)"
],
"group": {
"kind": "build",
"isDefault": false
}
}
]
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/device/stop_app_device.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Device Workspace Plugin: Stop App Device
*
* Stops an app running on a physical Apple device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro).
* Requires deviceId and processId.
*/
import * as z from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
// Define schema as ZodObject
const stopAppDeviceSchema = z.object({
deviceId: z.string().describe('UDID of the device (obtained from list_devices)'),
processId: z.number().describe('Process ID (PID) of the app to stop'),
});
// Use z.infer for type safety
type StopAppDeviceParams = z.infer<typeof stopAppDeviceSchema>;
const publicSchemaObject = stopAppDeviceSchema.omit({ deviceId: true } as const);
export async function stop_app_deviceLogic(
params: StopAppDeviceParams,
executor: CommandExecutor,
): Promise<ToolResponse> {
const { deviceId, processId } = params;
log('info', `Stopping app with PID ${processId} on device ${deviceId}`);
try {
const result = await executor(
[
'xcrun',
'devicectl',
'device',
'process',
'terminate',
'--device',
deviceId,
'--pid',
processId.toString(),
],
'Stop app on device',
true, // useShell
undefined, // env
);
if (!result.success) {
return {
content: [
{
type: 'text',
text: `Failed to stop app: ${result.error}`,
},
],
isError: true,
};
}
return {
content: [
{
type: 'text',
text: `✅ App stopped successfully\n\n${result.output}`,
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log('error', `Error stopping app on device: ${errorMessage}`);
return {
content: [
{
type: 'text',
text: `Failed to stop app on device: ${errorMessage}`,
},
],
isError: true,
};
}
}
export default {
name: 'stop_app_device',
description: 'Stops a running app on a connected device.',
schema: getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: stopAppDeviceSchema,
}),
annotations: {
title: 'Stop App Device',
destructiveHint: true,
},
handler: createSessionAwareTool<StopAppDeviceParams>({
internalSchema: stopAppDeviceSchema as unknown as z.ZodType<StopAppDeviceParams>,
logicFunction: stop_app_deviceLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [{ allOf: ['deviceId'], message: 'deviceId is required' }],
}),
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/macos/launch_mac_app.ts:
--------------------------------------------------------------------------------
```typescript
/**
* macOS Workspace Plugin: Launch macOS App
*
* Launches a macOS application using the 'open' command.
* IMPORTANT: You MUST provide the appPath parameter.
*/
import * as z from 'zod';
import { log } from '../../../utils/logging/index.ts';
import { validateFileExists } from '../../../utils/validation/index.ts';
import { ToolResponse } from '../../../types/common.ts';
import type { CommandExecutor, FileSystemExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
// Define schema as ZodObject
const launchMacAppSchema = z.object({
appPath: z
.string()
.describe('Path to the macOS .app bundle to launch (full path to the .app directory)'),
args: z.array(z.string()).optional().describe('Additional arguments to pass to the app'),
});
// Use z.infer for type safety
type LaunchMacAppParams = z.infer<typeof launchMacAppSchema>;
export async function launch_mac_appLogic(
params: LaunchMacAppParams,
executor: CommandExecutor,
fileSystem?: FileSystemExecutor,
): Promise<ToolResponse> {
// Validate that the app file exists
const fileExistsValidation = validateFileExists(params.appPath, fileSystem);
if (!fileExistsValidation.isValid) {
return fileExistsValidation.errorResponse!;
}
log('info', `Starting launch macOS app request for ${params.appPath}`);
try {
// Construct the command as string array for CommandExecutor
const command = ['open', params.appPath];
// Add any additional arguments if provided
if (params.args && Array.isArray(params.args) && params.args.length > 0) {
command.push('--args', ...params.args);
}
// Execute the command using CommandExecutor
await executor(command, 'Launch macOS App');
// Return success response
return {
content: [
{
type: 'text',
text: `✅ macOS app launched successfully: ${params.appPath}`,
},
],
};
} catch (error) {
// Handle errors
const errorMessage = error instanceof Error ? error.message : String(error);
log('error', `Error during launch macOS app operation: ${errorMessage}`);
return {
content: [
{
type: 'text',
text: `❌ Launch macOS app operation failed: ${errorMessage}`,
},
],
isError: true,
};
}
}
export default {
name: 'launch_mac_app',
description:
"Launches a macOS application. IMPORTANT: You MUST provide the appPath parameter. Example: launch_mac_app({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_launch_macos_app.",
schema: launchMacAppSchema.shape, // MCP SDK compatibility
annotations: {
title: 'Launch macOS App',
destructiveHint: true,
},
handler: createTypedTool(launchMacAppSchema, launch_mac_appLogic, getDefaultCommandExecutor),
};
```
--------------------------------------------------------------------------------
/docs/investigations/issue-describe-ui-empty-after-debugger-resume.md:
--------------------------------------------------------------------------------
```markdown
# RCA: describe_ui returns empty tree after debugger resume
## Summary
When the app is stopped under LLDB (breakpoints hit), the `describe_ui` tool frequently returns an empty accessibility tree (0x0 frame, no children). This is not because of a short timing gap after resume. The root cause is that the process is still stopped (or immediately re-stopped) due to active breakpoints, so AX snapshotting cannot retrieve a live hierarchy.
## Impact
- UI automation appears "broken" after resuming from breakpoints.
- Simulator UI may visually update only after detaching or clearing breakpoints because the process is repeatedly stopped.
- `describe_ui` can return misleading empty trees even though the app is running in the simulator.
## Environment
- App: Calculator (example project)
- Simulator: iPhone 16 (2FCB5689-88F1-4CDF-9E7F-8E310CD41D72)
- Debug backend: LLDB CLI
## Repro Steps
1. Attach debugger to the simulator app (`debug_attach_sim`).
2. Set breakpoint at `CalculatorButton.swift:18` and `CalculatorInputHandler.swift:12`.
3. `debug_lldb_command` -> `continue`.
4. Tap a button (e.g., "7") so breakpoints fire.
5. `debug_lldb_command` -> `continue`.
6. Call `describe_ui` immediately after resume.
## Observations
- `debug_stack` immediately after resume shows stop reason `breakpoint 1.2` or `breakpoint 2.1`.
- Multiple `continue` calls quickly re-stop the process due to breakpoints in SwiftUI button handling and input processing.
- While stopped, `describe_ui` often returns:
- Application frame: `{{0,0},{0,0}}`
- `AXLabel` null
- No children
- Waiting does not help. We tested 1s, 2s, 3s, 5s, 8s, and 10s delays; the tree remained empty in a stopped state.
- Once breakpoints are removed and the process is running, `describe_ui` returns the full tree immediately.
- Detaching the debugger also restores `describe_ui` output.
## Root Cause
The process is stopped due to breakpoints, or repeatedly re-stopped after resume. AX snapshots cannot read a paused process, so `describe_ui` returns an empty hierarchy.
## Confirming Evidence
- `debug_stack` after `continue` shows:
- `stop reason = breakpoint 1.2` at `CalculatorButton.swift:18`
- `stop reason = breakpoint 2.1` at `CalculatorInputHandler.swift:12`
- After removing breakpoints and `continue`, `describe_ui` returns a full hierarchy (buttons + display values).
## Current Workarounds
- Clear or remove breakpoints before calling `describe_ui`.
- Detach the debugger to allow the app to run normally.
## Recommendations
- Document that `describe_ui` requires the target process to be running (not stopped under LLDB).
- Provide guidance to:
- Remove or disable breakpoints before UI automation.
- Avoid calling `describe_ui` immediately after breakpoints unless resumed and confirmed running.
- Optional future enhancement: add a tool-level warning when the debugger session is stopped, or add a helper command that validates "running" state before UI inspection.
```
--------------------------------------------------------------------------------
/src/mcp/tools/device/install_app_device.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Device Workspace Plugin: Install App Device
*
* Installs an app on a physical Apple device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro).
* Requires deviceId and appPath.
*/
import * as z from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
// Define schema as ZodObject
const installAppDeviceSchema = z.object({
deviceId: z
.string()
.min(1, { message: 'Device ID cannot be empty' })
.describe('UDID of the device (obtained from list_devices)'),
appPath: z
.string()
.describe('Path to the .app bundle to install (full path to the .app directory)'),
});
const publicSchemaObject = installAppDeviceSchema.omit({ deviceId: true } as const);
// Use z.infer for type safety
type InstallAppDeviceParams = z.infer<typeof installAppDeviceSchema>;
/**
* Business logic for installing an app on a physical Apple device
*/
export async function install_app_deviceLogic(
params: InstallAppDeviceParams,
executor: CommandExecutor,
): Promise<ToolResponse> {
const { deviceId, appPath } = params;
log('info', `Installing app on device ${deviceId}`);
try {
const result = await executor(
['xcrun', 'devicectl', 'device', 'install', 'app', '--device', deviceId, appPath],
'Install app on device',
true, // useShell
undefined, // env
);
if (!result.success) {
return {
content: [
{
type: 'text',
text: `Failed to install app: ${result.error}`,
},
],
isError: true,
};
}
return {
content: [
{
type: 'text',
text: `✅ App installed successfully on device ${deviceId}\n\n${result.output}`,
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log('error', `Error installing app on device: ${errorMessage}`);
return {
content: [
{
type: 'text',
text: `Failed to install app on device: ${errorMessage}`,
},
],
isError: true,
};
}
}
export default {
name: 'install_app_device',
description: 'Installs an app on a connected device.',
schema: getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: installAppDeviceSchema,
}),
annotations: {
title: 'Install App Device',
destructiveHint: true,
},
handler: createSessionAwareTool<InstallAppDeviceParams>({
internalSchema: installAppDeviceSchema as unknown as z.ZodType<InstallAppDeviceParams, unknown>,
logicFunction: install_app_deviceLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [{ allOf: ['deviceId'], message: 'deviceId is required' }],
}),
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/swift-package/swift_package_build.ts:
--------------------------------------------------------------------------------
```typescript
import * as z from 'zod';
import path from 'node:path';
import { createErrorResponse } from '../../../utils/responses/index.ts';
import { log } from '../../../utils/logging/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { ToolResponse } from '../../../types/common.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
// Define schema as ZodObject
const swiftPackageBuildSchema = z.object({
packagePath: z.string().describe('Path to the Swift package root (Required)'),
targetName: z.string().optional().describe('Optional target to build'),
configuration: z
.enum(['debug', 'release'])
.optional()
.describe('Swift package configuration (debug, release)'),
architectures: z.array(z.string()).optional().describe('Target architectures to build for'),
parseAsLibrary: z.boolean().optional().describe('Build as library instead of executable'),
});
// Use z.infer for type safety
type SwiftPackageBuildParams = z.infer<typeof swiftPackageBuildSchema>;
export async function swift_package_buildLogic(
params: SwiftPackageBuildParams,
executor: CommandExecutor,
): Promise<ToolResponse> {
const resolvedPath = path.resolve(params.packagePath);
const swiftArgs = ['build', '--package-path', resolvedPath];
if (params.configuration && params.configuration.toLowerCase() === 'release') {
swiftArgs.push('-c', 'release');
}
if (params.targetName) {
swiftArgs.push('--target', params.targetName);
}
if (params.architectures) {
for (const arch of params.architectures) {
swiftArgs.push('--arch', arch);
}
}
if (params.parseAsLibrary) {
swiftArgs.push('-Xswiftc', '-parse-as-library');
}
log('info', `Running swift ${swiftArgs.join(' ')}`);
try {
const result = await executor(['swift', ...swiftArgs], 'Swift Package Build', true, undefined);
if (!result.success) {
const errorMessage = result.error ?? result.output ?? 'Unknown error';
return createErrorResponse('Swift package build failed', errorMessage);
}
return {
content: [
{ type: 'text', text: '✅ Swift package build succeeded.' },
{
type: 'text',
text: '💡 Next: Run tests with swift_package_test or execute with swift_package_run',
},
{ type: 'text', text: result.output },
],
isError: false,
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log('error', `Swift package build failed: ${message}`);
return createErrorResponse('Failed to execute swift build', message);
}
}
export default {
name: 'swift_package_build',
description: 'Builds a Swift Package with swift build',
schema: swiftPackageBuildSchema.shape, // MCP SDK compatibility
annotations: {
title: 'Swift Package Build',
destructiveHint: true,
},
handler: createTypedTool(
swiftPackageBuildSchema,
swift_package_buildLogic,
getDefaultCommandExecutor,
),
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/macos/__tests__/re-exports.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for macos-project re-export files
* These files re-export tools from macos-workspace to avoid duplication
*/
import { describe, it, expect } from 'vitest';
// Import all re-export tools
import testMacos from '../test_macos.ts';
import buildMacos from '../build_macos.ts';
import buildRunMacos from '../build_run_macos.ts';
import getMacAppPath from '../get_mac_app_path.ts';
describe('macos-project re-exports', () => {
describe('test_macos re-export', () => {
it('should re-export test_macos tool correctly', () => {
expect(testMacos.name).toBe('test_macos');
expect(typeof testMacos.handler).toBe('function');
expect(testMacos.schema).toBeDefined();
expect(typeof testMacos.description).toBe('string');
});
});
describe('build_macos re-export', () => {
it('should re-export build_macos tool correctly', () => {
expect(buildMacos.name).toBe('build_macos');
expect(typeof buildMacos.handler).toBe('function');
expect(buildMacos.schema).toBeDefined();
expect(typeof buildMacos.description).toBe('string');
});
});
describe('build_run_macos re-export', () => {
it('should re-export build_run_macos tool correctly', () => {
expect(buildRunMacos.name).toBe('build_run_macos');
expect(typeof buildRunMacos.handler).toBe('function');
expect(buildRunMacos.schema).toBeDefined();
expect(typeof buildRunMacos.description).toBe('string');
});
});
describe('get_mac_app_path re-export', () => {
it('should re-export get_mac_app_path tool correctly', () => {
expect(getMacAppPath.name).toBe('get_mac_app_path');
expect(typeof getMacAppPath.handler).toBe('function');
expect(getMacAppPath.schema).toBeDefined();
expect(typeof getMacAppPath.description).toBe('string');
});
});
describe('All re-exports validation', () => {
const reExports = [
{ tool: testMacos, name: 'test_macos' },
{ tool: buildMacos, name: 'build_macos' },
{ tool: buildRunMacos, name: 'build_run_macos' },
{ tool: getMacAppPath, name: 'get_mac_app_path' },
];
it('should have all required tool properties', () => {
reExports.forEach(({ tool, name }) => {
expect(tool).toHaveProperty('name');
expect(tool).toHaveProperty('description');
expect(tool).toHaveProperty('schema');
expect(tool).toHaveProperty('handler');
expect(tool.name).toBe(name);
});
});
it('should have callable handlers', () => {
reExports.forEach(({ tool, name }) => {
expect(typeof tool.handler).toBe('function');
expect(tool.handler.length).toBeGreaterThanOrEqual(0);
});
});
it('should have valid schemas', () => {
reExports.forEach(({ tool, name }) => {
expect(tool.schema).toBeDefined();
expect(typeof tool.schema).toBe('object');
});
});
it('should have non-empty descriptions', () => {
reExports.forEach(({ tool, name }) => {
expect(typeof tool.description).toBe('string');
expect(tool.description.length).toBeGreaterThan(0);
});
});
});
});
```
--------------------------------------------------------------------------------
/src/mcp/tools/swift-package/swift_package_list.ts:
--------------------------------------------------------------------------------
```typescript
// Note: This tool shares the activeProcesses map with swift_package_run
// Since both are in the same workflow directory, they can share state
// Import the shared activeProcesses map from swift_package_run
// This maintains the same behavior as the original implementation
import * as z from 'zod';
import { ToolResponse, createTextContent } from '../../../types/common.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
import { getDefaultCommandExecutor } from '../../../utils/command.ts';
interface ProcessInfo {
executableName?: string;
startedAt: Date;
packagePath: string;
}
const activeProcesses = new Map<number, ProcessInfo>();
/**
* Process list dependencies for dependency injection
*/
export interface ProcessListDependencies {
processMap?: Map<number, ProcessInfo>;
arrayFrom?: typeof Array.from;
dateNow?: typeof Date.now;
}
/**
* Swift package list business logic - extracted for testability and separation of concerns
* @param params - Parameters (unused, but maintained for consistency)
* @param dependencies - Injectable dependencies for testing
* @returns ToolResponse with process list information
*/
export async function swift_package_listLogic(
params?: unknown,
dependencies?: ProcessListDependencies,
): Promise<ToolResponse> {
const processMap = dependencies?.processMap ?? activeProcesses;
const arrayFrom = dependencies?.arrayFrom ?? Array.from;
const dateNow = dependencies?.dateNow ?? Date.now;
const processes = arrayFrom(processMap.entries());
if (processes.length === 0) {
return {
content: [
createTextContent('ℹ️ No Swift Package processes currently running.'),
createTextContent('💡 Use swift_package_run to start an executable.'),
],
};
}
const content = [createTextContent(`📋 Active Swift Package processes (${processes.length}):`)];
for (const [pid, info] of processes) {
// Use logical OR instead of nullish coalescing to treat empty strings as falsy
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const executableName = info.executableName || 'default';
const runtime = Math.max(1, Math.round((dateNow() - info.startedAt.getTime()) / 1000));
content.push(
createTextContent(
` • PID ${pid}: ${executableName} (${info.packagePath}) - running ${runtime}s`,
),
);
}
content.push(createTextContent('💡 Use swift_package_stop with a PID to terminate a process.'));
return { content };
}
// Define schema as ZodObject (empty for this tool)
const swiftPackageListSchema = z.object({});
// Use z.infer for type safety
type SwiftPackageListParams = z.infer<typeof swiftPackageListSchema>;
export default {
name: 'swift_package_list',
description: 'Lists currently running Swift Package processes',
schema: swiftPackageListSchema.shape, // MCP SDK compatibility
annotations: {
title: 'Swift Package List',
readOnlyHint: true,
},
handler: createTypedTool(
swiftPackageListSchema,
(params: SwiftPackageListParams) => {
return swift_package_listLogic(params);
},
getDefaultCommandExecutor,
),
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator/launch_app_logs_sim.ts:
--------------------------------------------------------------------------------
```typescript
import * as z from 'zod';
import { ToolResponse, createTextContent } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { startLogCapture } from '../../../utils/log-capture/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
export type LogCaptureFunction = (
params: {
simulatorUuid: string;
bundleId: string;
captureConsole?: boolean;
args?: string[];
},
executor: CommandExecutor,
) => Promise<{ sessionId: string; logFilePath: string; processes: unknown[]; error?: string }>;
const launchAppLogsSimSchemaObject = z.object({
simulatorId: z.string().describe('UUID of the simulator to use (obtained from list_sims)'),
bundleId: z
.string()
.describe("Bundle identifier of the app to launch (e.g., 'com.example.MyApp')"),
args: z.array(z.string()).optional().describe('Additional arguments to pass to the app'),
});
type LaunchAppLogsSimParams = z.infer<typeof launchAppLogsSimSchemaObject>;
const publicSchemaObject = z.strictObject(
launchAppLogsSimSchemaObject.omit({
simulatorId: true,
} as const).shape,
);
export async function launch_app_logs_simLogic(
params: LaunchAppLogsSimParams,
executor: CommandExecutor = getDefaultCommandExecutor(),
logCaptureFunction: LogCaptureFunction = startLogCapture,
): Promise<ToolResponse> {
log('info', `Starting app launch with logs for simulator ${params.simulatorId}`);
const captureParams = {
simulatorUuid: params.simulatorId,
bundleId: params.bundleId,
captureConsole: true,
...(params.args && params.args.length > 0 ? { args: params.args } : {}),
} as const;
const { sessionId, error } = await logCaptureFunction(captureParams, executor);
if (error) {
return {
content: [createTextContent(`App was launched but log capture failed: ${error}`)],
isError: true,
};
}
return {
content: [
createTextContent(
`App launched successfully in simulator ${params.simulatorId} with log capture enabled.\n\nLog capture session ID: ${sessionId}\n\nNext Steps:\n1. Interact with your app in the simulator.\n2. Use 'stop_and_get_simulator_log({ logSessionId: "${sessionId}" })' to stop capture and retrieve logs.`,
),
],
isError: false,
};
}
export default {
name: 'launch_app_logs_sim',
description: 'Launches an app in an iOS simulator and captures its logs.',
schema: getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: launchAppLogsSimSchemaObject,
}),
annotations: {
title: 'Launch App Logs Simulator',
destructiveHint: true,
},
handler: createSessionAwareTool<LaunchAppLogsSimParams>({
internalSchema: launchAppLogsSimSchemaObject as unknown as z.ZodType<
LaunchAppLogsSimParams,
unknown
>,
logicFunction: launch_app_logs_simLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
}),
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator-management/erase_sims.ts:
--------------------------------------------------------------------------------
```typescript
import * as z from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
const eraseSimsBaseSchema = z
.object({
simulatorId: z.uuid().describe('UDID of the simulator to erase.'),
shutdownFirst: z
.boolean()
.optional()
.describe('If true, shuts down the simulator before erasing.'),
})
.passthrough();
const eraseSimsSchema = eraseSimsBaseSchema;
type EraseSimsParams = z.infer<typeof eraseSimsSchema>;
export async function erase_simsLogic(
params: EraseSimsParams,
executor: CommandExecutor,
): Promise<ToolResponse> {
try {
const simulatorId = params.simulatorId;
log(
'info',
`Erasing simulator ${simulatorId}${params.shutdownFirst ? ' (shutdownFirst=true)' : ''}`,
);
if (params.shutdownFirst) {
try {
await executor(
['xcrun', 'simctl', 'shutdown', simulatorId],
'Shutdown Simulator',
true,
undefined,
);
} catch {
// ignore shutdown errors; proceed to erase attempt
}
}
const result = await executor(
['xcrun', 'simctl', 'erase', simulatorId],
'Erase Simulator',
true,
undefined,
);
if (result.success) {
return {
content: [{ type: 'text', text: `Successfully erased simulator ${simulatorId}` }],
};
}
// Add tool hint if simulator is booted and shutdownFirst was not requested
const errText = result.error ?? 'Unknown error';
if (/Unable to erase contents and settings.*Booted/i.test(errText) && !params.shutdownFirst) {
return {
content: [
{ type: 'text', text: `Failed to erase simulator: ${errText}` },
{
type: 'text',
text: `Tool hint: The simulator appears to be Booted. Re-run erase_sims with { simulatorId: '${simulatorId}', shutdownFirst: true } to shut it down before erasing.`,
},
],
};
}
return {
content: [{ type: 'text', text: `Failed to erase simulator: ${errText}` }],
};
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
log('error', `Error erasing simulators: ${message}`);
return { content: [{ type: 'text', text: `Failed to erase simulators: ${message}` }] };
}
}
const publicSchemaObject = eraseSimsSchema.omit({ simulatorId: true } as const).passthrough();
export default {
name: 'erase_sims',
description: 'Erases a simulator by UDID.',
schema: getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: eraseSimsSchema,
}),
annotations: {
title: 'Erase Simulators',
destructiveHint: true,
},
handler: createSessionAwareTool<EraseSimsParams>({
internalSchema: eraseSimsSchema as unknown as z.ZodType<EraseSimsParams>,
logicFunction: erase_simsLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
}),
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/device/__tests__/re-exports.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for device-project re-export files
* These files re-export tools from device-workspace to avoid duplication
*/
import { describe, it, expect } from 'vitest';
// Import all re-export tools
import launchAppDevice from '../launch_app_device.ts';
import stopAppDevice from '../stop_app_device.ts';
import listDevices from '../list_devices.ts';
import installAppDevice from '../install_app_device.ts';
describe('device-project re-exports', () => {
describe('launch_app_device re-export', () => {
it('should re-export launch_app_device tool correctly', () => {
expect(launchAppDevice.name).toBe('launch_app_device');
expect(typeof launchAppDevice.handler).toBe('function');
expect(launchAppDevice.schema).toBeDefined();
expect(typeof launchAppDevice.description).toBe('string');
});
});
describe('stop_app_device re-export', () => {
it('should re-export stop_app_device tool correctly', () => {
expect(stopAppDevice.name).toBe('stop_app_device');
expect(typeof stopAppDevice.handler).toBe('function');
expect(stopAppDevice.schema).toBeDefined();
expect(typeof stopAppDevice.description).toBe('string');
});
});
describe('list_devices re-export', () => {
it('should re-export list_devices tool correctly', () => {
expect(listDevices.name).toBe('list_devices');
expect(typeof listDevices.handler).toBe('function');
expect(listDevices.schema).toBeDefined();
expect(typeof listDevices.description).toBe('string');
});
});
describe('install_app_device re-export', () => {
it('should re-export install_app_device tool correctly', () => {
expect(installAppDevice.name).toBe('install_app_device');
expect(typeof installAppDevice.handler).toBe('function');
expect(installAppDevice.schema).toBeDefined();
expect(typeof installAppDevice.description).toBe('string');
});
});
describe('All re-exports validation', () => {
const reExports = [
{ tool: launchAppDevice, name: 'launch_app_device' },
{ tool: stopAppDevice, name: 'stop_app_device' },
{ tool: listDevices, name: 'list_devices' },
{ tool: installAppDevice, name: 'install_app_device' },
];
it('should have all required tool properties', () => {
reExports.forEach(({ tool, name }) => {
expect(tool).toHaveProperty('name');
expect(tool).toHaveProperty('description');
expect(tool).toHaveProperty('schema');
expect(tool).toHaveProperty('handler');
expect(tool.name).toBe(name);
});
});
it('should have callable handlers', () => {
reExports.forEach(({ tool, name }) => {
expect(typeof tool.handler).toBe('function');
expect(tool.handler.length).toBeGreaterThanOrEqual(0);
});
});
it('should have valid schemas', () => {
reExports.forEach(({ tool, name }) => {
expect(tool.schema).toBeDefined();
expect(typeof tool.schema).toBe('object');
});
});
it('should have non-empty descriptions', () => {
reExports.forEach(({ tool, name }) => {
expect(typeof tool.description).toBe('string');
expect(tool.description.length).toBeGreaterThan(0);
});
});
});
});
```
--------------------------------------------------------------------------------
/docs/CONFIGURATION.md:
--------------------------------------------------------------------------------
```markdown
# Configuration
XcodeBuildMCP is configured through environment variables provided by your MCP client. Here is a single example showing how to add options to a typical MCP config:
```json
"XcodeBuildMCP": {
"command": "npx",
"args": ["-y", "xcodebuildmcp@latest"],
"env": {
"XCODEBUILDMCP_ENABLED_WORKFLOWS": "simulator,device,project-discovery",
"INCREMENTAL_BUILDS_ENABLED": "false",
"XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS": "false",
"XCODEBUILDMCP_SENTRY_DISABLED": "false"
}
}
```
## Workflow selection
By default, XcodeBuildMCP loads all tools at startup. If you want a smaller tool surface for a specific workflow, set `XCODEBUILDMCP_ENABLED_WORKFLOWS` to a comma-separated list of workflow directory names. The `session-management` workflow is always auto-included since other tools depend on it.
**Available workflows:**
- `device` (14 tools) - iOS Device Development
- `simulator` (19 tools) - iOS Simulator Development
- `logging` (4 tools) - Log Capture & Management
- `macos` (11 tools) - macOS Development
- `project-discovery` (5 tools) - Project Discovery
- `project-scaffolding` (2 tools) - Project Scaffolding
- `utilities` (1 tool) - Project Utilities
- `session-management` (3 tools) - session-management
- `debugging` (8 tools) - Simulator Debugging
- `simulator-management` (8 tools) - Simulator Management
- `swift-package` (6 tools) - Swift Package Manager
- `doctor` (1 tool) - System Doctor
- `ui-testing` (11 tools) - UI Testing & Automation
## Incremental build support
XcodeBuildMCP includes experimental support for incremental builds. This feature is disabled by default and can be enabled by setting the `INCREMENTAL_BUILDS_ENABLED` environment variable to `true`.
> [!IMPORTANT]
> Incremental builds are highly experimental and your mileage may vary. Please report issues to the [issue tracker](https://github.com/cameroncooke/XcodeBuildMCP/issues).
## Session-aware opt-out
By default, XcodeBuildMCP uses a session-aware mode: the LLM (or client) sets shared defaults once (simulator, device, project/workspace, scheme, etc.), and all tools reuse them. This cuts context bloat not just in each call payload, but also in the tool schemas themselves.
If you prefer the older, explicit style where each tool requires its own parameters, set `XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS=true`. This restores the legacy schemas with per-call parameters while still honoring any session defaults you choose to set.
Leave this unset for the streamlined session-aware experience; enable it to force explicit parameters on each tool call.
## Sentry telemetry opt-out
If you do not wish to send error logs to Sentry, set `XCODEBUILDMCP_SENTRY_DISABLED=true`.
## AXe binary override
UI automation and simulator video capture require the AXe binary. By default, XcodeBuildMCP uses the bundled AXe when available, then falls back to `PATH`. To force a specific binary location, set `XCODEBUILDMCP_AXE_PATH` (preferred). `AXE_PATH` is also recognized for compatibility.
Example:
```
XCODEBUILDMCP_AXE_PATH=/opt/axe/bin/axe
```
## Related docs
- Session defaults: [SESSION_DEFAULTS.md](SESSION_DEFAULTS.md)
- Tools reference: [TOOLS.md](TOOLS.md)
- Privacy and telemetry: [PRIVACY.md](PRIVACY.md)
- Troubleshooting: [TROUBLESHOOTING.md](TROUBLESHOOTING.md)
```
--------------------------------------------------------------------------------
/src/mcp/resources/__tests__/devices.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from 'vitest';
import devicesResource, { devicesResourceLogic } from '../devices.ts';
import { createMockExecutor } from '../../../test-utils/mock-executors.ts';
describe('devices resource', () => {
describe('Export Field Validation', () => {
it('should export correct uri', () => {
expect(devicesResource.uri).toBe('xcodebuildmcp://devices');
});
it('should export correct description', () => {
expect(devicesResource.description).toBe(
'Connected physical Apple devices with their UUIDs, names, and connection status',
);
});
it('should export correct mimeType', () => {
expect(devicesResource.mimeType).toBe('text/plain');
});
it('should export handler function', () => {
expect(typeof devicesResource.handler).toBe('function');
});
});
describe('Handler Functionality', () => {
it('should handle successful device data retrieval with xctrace fallback', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: `iPhone (12345-ABCDE-FGHIJ-67890) (13.0)
iPad (98765-KLMNO-PQRST-43210) (14.0)
My Device (11111-22222-33333-44444) (15.0)`,
});
const result = await devicesResourceLogic(mockExecutor);
expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('Device listing (xctrace output)');
expect(result.contents[0].text).toContain('iPhone');
expect(result.contents[0].text).toContain('iPad');
});
it('should handle command execution failure', async () => {
const mockExecutor = createMockExecutor({
success: false,
output: '',
error: 'Command failed',
});
const result = await devicesResourceLogic(mockExecutor);
expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('Failed to list devices');
expect(result.contents[0].text).toContain('Command failed');
});
it('should handle spawn errors', async () => {
const mockExecutor = createMockExecutor(new Error('spawn xcrun ENOENT'));
const result = await devicesResourceLogic(mockExecutor);
expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('Error retrieving device data');
expect(result.contents[0].text).toContain('spawn xcrun ENOENT');
});
it('should handle empty device data with xctrace fallback', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: '',
});
const result = await devicesResourceLogic(mockExecutor);
expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('Device listing (xctrace output)');
expect(result.contents[0].text).toContain('Xcode 15 or later');
});
it('should handle device data with next steps guidance', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: `iPhone 15 Pro (12345-ABCDE-FGHIJ-67890) (17.0)`,
});
const result = await devicesResourceLogic(mockExecutor);
expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('Device listing (xctrace output)');
expect(result.contents[0].text).toContain('iPhone 15 Pro');
});
});
});
```
--------------------------------------------------------------------------------
/src/core/plugin-registry.ts:
--------------------------------------------------------------------------------
```typescript
import type { PluginMeta, WorkflowGroup, WorkflowMeta } from './plugin-types.ts';
import { WORKFLOW_LOADERS, WorkflowName, WORKFLOW_METADATA } from './generated-plugins.ts';
export async function loadPlugins(): Promise<Map<string, PluginMeta>> {
const plugins = new Map<string, PluginMeta>();
// Load all workflows and collect all their tools
const workflowGroups = await loadWorkflowGroups();
for (const [, workflow] of workflowGroups.entries()) {
for (const tool of workflow.tools) {
if (tool?.name && typeof tool.handler === 'function') {
plugins.set(tool.name, tool);
}
}
}
return plugins;
}
/**
* Load workflow groups with metadata validation using generated loaders
*/
export async function loadWorkflowGroups(): Promise<Map<string, WorkflowGroup>> {
const workflows = new Map<string, WorkflowGroup>();
for (const [workflowName, loader] of Object.entries(WORKFLOW_LOADERS)) {
try {
// Dynamic import with code-splitting
const workflowModule = (await loader()) as {
workflow?: WorkflowMeta;
[key: string]: unknown;
};
if (!workflowModule.workflow) {
throw new Error(`Workflow metadata missing in ${workflowName}/index.js`);
}
// Validate required fields
const workflowMeta = workflowModule.workflow as WorkflowMeta;
if (!workflowMeta.name || typeof workflowMeta.name !== 'string') {
throw new Error(
`Invalid workflow.name in ${workflowName}/index.js: must be a non-empty string`,
);
}
if (!workflowMeta.description || typeof workflowMeta.description !== 'string') {
throw new Error(
`Invalid workflow.description in ${workflowName}/index.js: must be a non-empty string`,
);
}
workflows.set(workflowName, {
workflow: workflowMeta,
tools: await loadWorkflowTools(workflowModule),
directoryName: workflowName,
});
} catch (error) {
throw new Error(
`Failed to load workflow '${workflowName}': ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
}
return workflows;
}
/**
* Load workflow tools from the workflow module
*/
async function loadWorkflowTools(workflowModule: Record<string, unknown>): Promise<PluginMeta[]> {
const tools: PluginMeta[] = [];
// Load individual tool files from the workflow module
for (const [key, value] of Object.entries(workflowModule)) {
if (key !== 'workflow' && value && typeof value === 'object') {
const tool = value as PluginMeta;
if (tool.name && typeof tool.handler === 'function') {
tools.push(tool);
}
}
}
return tools;
}
/**
* Get workflow metadata by directory name using generated loaders
*/
export async function getWorkflowMetadata(directoryName: string): Promise<WorkflowMeta | null> {
try {
// First try to get from generated metadata (fast path)
const metadata = WORKFLOW_METADATA[directoryName as WorkflowName];
if (metadata) {
return metadata;
}
// Fall back to loading the actual module
const loader = WORKFLOW_LOADERS[directoryName as WorkflowName];
if (loader) {
const workflowModule = (await loader()) as { workflow?: WorkflowMeta };
return workflowModule.workflow ?? null;
}
return null;
} catch {
return null;
}
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/device/build_device.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Device Shared Plugin: Build Device (Unified)
*
* Builds an app from a project or workspace for a physical Apple device.
* Accepts mutually exclusive `projectPath` or `workspacePath`.
*/
import * as z from 'zod';
import { ToolResponse, XcodePlatform } from '../../../types/common.ts';
import { executeXcodeBuildCommand } from '../../../utils/build/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
// Unified schema: XOR between projectPath and workspacePath
const baseSchemaObject = z.object({
projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
scheme: z.string().describe('The scheme to build'),
configuration: z.string().optional().describe('Build configuration (Debug, Release)'),
derivedDataPath: z.string().optional().describe('Path to derived data directory'),
extraArgs: z.array(z.string()).optional().describe('Additional arguments to pass to xcodebuild'),
preferXcodebuild: z.boolean().optional().describe('Prefer xcodebuild over faster alternatives'),
});
const buildDeviceSchema = z.preprocess(
nullifyEmptyStrings,
baseSchemaObject
.refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
message: 'Either projectPath or workspacePath is required.',
})
.refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
}),
);
export type BuildDeviceParams = z.infer<typeof buildDeviceSchema>;
const publicSchemaObject = baseSchemaObject.omit({
projectPath: true,
workspacePath: true,
scheme: true,
configuration: true,
} as const);
/**
* Business logic for building device project or workspace.
* Exported for direct testing and reuse.
*/
export async function buildDeviceLogic(
params: BuildDeviceParams,
executor: CommandExecutor,
): Promise<ToolResponse> {
const processedParams = {
...params,
configuration: params.configuration ?? 'Debug', // Default config
};
return executeXcodeBuildCommand(
processedParams,
{
platform: XcodePlatform.iOS,
logPrefix: 'iOS Device Build',
},
params.preferXcodebuild ?? false,
'build',
executor,
);
}
export default {
name: 'build_device',
description: 'Builds an app for a connected device.',
schema: getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: baseSchemaObject,
}),
annotations: {
title: 'Build Device',
destructiveHint: true,
},
handler: createSessionAwareTool<BuildDeviceParams>({
internalSchema: buildDeviceSchema as unknown as z.ZodType<BuildDeviceParams, unknown>,
logicFunction: buildDeviceLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [
{ allOf: ['scheme'], message: 'scheme is required' },
{ oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
],
exclusivePairs: [['projectPath', 'workspacePath']],
}),
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator/__tests__/test_sim.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for test_sim plugin (session-aware version)
* Follows CLAUDE.md guidance: dependency injection, no vi-mocks, literal validation.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import * as z from 'zod';
import { sessionStore } from '../../../../utils/session-store.ts';
import testSim from '../test_sim.ts';
describe('test_sim tool', () => {
beforeEach(() => {
sessionStore.clear();
});
describe('Export Field Validation (Literal)', () => {
it('should have correct name', () => {
expect(testSim.name).toBe('test_sim');
});
it('should have concise description', () => {
expect(testSim.description).toBe('Runs tests on an iOS simulator.');
});
it('should have handler function', () => {
expect(typeof testSim.handler).toBe('function');
});
it('should expose only non-session fields in public schema', () => {
const schema = z.object(testSim.schema);
expect(schema.safeParse({}).success).toBe(true);
expect(
schema.safeParse({
derivedDataPath: '/tmp/derived',
extraArgs: ['--quiet'],
preferXcodebuild: true,
testRunnerEnv: { FOO: 'BAR' },
}).success,
).toBe(true);
expect(schema.safeParse({ derivedDataPath: 123 }).success).toBe(false);
expect(schema.safeParse({ extraArgs: ['--ok', 42] }).success).toBe(false);
expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false);
expect(schema.safeParse({ testRunnerEnv: { FOO: 123 } }).success).toBe(false);
const schemaKeys = Object.keys(testSim.schema).sort();
expect(schemaKeys).toEqual(
['derivedDataPath', 'extraArgs', 'preferXcodebuild', 'testRunnerEnv'].sort(),
);
});
});
describe('Handler Requirements', () => {
it('should require scheme when not provided', async () => {
const result = await testSim.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 testSim.handler({});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Provide a project or workspace');
});
it('should require simulator identifier when scheme and project defaults exist', async () => {
sessionStore.setDefaults({
scheme: 'MyScheme',
projectPath: '/path/to/project.xcodeproj',
});
const result = await testSim.handler({});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Provide simulatorId or simulatorName');
});
it('should error when both simulatorId and simulatorName provided explicitly', async () => {
sessionStore.setDefaults({
scheme: 'MyScheme',
workspacePath: '/path/to/workspace.xcworkspace',
});
const result = await testSim.handler({
simulatorId: 'SIM-UUID',
simulatorName: 'iPhone 16',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
expect(result.content[0].text).toContain('simulatorId');
expect(result.content[0].text).toContain('simulatorName');
});
});
});
```
--------------------------------------------------------------------------------
/src/core/resources.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Resource Management - MCP Resource handlers and URI management
*
* This module manages MCP resources, providing a unified interface for exposing
* data through the Model Context Protocol resource system. Resources allow clients
* to access data via URI references without requiring tool calls.
*
* Responsibilities:
* - Loading resources from the plugin-based resource system
* - Managing resource registration with the MCP server
* - Providing fallback compatibility for clients without resource support
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { ReadResourceResult } from '@modelcontextprotocol/sdk/types.js';
import { log } from '../utils/logging/index.ts';
import type { CommandExecutor } from '../utils/execution/index.ts';
import { RESOURCE_LOADERS } from './generated-resources.ts';
/**
* Resource metadata interface
*/
export interface ResourceMeta {
uri: string;
name: string;
description: string;
mimeType: string;
handler: (
uri: URL,
executor?: CommandExecutor,
) => Promise<{
contents: Array<{ text: string }>;
}>;
}
/**
* Load all resources using generated loaders
* @returns Map of resource URI to resource metadata
*/
export async function loadResources(): Promise<Map<string, ResourceMeta>> {
const resources = new Map<string, ResourceMeta>();
for (const [resourceName, loader] of Object.entries(RESOURCE_LOADERS)) {
try {
const resource = (await loader()) as ResourceMeta;
if (!resource.uri || !resource.handler || typeof resource.handler !== 'function') {
throw new Error(`Invalid resource structure for ${resourceName}`);
}
resources.set(resource.uri, resource);
log('info', `Loaded resource: ${resourceName} (${resource.uri})`);
} catch (error) {
log(
'error',
`Failed to load resource ${resourceName}: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
return resources;
}
/**
* Register all resources with the MCP server if client supports resources
* @param server The MCP server instance
* @returns true if resources were registered, false if skipped due to client limitations
*/
export async function registerResources(server: McpServer): Promise<boolean> {
const resources = await loadResources();
for (const [uri, resource] of Array.from(resources)) {
// Create a handler wrapper that matches ReadResourceCallback signature
const readCallback = async (resourceUri: URL): Promise<ReadResourceResult> => {
const result = await resource.handler(resourceUri);
// Transform the content to match MCP SDK expectations
return {
contents: result.contents.map((content) => ({
uri: resourceUri.toString(),
text: content.text,
mimeType: resource.mimeType,
})),
};
};
server.resource(
resource.name,
uri,
{
mimeType: resource.mimeType,
title: resource.description,
},
readCallback,
);
log('info', `Registered resource: ${resource.name} at ${uri}`);
}
log('info', `Registered ${resources.size} resources`);
return true;
}
/**
* Get all available resource URIs
* @returns Array of resource URI strings
*/
export async function getAvailableResources(): Promise<string[]> {
const resources = await loadResources();
return Array.from(resources.keys());
}
```
--------------------------------------------------------------------------------
/example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/ContentView.swift:
--------------------------------------------------------------------------------
```swift
import SwiftUI
public struct ContentView: View {
@State private var calculatorService = CalculatorService()
@State private var backgroundGradient = BackgroundState.normal
private var inputHandler: CalculatorInputHandler {
CalculatorInputHandler(service: calculatorService)
}
public var body: some View {
GeometryReader { geometry in
ZStack {
// Dynamic gradient background
AnimatedBackground(backgroundGradient: backgroundGradient)
VStack(spacing: 0) {
Spacer()
// Display Section
CalculatorDisplay(
expressionDisplay: calculatorService.expressionDisplay,
display: calculatorService.display,
onDeleteLastDigit: {
inputHandler.deleteLastDigit()
}
)
// Button Grid
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: 4), spacing: 12) {
ForEach(calculatorButtons, id: \.self) { button in
CalculatorButton(
title: button,
buttonType: buttonType(for: button),
isWideButton: button == "0"
) {
handleButtonPress(button)
}
}
}
.padding(.horizontal, 20)
.padding(.bottom, max(geometry.safeAreaInsets.bottom, 20))
}
}
}
}
// Calculator button layout (proper grid with = button in correct position)
private var calculatorButtons: [String] {
[
"C", "±", "%", "÷",
"7", "8", "9", "×",
"4", "5", "6", "-",
"1", "2", "3", "+",
"", "0", ".", "="
]
}
private func buttonType(for button: String) -> CalculatorButtonType {
switch button {
case "C", "±", "%":
return .function
case "÷", "×", "-", "+", "=":
return .operation
case "":
return .hidden
default:
return .number
}
}
private func handleButtonPress(_ button: String) {
// Process input through the input handler
inputHandler.handleInput(button)
// Handle background state changes with modern animation
withAnimation(.easeInOut(duration: 0.3)) {
if button == "=" {
backgroundGradient = calculatorService.hasError ? .error : .calculated
// Reset to normal after a delay using structured concurrency
Task {
try await Task.sleep(for: .seconds(1.5))
await MainActor.run {
withAnimation(.easeInOut(duration: 0.5)) {
backgroundGradient = .normal
}
}
}
} else if button == "C" {
backgroundGradient = .normal
}
}
}
public init() {}
}
#Preview {
ContentView()
}
```
--------------------------------------------------------------------------------
/src/utils/environment.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Environment Detection Utilities
*
* Provides abstraction for environment detection to enable testability
* while maintaining production functionality.
*/
import { execSync } from 'child_process';
import { log } from './logger.ts';
/**
* Interface for environment detection abstraction
*/
export interface EnvironmentDetector {
/**
* Detects if the MCP server is running under Claude Code
* @returns true if Claude Code is detected, false otherwise
*/
isRunningUnderClaudeCode(): boolean;
}
/**
* Production implementation of environment detection
*/
export class ProductionEnvironmentDetector implements EnvironmentDetector {
isRunningUnderClaudeCode(): boolean {
// Disable Claude Code detection during tests for environment-agnostic testing
if (process.env.NODE_ENV === 'test' || process.env.VITEST === 'true') {
return false;
}
// Method 1: Check for Claude Code environment variables
if (process.env.CLAUDECODE === '1' || process.env.CLAUDE_CODE_ENTRYPOINT === 'cli') {
return true;
}
// Method 2: Check parent process name
try {
const parentPid = process.ppid;
if (parentPid) {
const parentCommand = execSync(`ps -o command= -p ${parentPid}`, {
encoding: 'utf8',
timeout: 1000,
}).trim();
if (parentCommand.includes('claude')) {
return true;
}
}
} catch (error) {
// If process detection fails, fall back to environment variables only
log('debug', `Failed to detect parent process: ${error}`);
}
return false;
}
}
/**
* Default environment detector instance for production use
*/
export const defaultEnvironmentDetector = new ProductionEnvironmentDetector();
/**
* Gets the default environment detector for production use
*/
export function getDefaultEnvironmentDetector(): EnvironmentDetector {
return defaultEnvironmentDetector;
}
/**
* Global opt-out for session defaults in MCP tool schemas.
* When enabled, tools re-expose all parameters instead of hiding session-managed fields.
*/
export function isSessionDefaultsSchemaOptOutEnabled(): boolean {
const raw = process.env.XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS;
if (!raw) return false;
const normalized = raw.trim().toLowerCase();
return ['1', 'true', 'yes', 'on'].includes(normalized);
}
export type UiDebuggerGuardMode = 'error' | 'warn' | 'off';
export function getUiDebuggerGuardMode(): UiDebuggerGuardMode {
const raw = process.env.XCODEBUILDMCP_UI_DEBUGGER_GUARD_MODE;
if (!raw) return 'error';
const normalized = raw.trim().toLowerCase();
if (['off', '0', 'false', 'no'].includes(normalized)) return 'off';
if (['warn', 'warning'].includes(normalized)) return 'warn';
return 'error';
}
/**
* Normalizes a set of user-provided environment variables by ensuring they are
* prefixed with TEST_RUNNER_. Variables already prefixed are preserved.
*
* Example:
* normalizeTestRunnerEnv({ FOO: '1', TEST_RUNNER_BAR: '2' })
* => { TEST_RUNNER_FOO: '1', TEST_RUNNER_BAR: '2' }
*/
export function normalizeTestRunnerEnv(vars: Record<string, string>): Record<string, string> {
const normalized: Record<string, string> = {};
for (const [key, value] of Object.entries(vars ?? {})) {
if (value == null) continue;
const prefixedKey = key.startsWith('TEST_RUNNER_') ? key : `TEST_RUNNER_${key}`;
normalized[prefixedKey] = value;
}
return normalized;
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/swift-package/swift_package_stop.ts:
--------------------------------------------------------------------------------
```typescript
import * as z from 'zod';
import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.ts';
import { getProcess, removeProcess, type ProcessInfo } from './active-processes.ts';
import { ToolResponse } from '../../../types/common.ts';
// Define schema as ZodObject
const swiftPackageStopSchema = z.object({
pid: z.number().describe('Process ID (PID) of the running executable'),
});
// Use z.infer for type safety
type SwiftPackageStopParams = z.infer<typeof swiftPackageStopSchema>;
/**
* Process manager interface for dependency injection
*/
export interface ProcessManager {
getProcess: (pid: number) => ProcessInfo | undefined;
removeProcess: (pid: number) => boolean;
}
/**
* Default process manager implementation
*/
const defaultProcessManager: ProcessManager = {
getProcess,
removeProcess,
};
/**
* Get the default process manager instance
*/
export function getDefaultProcessManager(): ProcessManager {
return defaultProcessManager;
}
/**
* Create a mock process manager for testing
*/
export function createMockProcessManager(overrides?: Partial<ProcessManager>): ProcessManager {
return {
getProcess: () => undefined,
removeProcess: () => true,
...overrides,
};
}
/**
* Business logic for stopping a Swift Package executable
*/
export async function swift_package_stopLogic(
params: SwiftPackageStopParams,
processManager: ProcessManager = getDefaultProcessManager(),
timeout: number = 5000,
): Promise<ToolResponse> {
const processInfo = processManager.getProcess(params.pid);
if (!processInfo) {
return createTextResponse(
`⚠️ No running process found with PID ${params.pid}. Use swift_package_run to check active processes.`,
true,
);
}
try {
processInfo.process.kill('SIGTERM');
// Give it time to terminate gracefully (configurable for testing)
await new Promise((resolve) => {
let terminated = false;
processInfo.process.on('exit', () => {
terminated = true;
resolve(true);
});
setTimeout(() => {
if (!terminated) {
processInfo.process.kill('SIGKILL');
}
resolve(true);
}, timeout);
});
processManager.removeProcess(params.pid);
return {
content: [
{
type: 'text',
text: `✅ Stopped executable (was running since ${processInfo.startedAt.toISOString()})`,
},
{
type: 'text',
text: `💡 Process terminated. You can now run swift_package_run again if needed.`,
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return createErrorResponse('Failed to stop process', message);
}
}
export default {
name: 'swift_package_stop',
description: 'Stops a running Swift Package executable started with swift_package_run',
schema: swiftPackageStopSchema.shape, // MCP SDK compatibility
annotations: {
title: 'Swift Package Stop',
destructiveHint: true,
},
async handler(args: Record<string, unknown>): Promise<ToolResponse> {
// Validate parameters using Zod
const parseResult = swiftPackageStopSchema.safeParse(args);
if (!parseResult.success) {
return createErrorResponse(
'Parameter validation failed',
parseResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
);
}
return swift_package_stopLogic(parseResult.data);
},
};
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "xcodebuildmcp",
"version": "1.15.1",
"mcpName": "com.xcodebuildmcp/XcodeBuildMCP",
"iOSTemplateVersion": "v1.0.8",
"macOSTemplateVersion": "v1.0.5",
"type": "module",
"module": "src/smithery.ts",
"exports": {
".": "./build/index.js",
"./package.json": "./package.json"
},
"bin": {
"xcodebuildmcp": "build/index.js",
"xcodebuildmcp-doctor": "build/doctor-cli.js"
},
"scripts": {
"build": "npm run build:tsup && npx smithery build",
"dev": "npm run generate:version && npm run generate:loaders && npx smithery dev",
"build:tsup": "npm run generate:version && npm run generate:loaders && tsup",
"dev:tsup": "npm run build:tsup && tsup --watch",
"generate:version": "npx tsx scripts/generate-version.ts",
"generate:loaders": "npx tsx scripts/generate-loaders.ts",
"bundle:axe": "scripts/bundle-axe.sh",
"lint": "eslint 'src/**/*.{js,ts}'",
"lint:fix": "eslint 'src/**/*.{js,ts}' --fix",
"format": "prettier --write 'src/**/*.{js,ts}'",
"format:check": "prettier --check 'src/**/*.{js,ts}'",
"typecheck": "npx tsc --noEmit && npx tsc -p tsconfig.test.json",
"typecheck:tests": "npx tsc -p tsconfig.test.json",
"verify:smithery-bundle": "bash scripts/verify-smithery-bundle.sh",
"inspect": "npx @modelcontextprotocol/inspector node build/index.js",
"doctor": "node build/doctor-cli.js",
"tools": "npx tsx scripts/tools-cli.ts",
"tools:list": "npx tsx scripts/tools-cli.ts list",
"tools:static": "npx tsx scripts/tools-cli.ts static",
"tools:count": "npx tsx scripts/tools-cli.ts count --static",
"tools:analysis": "npx tsx scripts/analysis/tools-analysis.ts",
"docs:update": "npx tsx scripts/update-tools-docs.ts",
"docs:update:dry-run": "npx tsx scripts/update-tools-docs.ts --dry-run --verbose",
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
},
"files": [
"build",
"bundled",
"plugins"
],
"keywords": [
"xcodebuild",
"mcp",
"modelcontextprotocol",
"xcode",
"ios",
"macos",
"simulator"
],
"author": "Cameron Cooke",
"license": "MIT",
"description": "XcodeBuildMCP is a ModelContextProtocol server that provides tools for Xcode project management, simulator management, and app utilities.",
"repository": {
"type": "git",
"url": "git+https://github.com/cameroncooke/XcodeBuildMCP.git"
},
"homepage": "https://www.async-let.com/blog/xcodebuild-mcp/",
"bugs": {
"url": "https://github.com/cameroncooke/XcodeBuildMCP/issues"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.1",
"@sentry/cli": "^2.43.1",
"@sentry/node": "^10.5.0",
"uuid": "^11.1.0",
"zod": "^4.0.0"
},
"devDependencies": {
"@bacons/xcode": "^1.0.0-alpha.24",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.23.0",
"@smithery/cli": "^1.4.6",
"@types/node": "^22.13.6",
"@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"eslint": "^9.23.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-prettier": "^5.2.5",
"playwright": "^1.53.0",
"prettier": "3.6.2",
"ts-node": "^10.9.2",
"tsup": "^8.5.0",
"tsx": "^4.20.4",
"typescript": "^5.8.2",
"typescript-eslint": "^8.28.0",
"vitest": "^3.2.4",
"xcode": "^3.0.1"
}
}
```
--------------------------------------------------------------------------------
/src/utils/xcode.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Xcode Utilities - Core infrastructure for interacting with Xcode tools
*
* This utility module provides the foundation for all Xcode interactions across the codebase.
* It offers platform-specific utilities, and common functionality that can be used by any module
* requiring Xcode tool integration.
*
* Responsibilities:
* - Constructing platform-specific destination strings (constructDestinationString)
*
* This file serves as the foundation layer for more specialized utilities like build-utils.ts,
* which build upon these core functions to provide higher-level abstractions.
*/
import { log } from './logger.ts';
import { XcodePlatform } from '../types/common.ts';
// Re-export XcodePlatform for use in other modules
export { XcodePlatform };
/**
* Constructs a destination string for xcodebuild from platform and simulator parameters
* @param platform The target platform
* @param simulatorName Optional simulator name
* @param simulatorId Optional simulator UUID
* @param useLatest Whether to use the latest simulator version (primarily for named simulators)
* @param arch Optional architecture for macOS builds (arm64 or x86_64)
* @returns Properly formatted destination string for xcodebuild
*/
export function constructDestinationString(
platform: XcodePlatform,
simulatorName?: string,
simulatorId?: string,
useLatest: boolean = true,
arch?: string,
): string {
const isSimulatorPlatform = [
XcodePlatform.iOSSimulator,
XcodePlatform.watchOSSimulator,
XcodePlatform.tvOSSimulator,
XcodePlatform.visionOSSimulator,
].includes(platform);
// If ID is provided for a simulator, it takes precedence and uniquely identifies it.
if (isSimulatorPlatform && simulatorId) {
return `platform=${platform},id=${simulatorId}`;
}
// If name is provided for a simulator
if (isSimulatorPlatform && simulatorName) {
return `platform=${platform},name=${simulatorName}${useLatest ? ',OS=latest' : ''}`;
}
// If it's a simulator platform but neither ID nor name is provided (should be prevented by callers now)
if (isSimulatorPlatform && !simulatorId && !simulatorName) {
// Throw error as specific simulator is needed unless it's a generic build action
// Allow fallback for generic simulator builds if needed, but generally require specifics for build/run
log(
'warning',
`Constructing generic destination for ${platform} without name or ID. This might not be specific enough.`,
);
// Example: return `platform=${platform},name=Any ${platform} Device`; // Or similar generic target
throw new Error(`Simulator name or ID is required for specific ${platform} operations`);
}
// Handle non-simulator platforms
switch (platform) {
case XcodePlatform.macOS:
return arch ? `platform=macOS,arch=${arch}` : 'platform=macOS';
case XcodePlatform.iOS:
return 'generic/platform=iOS';
case XcodePlatform.watchOS:
return 'generic/platform=watchOS';
case XcodePlatform.tvOS:
return 'generic/platform=tvOS';
case XcodePlatform.visionOS:
return 'generic/platform=visionOS';
// No default needed as enum covers all cases unless extended
// default:
// throw new Error(`Unsupported platform for destination string: ${platform}`);
}
// Fallback just in case (shouldn't be reached with enum)
log('error', `Reached unexpected point in constructDestinationString for platform: ${platform}`);
return `platform=${platform}`;
}
```
--------------------------------------------------------------------------------
/src/mcp/resources/__tests__/doctor.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from 'vitest';
import doctorResource, { doctorResourceLogic } from '../doctor.ts';
import { createMockExecutor } from '../../../test-utils/mock-executors.ts';
describe('doctor resource', () => {
describe('Export Field Validation', () => {
it('should export correct uri', () => {
expect(doctorResource.uri).toBe('xcodebuildmcp://doctor');
});
it('should export correct description', () => {
expect(doctorResource.description).toBe(
'Comprehensive development environment diagnostic information and configuration status',
);
});
it('should export correct mimeType', () => {
expect(doctorResource.mimeType).toBe('text/plain');
});
it('should export handler function', () => {
expect(typeof doctorResource.handler).toBe('function');
});
});
describe('Handler Functionality', () => {
it('should handle successful environment data retrieval', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'Mock command output',
});
const result = await doctorResourceLogic(mockExecutor);
expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('XcodeBuildMCP Doctor');
expect(result.contents[0].text).toContain('## System Information');
expect(result.contents[0].text).toContain('## Node.js Information');
expect(result.contents[0].text).toContain('## Dependencies');
expect(result.contents[0].text).toContain('## Environment Variables');
expect(result.contents[0].text).toContain('## Feature Status');
});
it('should handle spawn errors by showing doctor info', async () => {
const mockExecutor = createMockExecutor(new Error('spawn xcrun ENOENT'));
const result = await doctorResourceLogic(mockExecutor);
expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('XcodeBuildMCP Doctor');
expect(result.contents[0].text).toContain('Error: spawn xcrun ENOENT');
});
it('should include required doctor sections', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'Mock output',
});
const result = await doctorResourceLogic(mockExecutor);
expect(result.contents[0].text).toContain('## Troubleshooting Tips');
expect(result.contents[0].text).toContain('brew tap cameroncooke/axe');
expect(result.contents[0].text).toContain('INCREMENTAL_BUILDS_ENABLED=1');
});
it('should provide feature status information', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'Mock output',
});
const result = await doctorResourceLogic(mockExecutor);
expect(result.contents[0].text).toContain('### UI Automation (axe)');
expect(result.contents[0].text).toContain('### Incremental Builds');
expect(result.contents[0].text).toContain('### Mise Integration');
expect(result.contents[0].text).toContain('## Tool Availability Summary');
});
it('should handle error conditions gracefully', async () => {
const mockExecutor = createMockExecutor({
success: false,
output: '',
error: 'Command failed',
});
const result = await doctorResourceLogic(mockExecutor);
expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('XcodeBuildMCP Doctor');
});
});
});
```
--------------------------------------------------------------------------------
/src/mcp/tools/swift-package/swift_package_test.ts:
--------------------------------------------------------------------------------
```typescript
import * as z from 'zod';
import path from 'node:path';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.ts';
import { log } from '../../../utils/logging/index.ts';
import { ToolResponse } from '../../../types/common.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
// Define schema as ZodObject
const swiftPackageTestSchema = z.object({
packagePath: z.string().describe('Path to the Swift package root (Required)'),
testProduct: z.string().optional().describe('Optional specific test product to run'),
filter: z.string().optional().describe('Filter tests by name (regex pattern)'),
configuration: z
.enum(['debug', 'release'])
.optional()
.describe('Swift package configuration (debug, release)'),
parallel: z.boolean().optional().describe('Run tests in parallel (default: true)'),
showCodecov: z.boolean().optional().describe('Show code coverage (default: false)'),
parseAsLibrary: z
.boolean()
.optional()
.describe('Add -parse-as-library flag for @main support (default: false)'),
});
// Use z.infer for type safety
type SwiftPackageTestParams = z.infer<typeof swiftPackageTestSchema>;
export async function swift_package_testLogic(
params: SwiftPackageTestParams,
executor: CommandExecutor,
): Promise<ToolResponse> {
const resolvedPath = path.resolve(params.packagePath);
const swiftArgs = ['test', '--package-path', resolvedPath];
if (params.configuration && params.configuration.toLowerCase() === 'release') {
swiftArgs.push('-c', 'release');
} else if (params.configuration && params.configuration.toLowerCase() !== 'debug') {
return createTextResponse("Invalid configuration. Use 'debug' or 'release'.", true);
}
if (params.testProduct) {
swiftArgs.push('--test-product', params.testProduct);
}
if (params.filter) {
swiftArgs.push('--filter', params.filter);
}
if (params.parallel === false) {
swiftArgs.push('--no-parallel');
}
if (params.showCodecov) {
swiftArgs.push('--show-code-coverage');
}
if (params.parseAsLibrary) {
swiftArgs.push('-Xswiftc', '-parse-as-library');
}
log('info', `Running swift ${swiftArgs.join(' ')}`);
try {
const result = await executor(['swift', ...swiftArgs], 'Swift Package Test', true, undefined);
if (!result.success) {
const errorMessage = result.error ?? result.output ?? 'Unknown error';
return createErrorResponse('Swift package tests failed', errorMessage);
}
return {
content: [
{ type: 'text', text: '✅ Swift package tests completed.' },
{
type: 'text',
text: '💡 Next: Execute your app with swift_package_run if tests passed',
},
{ type: 'text', text: result.output },
],
isError: false,
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log('error', `Swift package test failed: ${message}`);
return createErrorResponse('Failed to execute swift test', message);
}
}
export default {
name: 'swift_package_test',
description: 'Runs tests for a Swift Package with swift test',
schema: swiftPackageTestSchema.shape, // MCP SDK compatibility
annotations: {
title: 'Swift Package Test',
destructiveHint: true,
},
handler: createTypedTool(
swiftPackageTestSchema,
swift_package_testLogic,
getDefaultCommandExecutor,
),
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator-management/__tests__/reset_sim_location.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from 'vitest';
import * as z from 'zod';
import resetSimLocationPlugin, { reset_sim_locationLogic } from '../reset_sim_location.ts';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
describe('reset_sim_location plugin', () => {
describe('Export Field Validation (Literal)', () => {
it('should have correct name field', () => {
expect(resetSimLocationPlugin.name).toBe('reset_sim_location');
});
it('should have correct description field', () => {
expect(resetSimLocationPlugin.description).toBe(
"Resets the simulator's location to default.",
);
});
it('should have handler function', () => {
expect(typeof resetSimLocationPlugin.handler).toBe('function');
});
it('should hide simulatorId from public schema', () => {
const schema = z.object(resetSimLocationPlugin.schema);
expect(schema.safeParse({}).success).toBe(true);
const withSimId = schema.safeParse({ simulatorId: 'abc123' });
expect(withSimId.success).toBe(true);
expect('simulatorId' in (withSimId.data as any)).toBe(false);
});
});
describe('Handler Behavior (Complete Literal Returns)', () => {
it('should successfully reset simulator location', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'Location reset successfully',
});
const result = await reset_sim_locationLogic(
{
simulatorId: 'test-uuid-123',
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Successfully reset simulator test-uuid-123 location.',
},
],
});
});
it('should handle command failure', async () => {
const mockExecutor = createMockExecutor({
success: false,
error: 'Command failed',
});
const result = await reset_sim_locationLogic(
{
simulatorId: 'test-uuid-123',
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Failed to reset simulator location: Command failed',
},
],
});
});
it('should handle exception during execution', async () => {
const mockExecutor = createMockExecutor(new Error('Network error'));
const result = await reset_sim_locationLogic(
{
simulatorId: 'test-uuid-123',
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Failed to reset simulator location: Network error',
},
],
});
});
it('should call correct command', async () => {
let capturedCommand: string[] = [];
let capturedLogPrefix: string | undefined;
const mockExecutor = createMockExecutor({
success: true,
output: 'Location reset successfully',
});
// Create a wrapper to capture the command arguments
const capturingExecutor = async (command: string[], logPrefix?: string) => {
capturedCommand = command;
capturedLogPrefix = logPrefix;
return mockExecutor(command, logPrefix);
};
await reset_sim_locationLogic(
{
simulatorId: 'test-uuid-123',
},
capturingExecutor,
);
expect(capturedCommand).toEqual(['xcrun', 'simctl', 'location', 'test-uuid-123', 'clear']);
expect(capturedLogPrefix).toBe('Reset Simulator Location');
});
});
});
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator-management/reset_sim_location.ts:
--------------------------------------------------------------------------------
```typescript
import * as z from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
// Define schema as ZodObject
const resetSimulatorLocationSchema = z.object({
simulatorId: z.uuid().describe('UUID of the simulator to use (obtained from list_simulators)'),
});
// Use z.infer for type safety
type ResetSimulatorLocationParams = z.infer<typeof resetSimulatorLocationSchema>;
// Helper function to execute simctl commands and handle responses
async function executeSimctlCommandAndRespond(
params: ResetSimulatorLocationParams,
simctlSubCommand: string[],
operationDescriptionForXcodeCommand: string,
successMessage: string,
failureMessagePrefix: string,
operationLogContext: string,
executor: CommandExecutor,
extraValidation?: () => ToolResponse | undefined,
): Promise<ToolResponse> {
if (extraValidation) {
const validationResult = extraValidation();
if (validationResult) {
return validationResult;
}
}
try {
const command = ['xcrun', 'simctl', ...simctlSubCommand];
const result = await executor(command, operationDescriptionForXcodeCommand, true, {});
if (!result.success) {
const fullFailureMessage = `${failureMessagePrefix}: ${result.error}`;
log(
'error',
`${fullFailureMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorId})`,
);
return {
content: [{ type: 'text', text: fullFailureMessage }],
};
}
log(
'info',
`${successMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorId})`,
);
return {
content: [{ type: 'text', text: successMessage }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const fullFailureMessage = `${failureMessagePrefix}: ${errorMessage}`;
log(
'error',
`Error during ${operationLogContext} for simulator ${params.simulatorId}: ${errorMessage}`,
);
return {
content: [{ type: 'text', text: fullFailureMessage }],
};
}
}
export async function reset_sim_locationLogic(
params: ResetSimulatorLocationParams,
executor: CommandExecutor,
): Promise<ToolResponse> {
log('info', `Resetting simulator ${params.simulatorId} location`);
return executeSimctlCommandAndRespond(
params,
['location', params.simulatorId, 'clear'],
'Reset Simulator Location',
`Successfully reset simulator ${params.simulatorId} location.`,
'Failed to reset simulator location',
'reset simulator location',
executor,
);
}
const publicSchemaObject = z.strictObject(
resetSimulatorLocationSchema.omit({ simulatorId: true } as const).shape,
);
export default {
name: 'reset_sim_location',
description: "Resets the simulator's location to default.",
schema: getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: resetSimulatorLocationSchema,
}),
annotations: {
title: 'Reset Simulator Location',
destructiveHint: true,
},
handler: createSessionAwareTool<ResetSimulatorLocationParams>({
internalSchema: resetSimulatorLocationSchema as unknown as z.ZodType<
ResetSimulatorLocationParams,
unknown
>,
logicFunction: reset_sim_locationLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
}),
};
```
--------------------------------------------------------------------------------
/src/utils/errors.ts:
--------------------------------------------------------------------------------
```typescript
import { ToolResponse } from '../types/common.ts';
/**
* Error Utilities - Type-safe error hierarchy for the application
*
* This utility module defines a structured error hierarchy for the application,
* providing specialized error types for different failure scenarios. Using these
* typed errors enables more precise error handling, improves debugging, and
* provides better error messages to users.
*
* Responsibilities:
* - Providing a base error class (XcodeBuildMCPError) for all application errors
* - Defining specialized error subtypes for different error categories:
* - ValidationError: Parameter validation failures
* - SystemError: Underlying system/OS issues
* - ConfigurationError: Application configuration problems
* - SimulatorError: iOS simulator-specific failures
* - AxeError: axe-specific errors
*
* The structured hierarchy allows error consumers to handle errors with the
* appropriate level of specificity using instanceof checks or catch clauses.
*/
/**
* Custom error types for XcodeBuildMCP
*/
/**
* Base error class for XcodeBuildMCP errors
*/
export class XcodeBuildMCPError extends Error {
constructor(message: string) {
super(message);
this.name = 'XcodeBuildMCPError';
// This is necessary for proper inheritance in TypeScript
Object.setPrototypeOf(this, XcodeBuildMCPError.prototype);
}
}
/**
* Error thrown when validation of parameters fails
*/
export class ValidationError extends XcodeBuildMCPError {
constructor(
message: string,
public paramName?: string,
) {
super(message);
this.name = 'ValidationError';
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
/**
* Error thrown for system-level errors (file access, permissions, etc.)
*/
export class SystemError extends XcodeBuildMCPError {
constructor(
message: string,
public originalError?: Error,
) {
super(message);
this.name = 'SystemError';
Object.setPrototypeOf(this, SystemError.prototype);
}
}
/**
* Error thrown for configuration issues
*/
export class ConfigurationError extends XcodeBuildMCPError {
constructor(message: string) {
super(message);
this.name = 'ConfigurationError';
Object.setPrototypeOf(this, ConfigurationError.prototype);
}
}
/**
* Error thrown for simulator-specific errors
*/
export class SimulatorError extends XcodeBuildMCPError {
constructor(
message: string,
public simulatorName?: string,
public simulatorId?: string,
) {
super(message);
this.name = 'SimulatorError';
Object.setPrototypeOf(this, SimulatorError.prototype);
}
}
/**
* Error thrown for axe-specific errors
*/
export class AxeError extends XcodeBuildMCPError {
constructor(
message: string,
public command?: string, // The axe command that failed
public axeOutput?: string, // Output from axe
public simulatorId?: string,
) {
super(message);
this.name = 'AxeError';
Object.setPrototypeOf(this, AxeError.prototype);
}
}
// Helper to create a standard error response
export function createErrorResponse(message: string, details?: string): ToolResponse {
const detailText = details ? `\nDetails: ${details}` : '';
return {
content: [
{
type: 'text',
text: `Error: ${message}${detailText}`,
},
],
isError: true,
};
}
/**
* Error class for missing dependencies
*/
export class DependencyError extends ConfigurationError {
constructor(
message: string,
public details?: string,
) {
super(message);
this.name = 'DependencyError';
Object.setPrototypeOf(this, DependencyError.prototype);
}
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator/install_app_sim.ts:
--------------------------------------------------------------------------------
```typescript
import * as z from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { validateFileExists } from '../../../utils/validation/index.ts';
import type { CommandExecutor, FileSystemExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
const installAppSimSchemaObject = z.object({
simulatorId: z.string().describe('UUID of the simulator to use (obtained from list_sims)'),
appPath: z
.string()
.describe('Path to the .app bundle to install (full path to the .app directory)'),
});
type InstallAppSimParams = z.infer<typeof installAppSimSchemaObject>;
const publicSchemaObject = z.strictObject(
installAppSimSchemaObject.omit({
simulatorId: true,
} as const).shape,
);
export async function install_app_simLogic(
params: InstallAppSimParams,
executor: CommandExecutor,
fileSystem?: FileSystemExecutor,
): Promise<ToolResponse> {
const appPathExistsValidation = validateFileExists(params.appPath, fileSystem);
if (!appPathExistsValidation.isValid) {
return appPathExistsValidation.errorResponse!;
}
log('info', `Starting xcrun simctl install request for simulator ${params.simulatorId}`);
try {
const command = ['xcrun', 'simctl', 'install', params.simulatorId, params.appPath];
const result = await executor(command, 'Install App in Simulator', true, undefined);
if (!result.success) {
return {
content: [
{
type: 'text',
text: `Install app in simulator operation failed: ${result.error}`,
},
],
};
}
let bundleId = '';
try {
const bundleIdResult = await executor(
['defaults', 'read', `${params.appPath}/Info`, 'CFBundleIdentifier'],
'Extract Bundle ID',
false,
undefined,
);
if (bundleIdResult.success) {
bundleId = bundleIdResult.output.trim();
}
} catch (error) {
log('warning', `Could not extract bundle ID from app: ${error}`);
}
return {
content: [
{
type: 'text',
text: `App installed successfully in simulator ${params.simulatorId}`,
},
{
type: 'text',
text: `Next Steps:
1. Open the Simulator app: open_sim({})
2. Launch the app: launch_app_sim({ simulatorId: "${params.simulatorId}"${
bundleId ? `, bundleId: "${bundleId}"` : ', bundleId: "YOUR_APP_BUNDLE_ID"'
} })`,
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log('error', `Error during install app in simulator operation: ${errorMessage}`);
return {
content: [
{
type: 'text',
text: `Install app in simulator operation failed: ${errorMessage}`,
},
],
};
}
}
export default {
name: 'install_app_sim',
description: 'Installs an app in an iOS simulator.',
schema: getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: installAppSimSchemaObject,
}),
annotations: {
title: 'Install App Simulator',
destructiveHint: true,
},
handler: createSessionAwareTool<InstallAppSimParams>({
internalSchema: installAppSimSchemaObject as unknown as z.ZodType<InstallAppSimParams, unknown>,
logicFunction: install_app_simLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
}),
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator-management/sim_statusbar.ts:
--------------------------------------------------------------------------------
```typescript
import * as z from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
// Define schema as ZodObject
const simStatusbarSchema = z.object({
simulatorId: z.uuid().describe('UUID of the simulator to use (obtained from list_simulators)'),
dataNetwork: z
.enum([
'clear',
'hide',
'wifi',
'3g',
'4g',
'lte',
'lte-a',
'lte+',
'5g',
'5g+',
'5g-uwb',
'5g-uc',
])
.describe(
'Data network type to display in status bar. Use "clear" to reset all overrides. Valid values: clear, hide, wifi, 3g, 4g, lte, lte-a, lte+, 5g, 5g+, 5g-uwb, 5g-uc.',
),
});
// Use z.infer for type safety
type SimStatusbarParams = z.infer<typeof simStatusbarSchema>;
export async function sim_statusbarLogic(
params: SimStatusbarParams,
executor: CommandExecutor,
): Promise<ToolResponse> {
log(
'info',
`Setting simulator ${params.simulatorId} status bar data network to ${params.dataNetwork}`,
);
try {
let command: string[];
let successMessage: string;
if (params.dataNetwork === 'clear') {
command = ['xcrun', 'simctl', 'status_bar', params.simulatorId, 'clear'];
successMessage = `Successfully cleared status bar overrides for simulator ${params.simulatorId}`;
} else {
command = [
'xcrun',
'simctl',
'status_bar',
params.simulatorId,
'override',
'--dataNetwork',
params.dataNetwork,
];
successMessage = `Successfully set simulator ${params.simulatorId} status bar data network to ${params.dataNetwork}`;
}
const result = await executor(command, 'Set Status Bar', true, undefined);
if (!result.success) {
const failureMessage = `Failed to set status bar: ${result.error}`;
log('error', `${failureMessage} (simulator: ${params.simulatorId})`);
return {
content: [{ type: 'text', text: failureMessage }],
isError: true,
};
}
log('info', `${successMessage} (simulator: ${params.simulatorId})`);
return {
content: [{ type: 'text', text: successMessage }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const failureMessage = `Failed to set status bar: ${errorMessage}`;
log('error', `Error setting status bar for simulator ${params.simulatorId}: ${errorMessage}`);
return {
content: [{ type: 'text', text: failureMessage }],
isError: true,
};
}
}
const publicSchemaObject = z.strictObject(
simStatusbarSchema.omit({ simulatorId: true } as const).shape,
);
export default {
name: 'sim_statusbar',
description:
'Sets the data network indicator in the iOS simulator status bar. Use "clear" to reset all overrides, or specify a network type (hide, wifi, 3g, 4g, lte, lte-a, lte+, 5g, 5g+, 5g-uwb, 5g-uc).',
schema: getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: simStatusbarSchema,
}), // MCP SDK compatibility
annotations: {
title: 'Simulator Statusbar',
destructiveHint: true,
},
handler: createSessionAwareTool<SimStatusbarParams>({
internalSchema: simStatusbarSchema as unknown as z.ZodType<SimStatusbarParams, unknown>,
logicFunction: sim_statusbarLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
}),
};
```
--------------------------------------------------------------------------------
/src/core/__tests__/resources.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { registerResources, getAvailableResources, loadResources } from '../resources.ts';
describe('resources', () => {
let mockServer: McpServer;
let registeredResources: Array<{
name: string;
uri: string;
metadata: { mimeType: string; title: string };
handler: any;
}>;
beforeEach(() => {
registeredResources = [];
// Create a mock MCP server using simple object structure
mockServer = {
resource: (
name: string,
uri: string,
metadata: { mimeType: string; title: string },
handler: any,
) => {
registeredResources.push({ name, uri, metadata, handler });
},
} as unknown as McpServer;
});
describe('Exports', () => {
it('should export registerResources function', () => {
expect(typeof registerResources).toBe('function');
});
it('should export getAvailableResources function', () => {
expect(typeof getAvailableResources).toBe('function');
});
it('should export loadResources function', () => {
expect(typeof loadResources).toBe('function');
});
});
describe('loadResources', () => {
it('should load resources from generated loaders', async () => {
const resources = await loadResources();
// Should have at least the simulators resource
expect(resources.size).toBeGreaterThan(0);
expect(resources.has('xcodebuildmcp://simulators')).toBe(true);
});
it('should validate resource structure', async () => {
const resources = await loadResources();
for (const [uri, resource] of resources) {
expect(resource.uri).toBe(uri);
expect(typeof resource.description).toBe('string');
expect(typeof resource.mimeType).toBe('string');
expect(typeof resource.handler).toBe('function');
}
});
});
describe('registerResources', () => {
it('should register all loaded resources with the server and return true', async () => {
const result = await registerResources(mockServer);
expect(result).toBe(true);
// Should have registered at least one resource
expect(registeredResources.length).toBeGreaterThan(0);
// Check simulators resource was registered
const simulatorsResource = registeredResources.find(
(r) => r.uri === 'xcodebuildmcp://simulators',
);
expect(typeof simulatorsResource?.handler).toBe('function');
expect(simulatorsResource?.metadata.title).toBe(
'Available iOS simulators with their UUIDs and states',
);
expect(simulatorsResource?.metadata.mimeType).toBe('text/plain');
expect(simulatorsResource?.name).toBe('simulators');
});
it('should register resources with correct handlers', async () => {
const result = await registerResources(mockServer);
expect(result).toBe(true);
const simulatorsResource = registeredResources.find(
(r) => r.uri === 'xcodebuildmcp://simulators',
);
expect(typeof simulatorsResource?.handler).toBe('function');
});
});
describe('getAvailableResources', () => {
it('should return array of available resource URIs', async () => {
const resources = await getAvailableResources();
expect(Array.isArray(resources)).toBe(true);
expect(resources.length).toBeGreaterThan(0);
expect(resources).toContain('xcodebuildmcp://simulators');
});
it('should return unique URIs', async () => {
const resources = await getAvailableResources();
const uniqueResources = [...new Set(resources)];
expect(resources.length).toBe(uniqueResources.length);
});
});
});
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator-management/set_sim_appearance.ts:
--------------------------------------------------------------------------------
```typescript
import * as z from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
// Define schema as ZodObject
const setSimAppearanceSchema = z.object({
simulatorId: z.uuid().describe('UUID of the simulator to use (obtained from list_simulators)'),
mode: z.enum(['dark', 'light']).describe('The appearance mode to set (either "dark" or "light")'),
});
// Use z.infer for type safety
type SetSimAppearanceParams = z.infer<typeof setSimAppearanceSchema>;
// Helper function to execute simctl commands and handle responses
async function executeSimctlCommandAndRespond(
params: SetSimAppearanceParams,
simctlSubCommand: string[],
operationDescriptionForXcodeCommand: string,
successMessage: string,
failureMessagePrefix: string,
operationLogContext: string,
extraValidation?: () => ToolResponse | undefined,
executor: CommandExecutor = getDefaultCommandExecutor(),
): Promise<ToolResponse> {
if (extraValidation) {
const validationResult = extraValidation();
if (validationResult) {
return validationResult;
}
}
try {
const command = ['xcrun', 'simctl', ...simctlSubCommand];
const result = await executor(command, operationDescriptionForXcodeCommand, true, undefined);
if (!result.success) {
const fullFailureMessage = `${failureMessagePrefix}: ${result.error}`;
log(
'error',
`${fullFailureMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorId})`,
);
return {
content: [{ type: 'text', text: fullFailureMessage }],
};
}
log(
'info',
`${successMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorId})`,
);
return {
content: [{ type: 'text', text: successMessage }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const fullFailureMessage = `${failureMessagePrefix}: ${errorMessage}`;
log(
'error',
`Error during ${operationLogContext} for simulator ${params.simulatorId}: ${errorMessage}`,
);
return {
content: [{ type: 'text', text: fullFailureMessage }],
};
}
}
export async function set_sim_appearanceLogic(
params: SetSimAppearanceParams,
executor: CommandExecutor,
): Promise<ToolResponse> {
log('info', `Setting simulator ${params.simulatorId} appearance to ${params.mode} mode`);
return executeSimctlCommandAndRespond(
params,
['ui', params.simulatorId, 'appearance', params.mode],
'Set Simulator Appearance',
`Successfully set simulator ${params.simulatorId} appearance to ${params.mode} mode`,
'Failed to set simulator appearance',
'set simulator appearance',
undefined,
executor,
);
}
const publicSchemaObject = z.strictObject(
setSimAppearanceSchema.omit({ simulatorId: true } as const).shape,
);
export default {
name: 'set_sim_appearance',
description: 'Sets the appearance mode (dark/light) of an iOS simulator.',
schema: getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: setSimAppearanceSchema,
}),
annotations: {
title: 'Set Simulator Appearance',
destructiveHint: true,
},
handler: createSessionAwareTool<SetSimAppearanceParams>({
internalSchema: setSimAppearanceSchema as unknown as z.ZodType<SetSimAppearanceParams, unknown>,
logicFunction: set_sim_appearanceLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
}),
};
```
--------------------------------------------------------------------------------
/example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/CalculatorButton.swift:
--------------------------------------------------------------------------------
```swift
import SwiftUI
// MARK: - Calculator Button Component
struct CalculatorButton: View {
let title: String
let buttonType: CalculatorButtonType
let isWideButton: Bool
let action: () -> Void
@State private var isPressed = false
var body: some View {
if buttonType == .hidden {
// Empty space for layout
Color.clear
.frame(height: 80)
} else {
Button(action: {
withAnimation(.easeInOut(duration: 0.1)) {
isPressed = true
}
action()
Task {
try await Task.sleep(for: .seconds(0.1))
await MainActor.run {
withAnimation(.easeInOut(duration: 0.1)) {
isPressed = false
}
}
}
}) {
ZStack {
// Frosted glass background
RoundedRectangle(cornerRadius: 20)
.fill(.ultraThinMaterial)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(buttonType.borderColor, lineWidth: 1)
)
.overlay(
// Subtle inner glow
RoundedRectangle(cornerRadius: 20)
.fill(
RadialGradient(
colors: [buttonType.glowColor.opacity(0.3), Color.clear],
center: .topLeading,
startRadius: 0,
endRadius: 50
)
)
)
.scaleEffect(isPressed ? 0.95 : 1.0)
.shadow(color: buttonType.shadowColor.opacity(0.3), radius: isPressed ? 2 : 8, x: 0, y: isPressed ? 1 : 4)
// Button text
Text(title)
.font(.system(size: 32, weight: .medium, design: .rounded))
.foregroundColor(buttonType.textColor)
.scaleEffect(isPressed ? 0.9 : 1.0)
}
}
.frame(height: 80)
.gridCellColumns(isWideButton ? 2 : 1)
.buttonStyle(PlainButtonStyle())
}
}
}
// MARK: - Button Type Configuration
enum CalculatorButtonType {
case number, operation, function, hidden
var textColor: Color {
switch self {
case .number:
return .white
case .operation:
return .white
case .function:
return .white
case .hidden:
return .clear
}
}
var borderColor: Color {
switch self {
case .number:
return .white.opacity(0.3)
case .operation:
return .orange.opacity(0.6)
case .function:
return .gray.opacity(0.5)
case .hidden:
return .clear
}
}
var glowColor: Color {
switch self {
case .number:
return .blue
case .operation:
return .orange
case .function:
return .gray
case .hidden:
return .clear
}
}
var shadowColor: Color {
switch self {
case .number:
return .blue
case .operation:
return .orange
case .function:
return .gray
case .hidden:
return .clear
}
}
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/project-discovery/get_mac_bundle_id.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Project Discovery Plugin: Get macOS Bundle ID
*
* Extracts the bundle identifier from a macOS app bundle (.app).
*/
import * as z from 'zod';
import { log } from '../../../utils/logging/index.ts';
import { ToolResponse } from '../../../types/common.ts';
import {
CommandExecutor,
getDefaultFileSystemExecutor,
getDefaultCommandExecutor,
} from '../../../utils/command.ts';
import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
/**
* Sync wrapper for CommandExecutor to handle synchronous commands
*/
async function executeSyncCommand(command: string, executor: CommandExecutor): Promise<string> {
const result = await executor(['/bin/sh', '-c', command], 'macOS Bundle ID Extraction');
if (!result.success) {
throw new Error(result.error ?? 'Command failed');
}
return result.output || '';
}
// Define schema as ZodObject
const getMacBundleIdSchema = z.object({
appPath: z
.string()
.describe(
'Path to the macOS .app bundle to extract bundle ID from (full path to the .app directory)',
),
});
// Use z.infer for type safety
type GetMacBundleIdParams = z.infer<typeof getMacBundleIdSchema>;
/**
* Business logic for extracting macOS bundle ID
*/
export async function get_mac_bundle_idLogic(
params: GetMacBundleIdParams,
executor: CommandExecutor,
fileSystemExecutor: FileSystemExecutor,
): Promise<ToolResponse> {
const appPath = params.appPath;
if (!fileSystemExecutor.existsSync(appPath)) {
return {
content: [
{
type: 'text',
text: `File not found: '${appPath}'. Please check the path and try again.`,
},
],
isError: true,
};
}
log('info', `Starting bundle ID extraction for macOS app: ${appPath}`);
try {
let bundleId;
try {
bundleId = await executeSyncCommand(
`defaults read "${appPath}/Contents/Info" CFBundleIdentifier`,
executor,
);
} catch {
try {
bundleId = await executeSyncCommand(
`/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${appPath}/Contents/Info.plist"`,
executor,
);
} catch (innerError) {
throw new Error(
`Could not extract bundle ID from Info.plist: ${innerError instanceof Error ? innerError.message : String(innerError)}`,
);
}
}
log('info', `Extracted macOS bundle ID: ${bundleId}`);
return {
content: [
{
type: 'text',
text: `✅ Bundle ID: ${bundleId}`,
},
{
type: 'text',
text: `Next Steps:
- Launch: launch_mac_app({ appPath: "${appPath}" })
- Build again: build_macos({ scheme: "SCHEME_NAME" })`,
},
],
isError: false,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log('error', `Error extracting macOS bundle ID: ${errorMessage}`);
return {
content: [
{
type: 'text',
text: `Error extracting macOS bundle ID: ${errorMessage}`,
},
{
type: 'text',
text: `Make sure the path points to a valid macOS app bundle (.app directory).`,
},
],
isError: true,
};
}
}
export default {
name: 'get_mac_bundle_id',
description:
"Extracts the bundle identifier from a macOS app bundle (.app). IMPORTANT: You MUST provide the appPath parameter. Example: get_mac_bundle_id({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_get_macos_bundle_id.",
schema: getMacBundleIdSchema.shape, // MCP SDK compatibility
annotations: {
title: 'Get Mac Bundle ID',
readOnlyHint: true,
},
handler: createTypedTool(
getMacBundleIdSchema,
(params: GetMacBundleIdParams) =>
get_mac_bundle_idLogic(params, getDefaultCommandExecutor(), getDefaultFileSystemExecutor()),
getDefaultCommandExecutor,
),
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/project-discovery/get_app_bundle_id.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Project Discovery Plugin: Get App Bundle ID
*
* Extracts the bundle identifier from an app bundle (.app) for any Apple platform
* (iOS, iPadOS, watchOS, tvOS, visionOS).
*/
import * as z from 'zod';
import { log } from '../../../utils/logging/index.ts';
import { ToolResponse } from '../../../types/common.ts';
import {
CommandExecutor,
getDefaultFileSystemExecutor,
getDefaultCommandExecutor,
} from '../../../utils/command.ts';
import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
// Define schema as ZodObject
const getAppBundleIdSchema = z.object({
appPath: z
.string()
.describe(
'Path to the .app bundle to extract bundle ID from (full path to the .app directory)',
),
});
// Use z.infer for type safety
type GetAppBundleIdParams = z.infer<typeof getAppBundleIdSchema>;
/**
* Sync wrapper for CommandExecutor to handle synchronous commands
*/
async function executeSyncCommand(command: string, executor: CommandExecutor): Promise<string> {
const result = await executor(['/bin/sh', '-c', command], 'Bundle ID Extraction');
if (!result.success) {
throw new Error(result.error ?? 'Command failed');
}
return result.output || '';
}
/**
* Business logic for extracting bundle ID from app.
* Separated for testing and reusability.
*/
export async function get_app_bundle_idLogic(
params: GetAppBundleIdParams,
executor: CommandExecutor,
fileSystemExecutor: FileSystemExecutor,
): Promise<ToolResponse> {
// Zod validation is handled by createTypedTool, so params.appPath is guaranteed to be a string
const appPath = params.appPath;
if (!fileSystemExecutor.existsSync(appPath)) {
return {
content: [
{
type: 'text',
text: `File not found: '${appPath}'. Please check the path and try again.`,
},
],
isError: true,
};
}
log('info', `Starting bundle ID extraction for app: ${appPath}`);
try {
let bundleId;
try {
bundleId = await executeSyncCommand(
`defaults read "${appPath}/Info" CFBundleIdentifier`,
executor,
);
} catch {
try {
bundleId = await executeSyncCommand(
`/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${appPath}/Info.plist"`,
executor,
);
} catch (innerError) {
throw new Error(
`Could not extract bundle ID from Info.plist: ${innerError instanceof Error ? innerError.message : String(innerError)}`,
);
}
}
log('info', `Extracted app bundle ID: ${bundleId}`);
return {
content: [
{
type: 'text',
text: `✅ Bundle ID: ${bundleId}`,
},
{
type: 'text',
text: `Next Steps:
- Simulator: install_app_sim + launch_app_sim
- Device: install_app_device + launch_app_device`,
},
],
isError: false,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log('error', `Error extracting app bundle ID: ${errorMessage}`);
return {
content: [
{
type: 'text',
text: `Error extracting app bundle ID: ${errorMessage}`,
},
{
type: 'text',
text: `Make sure the path points to a valid app bundle (.app directory).`,
},
],
isError: true,
};
}
}
export default {
name: 'get_app_bundle_id',
description:
"Extracts the bundle identifier from an app bundle (.app) for any Apple platform (iOS, iPadOS, watchOS, tvOS, visionOS). IMPORTANT: You MUST provide the appPath parameter. Example: get_app_bundle_id({ appPath: '/path/to/your/app.app' })",
schema: getAppBundleIdSchema.shape, // MCP SDK compatibility
annotations: {
title: 'Get App Bundle ID',
readOnlyHint: true,
},
handler: createTypedTool(
getAppBundleIdSchema,
(params: GetAppBundleIdParams) =>
get_app_bundle_idLogic(params, getDefaultCommandExecutor(), getDefaultFileSystemExecutor()),
getDefaultCommandExecutor,
),
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/macos/build_macos.ts:
--------------------------------------------------------------------------------
```typescript
/**
* macOS Shared Plugin: Build macOS (Unified)
*
* Builds a macOS app using xcodebuild from a project or workspace.
* Accepts mutually exclusive `projectPath` or `workspacePath`.
*/
import * as z from 'zod';
import { log } from '../../../utils/logging/index.ts';
import { executeXcodeBuildCommand } from '../../../utils/build/index.ts';
import { ToolResponse, XcodePlatform } from '../../../types/common.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
// Types for dependency injection
export interface BuildUtilsDependencies {
executeXcodeBuildCommand: typeof executeXcodeBuildCommand;
}
// Default implementations
const defaultBuildUtilsDependencies: BuildUtilsDependencies = {
executeXcodeBuildCommand,
};
// Unified schema: XOR between projectPath and workspacePath
const baseSchemaObject = z.object({
projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
scheme: z.string().describe('The scheme to use'),
configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
derivedDataPath: z
.string()
.optional()
.describe('Path where build products and other derived data will go'),
arch: z
.enum(['arm64', 'x86_64'])
.optional()
.describe('Architecture to build for (arm64 or x86_64). For macOS only.'),
extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'),
preferXcodebuild: z
.boolean()
.optional()
.describe('If true, prefers xcodebuild over the experimental incremental build system'),
});
const publicSchemaObject = baseSchemaObject.omit({
projectPath: true,
workspacePath: true,
scheme: true,
configuration: true,
arch: true,
} as const);
const buildMacOSSchema = z.preprocess(
nullifyEmptyStrings,
baseSchemaObject
.refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
message: 'Either projectPath or workspacePath is required.',
})
.refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
}),
);
export type BuildMacOSParams = z.infer<typeof buildMacOSSchema>;
/**
* Business logic for building macOS apps from project or workspace with dependency injection.
* Exported for direct testing and reuse.
*/
export async function buildMacOSLogic(
params: BuildMacOSParams,
executor: CommandExecutor,
buildUtilsDeps: BuildUtilsDependencies = defaultBuildUtilsDependencies,
): Promise<ToolResponse> {
log('info', `Starting macOS build for scheme ${params.scheme} (internal)`);
const processedParams = {
...params,
configuration: params.configuration ?? 'Debug',
preferXcodebuild: params.preferXcodebuild ?? false,
};
return buildUtilsDeps.executeXcodeBuildCommand(
processedParams,
{
platform: XcodePlatform.macOS,
arch: params.arch,
logPrefix: 'macOS Build',
},
processedParams.preferXcodebuild ?? false,
'build',
executor,
);
}
export default {
name: 'build_macos',
description: 'Builds a macOS app.',
schema: getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: baseSchemaObject,
}),
annotations: {
title: 'Build macOS',
destructiveHint: true,
},
handler: createSessionAwareTool<BuildMacOSParams>({
internalSchema: buildMacOSSchema as unknown as z.ZodType<BuildMacOSParams, unknown>,
logicFunction: buildMacOSLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [
{ allOf: ['scheme'], message: 'scheme is required' },
{ oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
],
exclusivePairs: [['projectPath', 'workspacePath']],
}),
};
```
--------------------------------------------------------------------------------
/src/utils/sentry.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Sentry instrumentation for XcodeBuildMCP
*
* This file initializes Sentry when explicitly called to avoid side effects
* during module import (needed for Smithery's module-based entry).
*/
import * as Sentry from '@sentry/node';
import { execSync } from 'child_process';
import { version } from '../version.ts';
// Inlined system info functions to avoid circular dependencies
function getXcodeInfo(): { version: string; path: string; selectedXcode: string; error?: string } {
try {
const xcodebuildOutput = execSync('xcodebuild -version', { encoding: 'utf8' }).trim();
const version = xcodebuildOutput.split('\n').slice(0, 2).join(' - ');
const path = execSync('xcode-select -p', { encoding: 'utf8' }).trim();
const selectedXcode = execSync('xcrun --find xcodebuild', { encoding: 'utf8' }).trim();
return { version, path, selectedXcode };
} catch (error) {
return {
version: 'Not available',
path: 'Not available',
selectedXcode: 'Not available',
error: error instanceof Error ? error.message : String(error),
};
}
}
function getEnvironmentVariables(): Record<string, string> {
const relevantVars = [
'INCREMENTAL_BUILDS_ENABLED',
'PATH',
'DEVELOPER_DIR',
'HOME',
'USER',
'TMPDIR',
'NODE_ENV',
'SENTRY_DISABLED',
];
const envVars: Record<string, string> = {};
relevantVars.forEach((varName) => {
envVars[varName] = process.env[varName] ?? '';
});
Object.keys(process.env).forEach((key) => {
if (key.startsWith('XCODEBUILDMCP_')) {
envVars[key] = process.env[key] ?? '';
}
});
return envVars;
}
function checkBinaryAvailability(binary: string): { available: boolean; version?: string } {
try {
execSync(`which ${binary}`, { stdio: 'ignore' });
} catch {
return { available: false };
}
let version: string | undefined;
const versionCommands: Record<string, string> = {
axe: 'axe --version',
mise: 'mise --version',
};
if (binary in versionCommands) {
try {
version = execSync(versionCommands[binary], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
} catch {
// Version command failed, but binary exists
}
}
return { available: true, version };
}
let initialized = false;
function isSentryDisabled(): boolean {
return (
process.env.SENTRY_DISABLED === 'true' || process.env.XCODEBUILDMCP_SENTRY_DISABLED === 'true'
);
}
function isTestEnv(): boolean {
return process.env.VITEST === 'true' || process.env.NODE_ENV === 'test';
}
export function initSentry(): void {
if (initialized || isSentryDisabled() || isTestEnv()) {
return;
}
initialized = true;
Sentry.init({
dsn:
process.env.SENTRY_DSN ??
'https://798607831167c7b9fe2f2912f5d3c665@o4509258288332800.ingest.de.sentry.io/4509258293837904',
// Setting this option to true will send default PII data to Sentry
// For example, automatic IP address collection on events
sendDefaultPii: true,
// Set release version to match application version
release: `xcodebuildmcp@${version}`,
// Always report under production environment
environment: 'production',
// Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring
// We recommend adjusting this value in production
tracesSampleRate: 1.0,
});
const axeAvailable = checkBinaryAvailability('axe');
const miseAvailable = checkBinaryAvailability('mise');
const envVars = getEnvironmentVariables();
const xcodeInfo = getXcodeInfo();
// Add additional context that might be helpful for debugging
const tags: Record<string, string> = {
nodeVersion: process.version,
platform: process.platform,
arch: process.arch,
axeAvailable: axeAvailable.available ? 'true' : 'false',
axeVersion: axeAvailable.version ?? 'Unknown',
miseAvailable: miseAvailable.available ? 'true' : 'false',
miseVersion: miseAvailable.version ?? 'Unknown',
...Object.fromEntries(Object.entries(envVars).map(([k, v]) => [`env_${k}`, v ?? ''])),
xcodeVersion: xcodeInfo.version ?? 'Unknown',
xcodePath: xcodeInfo.path ?? 'Unknown',
};
Sentry.setTags(tags);
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator-management/__tests__/set_sim_appearance.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from 'vitest';
import * as z from 'zod';
import setSimAppearancePlugin, { set_sim_appearanceLogic } from '../set_sim_appearance.ts';
import {
createMockCommandResponse,
createMockExecutor,
} from '../../../../test-utils/mock-executors.ts';
describe('set_sim_appearance plugin', () => {
describe('Export Field Validation (Literal)', () => {
it('should have correct name field', () => {
expect(setSimAppearancePlugin.name).toBe('set_sim_appearance');
});
it('should have correct description field', () => {
expect(setSimAppearancePlugin.description).toBe(
'Sets the appearance mode (dark/light) of an iOS simulator.',
);
});
it('should have handler function', () => {
expect(typeof setSimAppearancePlugin.handler).toBe('function');
});
it('should expose public schema without simulatorId field', () => {
const schema = z.object(setSimAppearancePlugin.schema);
expect(schema.safeParse({ mode: 'dark' }).success).toBe(true);
expect(schema.safeParse({ mode: 'light' }).success).toBe(true);
expect(schema.safeParse({ mode: 'invalid' }).success).toBe(false);
const withSimId = schema.safeParse({ simulatorId: 'abc123', mode: 'dark' });
expect(withSimId.success).toBe(true);
expect('simulatorId' in (withSimId.data as any)).toBe(false);
});
});
describe('Handler Behavior (Complete Literal Returns)', () => {
it('should handle successful appearance change', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: '',
error: '',
});
const result = await set_sim_appearanceLogic(
{
simulatorId: 'test-uuid-123',
mode: 'dark',
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Successfully set simulator test-uuid-123 appearance to dark mode',
},
],
});
});
it('should handle appearance change failure', async () => {
const mockExecutor = createMockExecutor({
success: false,
error: 'Invalid device: invalid-uuid',
});
const result = await set_sim_appearanceLogic(
{
simulatorId: 'invalid-uuid',
mode: 'light',
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Failed to set simulator appearance: Invalid device: invalid-uuid',
},
],
});
});
it('should surface session default requirement when simulatorId is missing', async () => {
const result = await setSimAppearancePlugin.handler({ mode: 'dark' });
const message = result.content?.[0]?.text ?? '';
expect(message).toContain('Error: Missing required session defaults');
expect(message).toContain('simulatorId is required');
expect(result.isError).toBe(true);
});
it('should handle exception during execution', async () => {
const mockExecutor = createMockExecutor(new Error('Network error'));
const result = await set_sim_appearanceLogic(
{
simulatorId: 'test-uuid-123',
mode: 'dark',
},
mockExecutor,
);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Failed to set simulator appearance: Network error',
},
],
});
});
it('should call correct command', async () => {
const commandCalls: any[] = [];
const mockExecutor = (...args: any[]) => {
commandCalls.push(args);
return Promise.resolve(
createMockCommandResponse({
success: true,
output: '',
error: '',
}),
);
};
await set_sim_appearanceLogic(
{
simulatorId: 'test-uuid-123',
mode: 'dark',
},
mockExecutor,
);
expect(commandCalls).toEqual([
[
['xcrun', 'simctl', 'ui', 'test-uuid-123', 'appearance', 'dark'],
'Set Simulator Appearance',
true,
undefined,
],
]);
});
});
});
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator-management/set_sim_location.ts:
--------------------------------------------------------------------------------
```typescript
import * as z from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
// Define schema as ZodObject
const setSimulatorLocationSchema = z.object({
simulatorId: z.uuid().describe('UUID of the simulator to use (obtained from list_simulators)'),
latitude: z.number().describe('The latitude for the custom location.'),
longitude: z.number().describe('The longitude for the custom location.'),
});
// Use z.infer for type safety
type SetSimulatorLocationParams = z.infer<typeof setSimulatorLocationSchema>;
// Helper function to execute simctl commands and handle responses
async function executeSimctlCommandAndRespond(
params: SetSimulatorLocationParams,
simctlSubCommand: string[],
operationDescriptionForXcodeCommand: string,
successMessage: string,
failureMessagePrefix: string,
operationLogContext: string,
executor: CommandExecutor = getDefaultCommandExecutor(),
extraValidation?: () => ToolResponse | null,
): Promise<ToolResponse> {
if (extraValidation) {
const validationResult = extraValidation();
if (validationResult) {
return validationResult;
}
}
try {
const command = ['xcrun', 'simctl', ...simctlSubCommand];
const result = await executor(command, operationDescriptionForXcodeCommand, true, {});
if (!result.success) {
const fullFailureMessage = `${failureMessagePrefix}: ${result.error}`;
log(
'error',
`${fullFailureMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorId})`,
);
return {
content: [{ type: 'text', text: fullFailureMessage }],
};
}
log(
'info',
`${successMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorId})`,
);
return {
content: [{ type: 'text', text: successMessage }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const fullFailureMessage = `${failureMessagePrefix}: ${errorMessage}`;
log(
'error',
`Error during ${operationLogContext} for simulator ${params.simulatorId}: ${errorMessage}`,
);
return {
content: [{ type: 'text', text: fullFailureMessage }],
};
}
}
export async function set_sim_locationLogic(
params: SetSimulatorLocationParams,
executor: CommandExecutor,
): Promise<ToolResponse> {
const extraValidation = (): ToolResponse | null => {
if (params.latitude < -90 || params.latitude > 90) {
return {
content: [
{
type: 'text',
text: 'Latitude must be between -90 and 90 degrees',
},
],
};
}
if (params.longitude < -180 || params.longitude > 180) {
return {
content: [
{
type: 'text',
text: 'Longitude must be between -180 and 180 degrees',
},
],
};
}
return null;
};
log(
'info',
`Setting simulator ${params.simulatorId} location to ${params.latitude},${params.longitude}`,
);
return executeSimctlCommandAndRespond(
params,
['location', params.simulatorId, 'set', `${params.latitude},${params.longitude}`],
'Set Simulator Location',
`Successfully set simulator ${params.simulatorId} location to ${params.latitude},${params.longitude}`,
'Failed to set simulator location',
'set simulator location',
executor,
extraValidation,
);
}
const publicSchemaObject = z.strictObject(
setSimulatorLocationSchema.omit({ simulatorId: true } as const).shape,
);
export default {
name: 'set_sim_location',
description: 'Sets a custom GPS location for the simulator.',
schema: getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: setSimulatorLocationSchema,
}),
annotations: {
title: 'Set Simulator Location',
destructiveHint: true,
},
handler: createSessionAwareTool<SetSimulatorLocationParams>({
internalSchema: setSimulatorLocationSchema as unknown as z.ZodType<
SetSimulatorLocationParams,
unknown
>,
logicFunction: set_sim_locationLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
}),
};
```
--------------------------------------------------------------------------------
/src/utils/simulator-utils.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Simulator utility functions for name to UUID resolution
*/
import type { CommandExecutor } from './execution/index.ts';
import { ToolResponse } from '../types/common.ts';
import { log } from './logging/index.ts';
import { createErrorResponse } from './responses/index.ts';
/**
* UUID regex pattern to check if a string looks like a UUID
*/
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
/**
* Determines the simulator UUID from either a UUID or name.
*
* Behavior:
* - If simulatorUuid provided: return it directly
* - Else if simulatorName looks like a UUID (regex): treat it as UUID and return it
* - Else: resolve name → UUID via simctl and return the match (isAvailable === true)
*
* @param params Object containing optional simulatorUuid or simulatorName
* @param executor Command executor for running simctl commands
* @returns Object with uuid, optional warning, or error
*/
export async function determineSimulatorUuid(
params: { simulatorUuid?: string; simulatorId?: string; simulatorName?: string },
executor: CommandExecutor,
): Promise<{ uuid?: string; warning?: string; error?: ToolResponse }> {
const directUuid = params.simulatorUuid ?? params.simulatorId;
// If UUID is provided directly, use it
if (directUuid) {
log('info', `Using provided simulator UUID: ${directUuid}`);
return { uuid: directUuid };
}
// If name is provided, check if it's actually a UUID
if (params.simulatorName) {
// Check if the "name" is actually a UUID string
if (UUID_REGEX.test(params.simulatorName)) {
log(
'info',
`Simulator name '${params.simulatorName}' appears to be a UUID, using it directly`,
);
return {
uuid: params.simulatorName,
warning: `The simulatorName '${params.simulatorName}' appears to be a UUID. Consider using simulatorUuid parameter instead.`,
};
}
// Resolve name to UUID via simctl
log('info', `Looking up simulator UUID for name: ${params.simulatorName}`);
const listResult = await executor(
['xcrun', 'simctl', 'list', 'devices', 'available', '-j'],
'List available simulators',
);
if (!listResult.success) {
return {
error: createErrorResponse(
'Failed to list simulators',
listResult.error ?? 'Unknown error',
),
};
}
try {
interface SimulatorDevice {
udid: string;
name: string;
isAvailable: boolean;
}
interface DevicesData {
devices: Record<string, SimulatorDevice[]>;
}
const devicesData = JSON.parse(listResult.output ?? '{}') as DevicesData;
// Search through all runtime sections for the named device
for (const runtime of Object.keys(devicesData.devices)) {
const devices = devicesData.devices[runtime];
if (!Array.isArray(devices)) continue;
// Look for exact name match with isAvailable === true
const device = devices.find(
(d) => d.name === params.simulatorName && d.isAvailable === true,
);
if (device) {
log('info', `Found simulator '${params.simulatorName}' with UUID: ${device.udid}`);
return { uuid: device.udid };
}
}
// If no available device found, check if device exists but is unavailable
for (const runtime of Object.keys(devicesData.devices)) {
const devices = devicesData.devices[runtime];
if (!Array.isArray(devices)) continue;
const unavailableDevice = devices.find(
(d) => d.name === params.simulatorName && d.isAvailable === false,
);
if (unavailableDevice) {
return {
error: createErrorResponse(
`Simulator '${params.simulatorName}' exists but is not available`,
'The simulator may need to be downloaded or is incompatible with the current Xcode version',
),
};
}
}
// Device not found at all
return {
error: createErrorResponse(
`Simulator '${params.simulatorName}' not found`,
'Please check the simulator name or use "xcrun simctl list devices" to see available simulators',
),
};
} catch (parseError) {
return {
error: createErrorResponse(
'Failed to parse simulator list',
parseError instanceof Error ? parseError.message : String(parseError),
),
};
}
}
// Neither UUID nor name provided
return {
error: createErrorResponse(
'No simulator identifier provided',
'Either simulatorUuid or simulatorName is required',
),
};
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator/__tests__/open_sim.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for open_sim plugin
* Following CLAUDE.md testing standards with literal validation
* Using dependency injection for deterministic testing
*/
import { describe, it, expect } from 'vitest';
import * as z from 'zod';
import {
createMockCommandResponse,
createMockExecutor,
type CommandExecutor,
} from '../../../../test-utils/mock-executors.ts';
import openSim, { open_simLogic } from '../open_sim.ts';
describe('open_sim tool', () => {
describe('Export Field Validation (Literal)', () => {
it('should have correct name field', () => {
expect(openSim.name).toBe('open_sim');
});
it('should have correct description field', () => {
expect(openSim.description).toBe('Opens the iOS Simulator app.');
});
it('should have handler function', () => {
expect(typeof openSim.handler).toBe('function');
});
it('should have correct schema validation', () => {
const schema = z.object(openSim.schema);
// Schema is empty, so any object should pass
expect(schema.safeParse({}).success).toBe(true);
expect(
schema.safeParse({
anyProperty: 'value',
}).success,
).toBe(true);
// Empty schema should accept anything
expect(
schema.safeParse({
enabled: true,
}).success,
).toBe(true);
});
});
describe('Handler Behavior (Complete Literal Returns)', () => {
it('should return exact successful open simulator response', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: '',
});
const result = await open_simLogic({}, mockExecutor);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Simulator app opened successfully',
},
{
type: 'text',
text: `Next Steps:
1. Boot a simulator if needed: boot_sim({ simulatorId: 'UUID_FROM_LIST_SIMULATORS' })
2. Launch your app and interact with it
3. Log capture options:
- Option 1: Capture structured logs only (app continues running):
start_sim_log_cap({ simulatorId: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID' })
- Option 2: Capture both console and structured logs (app will restart):
start_sim_log_cap({ simulatorId: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID', captureConsole: true })
- Option 3: Launch app with logs in one step:
launch_app_logs_sim({ simulatorId: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID' })`,
},
],
});
});
it('should return exact command failure response', async () => {
const mockExecutor = createMockExecutor({
success: false,
error: 'Command failed',
});
const result = await open_simLogic({}, mockExecutor);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Open simulator operation failed: Command failed',
},
],
});
});
it('should return exact exception handling response', async () => {
const mockExecutor: CommandExecutor = async () => {
throw new Error('Test error');
};
const result = await open_simLogic({}, mockExecutor);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Open simulator operation failed: Test error',
},
],
});
});
it('should return exact string error handling response', async () => {
const mockExecutor: CommandExecutor = async () => {
throw 'String error';
};
const result = await open_simLogic({}, mockExecutor);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Open simulator operation failed: String error',
},
],
});
});
it('should verify command generation with mock executor', async () => {
const calls: Array<{
command: string[];
description?: string;
hideOutput?: boolean;
opts?: { cwd?: string };
}> = [];
const mockExecutor: CommandExecutor = async (
command,
description,
hideOutput,
opts,
detached,
) => {
calls.push({ command, description, hideOutput, opts });
void detached;
return createMockCommandResponse({
success: true,
output: '',
error: undefined,
});
};
await open_simLogic({}, mockExecutor);
expect(calls).toHaveLength(1);
expect(calls[0]).toEqual({
command: ['open', '-a', 'Simulator'],
description: 'Open Simulator',
hideOutput: true,
opts: undefined,
});
});
});
});
```
--------------------------------------------------------------------------------
/docs/dev/session-aware-migration-todo.md:
--------------------------------------------------------------------------------
```markdown
# Session-Aware Migration TODO
_Audit date: October 6, 2025_
Reference: [session_management_plan.md](session_management_plan.md)
## Utilities
- [x] `src/mcp/tools/utilities/clean.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
## Project Discovery
- [x] `src/mcp/tools/project-discovery/list_schemes.ts` — session defaults: `projectPath`, `workspacePath`.
- [x] `src/mcp/tools/project-discovery/show_build_settings.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`.
## Device Workflows
- [x] `src/mcp/tools/device/build_device.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
- [x] `src/mcp/tools/device/test_device.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `deviceId`, `configuration`.
- [x] `src/mcp/tools/device/get_device_app_path.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
- [x] `src/mcp/tools/device/install_app_device.ts` — session defaults: `deviceId`.
- [x] `src/mcp/tools/device/launch_app_device.ts` — session defaults: `deviceId`.
- [x] `src/mcp/tools/device/stop_app_device.ts` — session defaults: `deviceId`.
## Device Logging
- [x] `src/mcp/tools/logging/start_device_log_cap.ts` — session defaults: `deviceId`.
## macOS Workflows
- [x] `src/mcp/tools/macos/build_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`.
- [x] `src/mcp/tools/macos/build_run_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`.
- [x] `src/mcp/tools/macos/test_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
- [x] `src/mcp/tools/macos/get_mac_app_path.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`.
## Simulator Build/Test/Path
- [x] `src/mcp/tools/simulator/test_sim.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `simulatorId`, `simulatorName`, `configuration`, `useLatestOS`.
- [x] `src/mcp/tools/simulator/get_sim_app_path.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `simulatorId`, `simulatorName`, `configuration`, `useLatestOS`, `arch`.
## Simulator Runtime Actions
- [x] `src/mcp/tools/simulator/boot_sim.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
- [x] `src/mcp/tools/simulator/install_app_sim.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
- [x] `src/mcp/tools/simulator/launch_app_sim.ts` — session defaults: `simulatorId`, `simulatorName` (hydrate `simulatorUuid`).
- [x] `src/mcp/tools/simulator/launch_app_logs_sim.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
- [x] `src/mcp/tools/simulator/stop_app_sim.ts` — session defaults: `simulatorId`, `simulatorName` (hydrate `simulatorUuid`).
- [x] `src/mcp/tools/simulator/record_sim_video.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
## Simulator Management
- [x] `src/mcp/tools/simulator-management/erase_sims.ts` — session defaults: `simulatorId` (covers `simulatorUdid`).
- [x] `src/mcp/tools/simulator-management/set_sim_location.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
- [x] `src/mcp/tools/simulator-management/reset_sim_location.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
- [x] `src/mcp/tools/simulator-management/set_sim_appearance.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
- [x] `src/mcp/tools/simulator-management/sim_statusbar.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
## Simulator Logging
- [x] `src/mcp/tools/logging/start_sim_log_cap.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
## AXe UI Testing Tools
- [x] `src/mcp/tools/ui-testing/button.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
- [x] `src/mcp/tools/ui-testing/describe_ui.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
- [x] `src/mcp/tools/ui-testing/gesture.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
- [x] `src/mcp/tools/ui-testing/key_press.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
- [x] `src/mcp/tools/ui-testing/key_sequence.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
- [x] `src/mcp/tools/ui-testing/long_press.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
- [x] `src/mcp/tools/ui-testing/screenshot.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
- [x] `src/mcp/tools/ui-testing/swipe.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
- [x] `src/mcp/tools/ui-testing/tap.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
- [x] `src/mcp/tools/ui-testing/touch.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
- [x] `src/mcp/tools/ui-testing/type_text.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
```
--------------------------------------------------------------------------------
/src/mcp/tools/project-discovery/show_build_settings.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Project Discovery Plugin: Show Build Settings (Unified)
*
* Shows build settings from either a project or workspace using xcodebuild.
* Accepts mutually exclusive `projectPath` or `workspacePath`.
*/
import * as z from 'zod';
import { log } from '../../../utils/logging/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { createTextResponse } from '../../../utils/responses/index.ts';
import { ToolResponse } from '../../../types/common.ts';
import {
createSessionAwareTool,
getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
// Unified schema: XOR between projectPath and workspacePath
const baseSchemaObject = z.object({
projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
scheme: z.string().describe('Scheme name to show build settings for (Required)'),
});
const showBuildSettingsSchema = z.preprocess(
nullifyEmptyStrings,
baseSchemaObject
.refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
message: 'Either projectPath or workspacePath is required.',
})
.refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
}),
);
export type ShowBuildSettingsParams = z.infer<typeof showBuildSettingsSchema>;
/**
* Business logic for showing build settings from a project or workspace.
* Exported for direct testing and reuse.
*/
export async function showBuildSettingsLogic(
params: ShowBuildSettingsParams,
executor: CommandExecutor,
): Promise<ToolResponse> {
log('info', `Showing build settings for scheme ${params.scheme}`);
try {
// Create the command array for xcodebuild
const command = ['xcodebuild', '-showBuildSettings']; // -showBuildSettings as an option, not an action
const hasProjectPath = typeof params.projectPath === 'string';
const path = hasProjectPath ? params.projectPath : params.workspacePath;
if (hasProjectPath) {
command.push('-project', params.projectPath!);
} else {
command.push('-workspace', params.workspacePath!);
}
// Add the scheme
command.push('-scheme', params.scheme);
// Execute the command directly
const result = await executor(command, 'Show Build Settings', true);
if (!result.success) {
return createTextResponse(`Failed to show build settings: ${result.error}`, true);
}
// Create response based on which type was used (similar to workspace version with next steps)
const content: Array<{ type: 'text'; text: string }> = [
{
type: 'text',
text: hasProjectPath
? `✅ Build settings for scheme ${params.scheme}:`
: '✅ Build settings retrieved successfully',
},
{
type: 'text',
text: result.output || 'Build settings retrieved successfully.',
},
];
// Add next steps for workspace (similar to original workspace implementation)
if (!hasProjectPath && path) {
content.push({
type: 'text',
text: `Next Steps:
- Build the workspace: build_macos({ workspacePath: "${path}", scheme: "${params.scheme}" })
- For iOS: build_sim({ workspacePath: "${path}", scheme: "${params.scheme}", simulatorName: "iPhone 16" })
- List schemes: list_schemes({ workspacePath: "${path}" })`,
});
}
return {
content,
isError: false,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log('error', `Error showing build settings: ${errorMessage}`);
return createTextResponse(`Error showing build settings: ${errorMessage}`, true);
}
}
const publicSchemaObject = baseSchemaObject.omit({
projectPath: true,
workspacePath: true,
scheme: true,
} as const);
export default {
name: 'show_build_settings',
description: 'Shows xcodebuild build settings.',
schema: getSessionAwareToolSchemaShape({
sessionAware: publicSchemaObject,
legacy: baseSchemaObject,
}),
annotations: {
title: 'Show Build Settings',
readOnlyHint: true,
},
handler: createSessionAwareTool<ShowBuildSettingsParams>({
internalSchema: showBuildSettingsSchema as unknown as z.ZodType<
ShowBuildSettingsParams,
unknown
>,
logicFunction: showBuildSettingsLogic,
getExecutor: getDefaultCommandExecutor,
requirements: [
{ allOf: ['scheme'], message: 'scheme is required' },
{ oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
],
exclusivePairs: [['projectPath', 'workspacePath']],
}),
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator/__tests__/boot_sim.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for boot_sim plugin (session-aware version)
* Follows CLAUDE.md guidance: dependency injection, no vi-mocks, literal validation.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import * as z from 'zod';
import {
createMockCommandResponse,
createMockExecutor,
} from '../../../../test-utils/mock-executors.ts';
import { sessionStore } from '../../../../utils/session-store.ts';
import bootSim, { boot_simLogic } from '../boot_sim.ts';
describe('boot_sim tool', () => {
beforeEach(() => {
sessionStore.clear();
});
describe('Export Field Validation (Literal)', () => {
it('should have correct name', () => {
expect(bootSim.name).toBe('boot_sim');
});
it('should have concise description', () => {
expect(bootSim.description).toBe('Boots an iOS simulator.');
});
it('should expose empty public schema', () => {
const schema = z.object(bootSim.schema);
expect(schema.safeParse({}).success).toBe(true);
expect(Object.keys(bootSim.schema)).toHaveLength(0);
const withSimId = schema.safeParse({ simulatorId: 'abc' });
expect(withSimId.success).toBe(true);
expect('simulatorId' in (withSimId.data as Record<string, unknown>)).toBe(false);
});
});
describe('Handler Requirements', () => {
it('should require simulatorId when not provided', async () => {
const result = await bootSim.handler({});
expect(result.isError).toBe(true);
const message = result.content[0].text;
expect(message).toContain('Missing required session defaults');
expect(message).toContain('simulatorId is required');
expect(message).toContain('session-set-defaults');
});
});
describe('Logic Behavior (Literal Results)', () => {
it('should handle successful boot', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'Simulator booted successfully',
});
const result = await boot_simLogic({ simulatorId: 'test-uuid-123' }, mockExecutor);
expect(result).toEqual({
content: [
{
type: 'text',
text: `✅ Simulator booted successfully. To make it visible, use: open_sim()\n\nNext steps:\n1. Open the Simulator app (makes it visible): open_sim()\n2. Install an app: install_app_sim({ simulatorId: "test-uuid-123", appPath: "PATH_TO_YOUR_APP" })\n3. Launch an app: launch_app_sim({ simulatorId: "test-uuid-123", bundleId: "YOUR_APP_BUNDLE_ID" })`,
},
],
});
});
it('should handle command failure', async () => {
const mockExecutor = createMockExecutor({
success: false,
error: 'Simulator not found',
});
const result = await boot_simLogic({ simulatorId: 'invalid-uuid' }, mockExecutor);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Boot simulator operation failed: Simulator not found',
},
],
});
});
it('should handle exception with Error object', async () => {
const mockExecutor = async () => {
throw new Error('Connection failed');
};
const result = await boot_simLogic({ simulatorId: 'test-uuid-123' }, mockExecutor);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Boot simulator operation failed: Connection failed',
},
],
});
});
it('should handle exception with string error', async () => {
const mockExecutor = async () => {
throw 'String error';
};
const result = await boot_simLogic({ simulatorId: 'test-uuid-123' }, mockExecutor);
expect(result).toEqual({
content: [
{
type: 'text',
text: 'Boot simulator operation failed: String error',
},
],
});
});
it('should verify command generation with mock executor', async () => {
const calls: Array<{
command: string[];
description?: string;
allowStderr?: boolean;
opts?: { cwd?: string };
}> = [];
const mockExecutor = async (
command: string[],
description?: string,
allowStderr?: boolean,
opts?: { cwd?: string },
detached?: boolean,
) => {
calls.push({ command, description, allowStderr, opts });
void detached;
return createMockCommandResponse({
success: true,
output: 'Simulator booted successfully',
error: undefined,
});
};
await boot_simLogic({ simulatorId: 'test-uuid-123' }, mockExecutor);
expect(calls).toHaveLength(1);
expect(calls[0]).toEqual({
command: ['xcrun', 'simctl', 'boot', 'test-uuid-123'],
description: 'Boot Simulator',
allowStderr: true,
opts: undefined,
});
});
});
});
```