This is page 3 of 16. Use http://codebase.md/cameroncooke/xcodebuildmcp?lines=true&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
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "xcodebuildmcp",
3 | "version": "1.15.1",
4 | "mcpName": "com.xcodebuildmcp/XcodeBuildMCP",
5 | "iOSTemplateVersion": "v1.0.8",
6 | "macOSTemplateVersion": "v1.0.5",
7 | "type": "module",
8 | "module": "src/smithery.ts",
9 | "exports": {
10 | ".": "./build/index.js",
11 | "./package.json": "./package.json"
12 | },
13 | "bin": {
14 | "xcodebuildmcp": "build/index.js",
15 | "xcodebuildmcp-doctor": "build/doctor-cli.js"
16 | },
17 | "scripts": {
18 | "build": "npm run build:tsup && npx smithery build",
19 | "dev": "npm run generate:version && npm run generate:loaders && npx smithery dev",
20 | "build:tsup": "npm run generate:version && npm run generate:loaders && tsup",
21 | "dev:tsup": "npm run build:tsup && tsup --watch",
22 | "generate:version": "npx tsx scripts/generate-version.ts",
23 | "generate:loaders": "npx tsx scripts/generate-loaders.ts",
24 | "bundle:axe": "scripts/bundle-axe.sh",
25 | "lint": "eslint 'src/**/*.{js,ts}'",
26 | "lint:fix": "eslint 'src/**/*.{js,ts}' --fix",
27 | "format": "prettier --write 'src/**/*.{js,ts}'",
28 | "format:check": "prettier --check 'src/**/*.{js,ts}'",
29 | "typecheck": "npx tsc --noEmit && npx tsc -p tsconfig.test.json",
30 | "typecheck:tests": "npx tsc -p tsconfig.test.json",
31 | "verify:smithery-bundle": "bash scripts/verify-smithery-bundle.sh",
32 | "inspect": "npx @modelcontextprotocol/inspector node build/index.js",
33 | "doctor": "node build/doctor-cli.js",
34 | "tools": "npx tsx scripts/tools-cli.ts",
35 | "tools:list": "npx tsx scripts/tools-cli.ts list",
36 | "tools:static": "npx tsx scripts/tools-cli.ts static",
37 | "tools:count": "npx tsx scripts/tools-cli.ts count --static",
38 | "tools:analysis": "npx tsx scripts/analysis/tools-analysis.ts",
39 | "docs:update": "npx tsx scripts/update-tools-docs.ts",
40 | "docs:update:dry-run": "npx tsx scripts/update-tools-docs.ts --dry-run --verbose",
41 | "test": "vitest run",
42 | "test:watch": "vitest",
43 | "test:ui": "vitest --ui",
44 | "test:coverage": "vitest run --coverage"
45 | },
46 | "files": [
47 | "build",
48 | "bundled",
49 | "plugins"
50 | ],
51 | "keywords": [
52 | "xcodebuild",
53 | "mcp",
54 | "modelcontextprotocol",
55 | "xcode",
56 | "ios",
57 | "macos",
58 | "simulator"
59 | ],
60 | "author": "Cameron Cooke",
61 | "license": "MIT",
62 | "description": "XcodeBuildMCP is a ModelContextProtocol server that provides tools for Xcode project management, simulator management, and app utilities.",
63 | "repository": {
64 | "type": "git",
65 | "url": "git+https://github.com/cameroncooke/XcodeBuildMCP.git"
66 | },
67 | "homepage": "https://www.async-let.com/blog/xcodebuild-mcp/",
68 | "bugs": {
69 | "url": "https://github.com/cameroncooke/XcodeBuildMCP/issues"
70 | },
71 | "dependencies": {
72 | "@modelcontextprotocol/sdk": "^1.25.1",
73 | "@sentry/cli": "^2.43.1",
74 | "@sentry/node": "^10.5.0",
75 | "uuid": "^11.1.0",
76 | "zod": "^4.0.0"
77 | },
78 | "devDependencies": {
79 | "@bacons/xcode": "^1.0.0-alpha.24",
80 | "@eslint/eslintrc": "^3.3.1",
81 | "@eslint/js": "^9.23.0",
82 | "@smithery/cli": "^1.4.6",
83 | "@types/node": "^22.13.6",
84 | "@typescript-eslint/eslint-plugin": "^8.28.0",
85 | "@typescript-eslint/parser": "^8.28.0",
86 | "@vitest/coverage-v8": "^3.2.4",
87 | "@vitest/ui": "^3.2.4",
88 | "eslint": "^9.23.0",
89 | "eslint-config-prettier": "^10.1.1",
90 | "eslint-plugin-prettier": "^5.2.5",
91 | "playwright": "^1.53.0",
92 | "prettier": "3.6.2",
93 | "ts-node": "^10.9.2",
94 | "tsup": "^8.5.0",
95 | "tsx": "^4.20.4",
96 | "typescript": "^5.8.2",
97 | "typescript-eslint": "^8.28.0",
98 | "vitest": "^3.2.4",
99 | "xcode": "^3.0.1"
100 | }
101 | }
```
--------------------------------------------------------------------------------
/src/utils/xcode.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Xcode Utilities - Core infrastructure for interacting with Xcode tools
3 | *
4 | * This utility module provides the foundation for all Xcode interactions across the codebase.
5 | * It offers platform-specific utilities, and common functionality that can be used by any module
6 | * requiring Xcode tool integration.
7 | *
8 | * Responsibilities:
9 | * - Constructing platform-specific destination strings (constructDestinationString)
10 | *
11 | * This file serves as the foundation layer for more specialized utilities like build-utils.ts,
12 | * which build upon these core functions to provide higher-level abstractions.
13 | */
14 |
15 | import { log } from './logger.ts';
16 | import { XcodePlatform } from '../types/common.ts';
17 |
18 | // Re-export XcodePlatform for use in other modules
19 | export { XcodePlatform };
20 |
21 | /**
22 | * Constructs a destination string for xcodebuild from platform and simulator parameters
23 | * @param platform The target platform
24 | * @param simulatorName Optional simulator name
25 | * @param simulatorId Optional simulator UUID
26 | * @param useLatest Whether to use the latest simulator version (primarily for named simulators)
27 | * @param arch Optional architecture for macOS builds (arm64 or x86_64)
28 | * @returns Properly formatted destination string for xcodebuild
29 | */
30 | export function constructDestinationString(
31 | platform: XcodePlatform,
32 | simulatorName?: string,
33 | simulatorId?: string,
34 | useLatest: boolean = true,
35 | arch?: string,
36 | ): string {
37 | const isSimulatorPlatform = [
38 | XcodePlatform.iOSSimulator,
39 | XcodePlatform.watchOSSimulator,
40 | XcodePlatform.tvOSSimulator,
41 | XcodePlatform.visionOSSimulator,
42 | ].includes(platform);
43 |
44 | // If ID is provided for a simulator, it takes precedence and uniquely identifies it.
45 | if (isSimulatorPlatform && simulatorId) {
46 | return `platform=${platform},id=${simulatorId}`;
47 | }
48 |
49 | // If name is provided for a simulator
50 | if (isSimulatorPlatform && simulatorName) {
51 | return `platform=${platform},name=${simulatorName}${useLatest ? ',OS=latest' : ''}`;
52 | }
53 |
54 | // If it's a simulator platform but neither ID nor name is provided (should be prevented by callers now)
55 | if (isSimulatorPlatform && !simulatorId && !simulatorName) {
56 | // Throw error as specific simulator is needed unless it's a generic build action
57 | // Allow fallback for generic simulator builds if needed, but generally require specifics for build/run
58 | log(
59 | 'warning',
60 | `Constructing generic destination for ${platform} without name or ID. This might not be specific enough.`,
61 | );
62 | // Example: return `platform=${platform},name=Any ${platform} Device`; // Or similar generic target
63 | throw new Error(`Simulator name or ID is required for specific ${platform} operations`);
64 | }
65 |
66 | // Handle non-simulator platforms
67 | switch (platform) {
68 | case XcodePlatform.macOS:
69 | return arch ? `platform=macOS,arch=${arch}` : 'platform=macOS';
70 | case XcodePlatform.iOS:
71 | return 'generic/platform=iOS';
72 | case XcodePlatform.watchOS:
73 | return 'generic/platform=watchOS';
74 | case XcodePlatform.tvOS:
75 | return 'generic/platform=tvOS';
76 | case XcodePlatform.visionOS:
77 | return 'generic/platform=visionOS';
78 | // No default needed as enum covers all cases unless extended
79 | // default:
80 | // throw new Error(`Unsupported platform for destination string: ${platform}`);
81 | }
82 | // Fallback just in case (shouldn't be reached with enum)
83 | log('error', `Reached unexpected point in constructDestinationString for platform: ${platform}`);
84 | return `platform=${platform}`;
85 | }
86 |
```
--------------------------------------------------------------------------------
/src/mcp/resources/__tests__/doctor.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from 'vitest';
2 |
3 | import doctorResource, { doctorResourceLogic } from '../doctor.ts';
4 | import { createMockExecutor } from '../../../test-utils/mock-executors.ts';
5 |
6 | describe('doctor resource', () => {
7 | describe('Export Field Validation', () => {
8 | it('should export correct uri', () => {
9 | expect(doctorResource.uri).toBe('xcodebuildmcp://doctor');
10 | });
11 |
12 | it('should export correct description', () => {
13 | expect(doctorResource.description).toBe(
14 | 'Comprehensive development environment diagnostic information and configuration status',
15 | );
16 | });
17 |
18 | it('should export correct mimeType', () => {
19 | expect(doctorResource.mimeType).toBe('text/plain');
20 | });
21 |
22 | it('should export handler function', () => {
23 | expect(typeof doctorResource.handler).toBe('function');
24 | });
25 | });
26 |
27 | describe('Handler Functionality', () => {
28 | it('should handle successful environment data retrieval', async () => {
29 | const mockExecutor = createMockExecutor({
30 | success: true,
31 | output: 'Mock command output',
32 | });
33 |
34 | const result = await doctorResourceLogic(mockExecutor);
35 |
36 | expect(result.contents).toHaveLength(1);
37 | expect(result.contents[0].text).toContain('XcodeBuildMCP Doctor');
38 | expect(result.contents[0].text).toContain('## System Information');
39 | expect(result.contents[0].text).toContain('## Node.js Information');
40 | expect(result.contents[0].text).toContain('## Dependencies');
41 | expect(result.contents[0].text).toContain('## Environment Variables');
42 | expect(result.contents[0].text).toContain('## Feature Status');
43 | });
44 |
45 | it('should handle spawn errors by showing doctor info', async () => {
46 | const mockExecutor = createMockExecutor(new Error('spawn xcrun ENOENT'));
47 |
48 | const result = await doctorResourceLogic(mockExecutor);
49 |
50 | expect(result.contents).toHaveLength(1);
51 | expect(result.contents[0].text).toContain('XcodeBuildMCP Doctor');
52 | expect(result.contents[0].text).toContain('Error: spawn xcrun ENOENT');
53 | });
54 |
55 | it('should include required doctor sections', async () => {
56 | const mockExecutor = createMockExecutor({
57 | success: true,
58 | output: 'Mock output',
59 | });
60 |
61 | const result = await doctorResourceLogic(mockExecutor);
62 |
63 | expect(result.contents[0].text).toContain('## Troubleshooting Tips');
64 | expect(result.contents[0].text).toContain('brew tap cameroncooke/axe');
65 | expect(result.contents[0].text).toContain('INCREMENTAL_BUILDS_ENABLED=1');
66 | });
67 |
68 | it('should provide feature status information', async () => {
69 | const mockExecutor = createMockExecutor({
70 | success: true,
71 | output: 'Mock output',
72 | });
73 |
74 | const result = await doctorResourceLogic(mockExecutor);
75 |
76 | expect(result.contents[0].text).toContain('### UI Automation (axe)');
77 | expect(result.contents[0].text).toContain('### Incremental Builds');
78 | expect(result.contents[0].text).toContain('### Mise Integration');
79 | expect(result.contents[0].text).toContain('## Tool Availability Summary');
80 | });
81 |
82 | it('should handle error conditions gracefully', async () => {
83 | const mockExecutor = createMockExecutor({
84 | success: false,
85 | output: '',
86 | error: 'Command failed',
87 | });
88 |
89 | const result = await doctorResourceLogic(mockExecutor);
90 |
91 | expect(result.contents).toHaveLength(1);
92 | expect(result.contents[0].text).toContain('XcodeBuildMCP Doctor');
93 | });
94 | });
95 | });
96 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/swift-package/swift_package_test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import * as z from 'zod';
2 | import path from 'node:path';
3 | import type { CommandExecutor } from '../../../utils/execution/index.ts';
4 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
5 | import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.ts';
6 | import { log } from '../../../utils/logging/index.ts';
7 | import { ToolResponse } from '../../../types/common.ts';
8 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
9 |
10 | // Define schema as ZodObject
11 | const swiftPackageTestSchema = z.object({
12 | packagePath: z.string().describe('Path to the Swift package root (Required)'),
13 | testProduct: z.string().optional().describe('Optional specific test product to run'),
14 | filter: z.string().optional().describe('Filter tests by name (regex pattern)'),
15 | configuration: z
16 | .enum(['debug', 'release'])
17 | .optional()
18 | .describe('Swift package configuration (debug, release)'),
19 | parallel: z.boolean().optional().describe('Run tests in parallel (default: true)'),
20 | showCodecov: z.boolean().optional().describe('Show code coverage (default: false)'),
21 | parseAsLibrary: z
22 | .boolean()
23 | .optional()
24 | .describe('Add -parse-as-library flag for @main support (default: false)'),
25 | });
26 |
27 | // Use z.infer for type safety
28 | type SwiftPackageTestParams = z.infer<typeof swiftPackageTestSchema>;
29 |
30 | export async function swift_package_testLogic(
31 | params: SwiftPackageTestParams,
32 | executor: CommandExecutor,
33 | ): Promise<ToolResponse> {
34 | const resolvedPath = path.resolve(params.packagePath);
35 | const swiftArgs = ['test', '--package-path', resolvedPath];
36 |
37 | if (params.configuration && params.configuration.toLowerCase() === 'release') {
38 | swiftArgs.push('-c', 'release');
39 | } else if (params.configuration && params.configuration.toLowerCase() !== 'debug') {
40 | return createTextResponse("Invalid configuration. Use 'debug' or 'release'.", true);
41 | }
42 |
43 | if (params.testProduct) {
44 | swiftArgs.push('--test-product', params.testProduct);
45 | }
46 |
47 | if (params.filter) {
48 | swiftArgs.push('--filter', params.filter);
49 | }
50 |
51 | if (params.parallel === false) {
52 | swiftArgs.push('--no-parallel');
53 | }
54 |
55 | if (params.showCodecov) {
56 | swiftArgs.push('--show-code-coverage');
57 | }
58 |
59 | if (params.parseAsLibrary) {
60 | swiftArgs.push('-Xswiftc', '-parse-as-library');
61 | }
62 |
63 | log('info', `Running swift ${swiftArgs.join(' ')}`);
64 | try {
65 | const result = await executor(['swift', ...swiftArgs], 'Swift Package Test', true, undefined);
66 | if (!result.success) {
67 | const errorMessage = result.error ?? result.output ?? 'Unknown error';
68 | return createErrorResponse('Swift package tests failed', errorMessage);
69 | }
70 |
71 | return {
72 | content: [
73 | { type: 'text', text: '✅ Swift package tests completed.' },
74 | {
75 | type: 'text',
76 | text: '💡 Next: Execute your app with swift_package_run if tests passed',
77 | },
78 | { type: 'text', text: result.output },
79 | ],
80 | isError: false,
81 | };
82 | } catch (error) {
83 | const message = error instanceof Error ? error.message : String(error);
84 | log('error', `Swift package test failed: ${message}`);
85 | return createErrorResponse('Failed to execute swift test', message);
86 | }
87 | }
88 |
89 | export default {
90 | name: 'swift_package_test',
91 | description: 'Runs tests for a Swift Package with swift test',
92 | schema: swiftPackageTestSchema.shape, // MCP SDK compatibility
93 | annotations: {
94 | title: 'Swift Package Test',
95 | destructiveHint: true,
96 | },
97 | handler: createTypedTool(
98 | swiftPackageTestSchema,
99 | swift_package_testLogic,
100 | getDefaultCommandExecutor,
101 | ),
102 | };
103 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator-management/__tests__/reset_sim_location.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from 'vitest';
2 | import * as z from 'zod';
3 | import resetSimLocationPlugin, { reset_sim_locationLogic } from '../reset_sim_location.ts';
4 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
5 |
6 | describe('reset_sim_location plugin', () => {
7 | describe('Export Field Validation (Literal)', () => {
8 | it('should have correct name field', () => {
9 | expect(resetSimLocationPlugin.name).toBe('reset_sim_location');
10 | });
11 |
12 | it('should have correct description field', () => {
13 | expect(resetSimLocationPlugin.description).toBe(
14 | "Resets the simulator's location to default.",
15 | );
16 | });
17 |
18 | it('should have handler function', () => {
19 | expect(typeof resetSimLocationPlugin.handler).toBe('function');
20 | });
21 |
22 | it('should hide simulatorId from public schema', () => {
23 | const schema = z.object(resetSimLocationPlugin.schema);
24 |
25 | expect(schema.safeParse({}).success).toBe(true);
26 |
27 | const withSimId = schema.safeParse({ simulatorId: 'abc123' });
28 | expect(withSimId.success).toBe(true);
29 | expect('simulatorId' in (withSimId.data as any)).toBe(false);
30 | });
31 | });
32 |
33 | describe('Handler Behavior (Complete Literal Returns)', () => {
34 | it('should successfully reset simulator location', async () => {
35 | const mockExecutor = createMockExecutor({
36 | success: true,
37 | output: 'Location reset successfully',
38 | });
39 |
40 | const result = await reset_sim_locationLogic(
41 | {
42 | simulatorId: 'test-uuid-123',
43 | },
44 | mockExecutor,
45 | );
46 |
47 | expect(result).toEqual({
48 | content: [
49 | {
50 | type: 'text',
51 | text: 'Successfully reset simulator test-uuid-123 location.',
52 | },
53 | ],
54 | });
55 | });
56 |
57 | it('should handle command failure', async () => {
58 | const mockExecutor = createMockExecutor({
59 | success: false,
60 | error: 'Command failed',
61 | });
62 |
63 | const result = await reset_sim_locationLogic(
64 | {
65 | simulatorId: 'test-uuid-123',
66 | },
67 | mockExecutor,
68 | );
69 |
70 | expect(result).toEqual({
71 | content: [
72 | {
73 | type: 'text',
74 | text: 'Failed to reset simulator location: Command failed',
75 | },
76 | ],
77 | });
78 | });
79 |
80 | it('should handle exception during execution', async () => {
81 | const mockExecutor = createMockExecutor(new Error('Network error'));
82 |
83 | const result = await reset_sim_locationLogic(
84 | {
85 | simulatorId: 'test-uuid-123',
86 | },
87 | mockExecutor,
88 | );
89 |
90 | expect(result).toEqual({
91 | content: [
92 | {
93 | type: 'text',
94 | text: 'Failed to reset simulator location: Network error',
95 | },
96 | ],
97 | });
98 | });
99 |
100 | it('should call correct command', async () => {
101 | let capturedCommand: string[] = [];
102 | let capturedLogPrefix: string | undefined;
103 |
104 | const mockExecutor = createMockExecutor({
105 | success: true,
106 | output: 'Location reset successfully',
107 | });
108 |
109 | // Create a wrapper to capture the command arguments
110 | const capturingExecutor = async (command: string[], logPrefix?: string) => {
111 | capturedCommand = command;
112 | capturedLogPrefix = logPrefix;
113 | return mockExecutor(command, logPrefix);
114 | };
115 |
116 | await reset_sim_locationLogic(
117 | {
118 | simulatorId: 'test-uuid-123',
119 | },
120 | capturingExecutor,
121 | );
122 |
123 | expect(capturedCommand).toEqual(['xcrun', 'simctl', 'location', 'test-uuid-123', 'clear']);
124 | expect(capturedLogPrefix).toBe('Reset Simulator Location');
125 | });
126 | });
127 | });
128 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator-management/reset_sim_location.ts:
--------------------------------------------------------------------------------
```typescript
1 | import * as z from 'zod';
2 | import { ToolResponse } from '../../../types/common.ts';
3 | import { log } from '../../../utils/logging/index.ts';
4 | import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
5 | import {
6 | createSessionAwareTool,
7 | getSessionAwareToolSchemaShape,
8 | } from '../../../utils/typed-tool-factory.ts';
9 |
10 | // Define schema as ZodObject
11 | const resetSimulatorLocationSchema = z.object({
12 | simulatorId: z.uuid().describe('UUID of the simulator to use (obtained from list_simulators)'),
13 | });
14 |
15 | // Use z.infer for type safety
16 | type ResetSimulatorLocationParams = z.infer<typeof resetSimulatorLocationSchema>;
17 |
18 | // Helper function to execute simctl commands and handle responses
19 | async function executeSimctlCommandAndRespond(
20 | params: ResetSimulatorLocationParams,
21 | simctlSubCommand: string[],
22 | operationDescriptionForXcodeCommand: string,
23 | successMessage: string,
24 | failureMessagePrefix: string,
25 | operationLogContext: string,
26 | executor: CommandExecutor,
27 | extraValidation?: () => ToolResponse | undefined,
28 | ): Promise<ToolResponse> {
29 | if (extraValidation) {
30 | const validationResult = extraValidation();
31 | if (validationResult) {
32 | return validationResult;
33 | }
34 | }
35 |
36 | try {
37 | const command = ['xcrun', 'simctl', ...simctlSubCommand];
38 | const result = await executor(command, operationDescriptionForXcodeCommand, true, {});
39 |
40 | if (!result.success) {
41 | const fullFailureMessage = `${failureMessagePrefix}: ${result.error}`;
42 | log(
43 | 'error',
44 | `${fullFailureMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorId})`,
45 | );
46 | return {
47 | content: [{ type: 'text', text: fullFailureMessage }],
48 | };
49 | }
50 |
51 | log(
52 | 'info',
53 | `${successMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorId})`,
54 | );
55 | return {
56 | content: [{ type: 'text', text: successMessage }],
57 | };
58 | } catch (error) {
59 | const errorMessage = error instanceof Error ? error.message : String(error);
60 | const fullFailureMessage = `${failureMessagePrefix}: ${errorMessage}`;
61 | log(
62 | 'error',
63 | `Error during ${operationLogContext} for simulator ${params.simulatorId}: ${errorMessage}`,
64 | );
65 | return {
66 | content: [{ type: 'text', text: fullFailureMessage }],
67 | };
68 | }
69 | }
70 |
71 | export async function reset_sim_locationLogic(
72 | params: ResetSimulatorLocationParams,
73 | executor: CommandExecutor,
74 | ): Promise<ToolResponse> {
75 | log('info', `Resetting simulator ${params.simulatorId} location`);
76 |
77 | return executeSimctlCommandAndRespond(
78 | params,
79 | ['location', params.simulatorId, 'clear'],
80 | 'Reset Simulator Location',
81 | `Successfully reset simulator ${params.simulatorId} location.`,
82 | 'Failed to reset simulator location',
83 | 'reset simulator location',
84 | executor,
85 | );
86 | }
87 |
88 | const publicSchemaObject = z.strictObject(
89 | resetSimulatorLocationSchema.omit({ simulatorId: true } as const).shape,
90 | );
91 |
92 | export default {
93 | name: 'reset_sim_location',
94 | description: "Resets the simulator's location to default.",
95 | schema: getSessionAwareToolSchemaShape({
96 | sessionAware: publicSchemaObject,
97 | legacy: resetSimulatorLocationSchema,
98 | }),
99 | annotations: {
100 | title: 'Reset Simulator Location',
101 | destructiveHint: true,
102 | },
103 | handler: createSessionAwareTool<ResetSimulatorLocationParams>({
104 | internalSchema: resetSimulatorLocationSchema as unknown as z.ZodType<
105 | ResetSimulatorLocationParams,
106 | unknown
107 | >,
108 | logicFunction: reset_sim_locationLogic,
109 | getExecutor: getDefaultCommandExecutor,
110 | requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
111 | }),
112 | };
113 |
```
--------------------------------------------------------------------------------
/src/utils/errors.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolResponse } from '../types/common.ts';
2 |
3 | /**
4 | * Error Utilities - Type-safe error hierarchy for the application
5 | *
6 | * This utility module defines a structured error hierarchy for the application,
7 | * providing specialized error types for different failure scenarios. Using these
8 | * typed errors enables more precise error handling, improves debugging, and
9 | * provides better error messages to users.
10 | *
11 | * Responsibilities:
12 | * - Providing a base error class (XcodeBuildMCPError) for all application errors
13 | * - Defining specialized error subtypes for different error categories:
14 | * - ValidationError: Parameter validation failures
15 | * - SystemError: Underlying system/OS issues
16 | * - ConfigurationError: Application configuration problems
17 | * - SimulatorError: iOS simulator-specific failures
18 | * - AxeError: axe-specific errors
19 | *
20 | * The structured hierarchy allows error consumers to handle errors with the
21 | * appropriate level of specificity using instanceof checks or catch clauses.
22 | */
23 |
24 | /**
25 | * Custom error types for XcodeBuildMCP
26 | */
27 |
28 | /**
29 | * Base error class for XcodeBuildMCP errors
30 | */
31 | export class XcodeBuildMCPError extends Error {
32 | constructor(message: string) {
33 | super(message);
34 | this.name = 'XcodeBuildMCPError';
35 | // This is necessary for proper inheritance in TypeScript
36 | Object.setPrototypeOf(this, XcodeBuildMCPError.prototype);
37 | }
38 | }
39 |
40 | /**
41 | * Error thrown when validation of parameters fails
42 | */
43 | export class ValidationError extends XcodeBuildMCPError {
44 | constructor(
45 | message: string,
46 | public paramName?: string,
47 | ) {
48 | super(message);
49 | this.name = 'ValidationError';
50 | Object.setPrototypeOf(this, ValidationError.prototype);
51 | }
52 | }
53 |
54 | /**
55 | * Error thrown for system-level errors (file access, permissions, etc.)
56 | */
57 | export class SystemError extends XcodeBuildMCPError {
58 | constructor(
59 | message: string,
60 | public originalError?: Error,
61 | ) {
62 | super(message);
63 | this.name = 'SystemError';
64 | Object.setPrototypeOf(this, SystemError.prototype);
65 | }
66 | }
67 |
68 | /**
69 | * Error thrown for configuration issues
70 | */
71 | export class ConfigurationError extends XcodeBuildMCPError {
72 | constructor(message: string) {
73 | super(message);
74 | this.name = 'ConfigurationError';
75 | Object.setPrototypeOf(this, ConfigurationError.prototype);
76 | }
77 | }
78 |
79 | /**
80 | * Error thrown for simulator-specific errors
81 | */
82 | export class SimulatorError extends XcodeBuildMCPError {
83 | constructor(
84 | message: string,
85 | public simulatorName?: string,
86 | public simulatorId?: string,
87 | ) {
88 | super(message);
89 | this.name = 'SimulatorError';
90 | Object.setPrototypeOf(this, SimulatorError.prototype);
91 | }
92 | }
93 |
94 | /**
95 | * Error thrown for axe-specific errors
96 | */
97 | export class AxeError extends XcodeBuildMCPError {
98 | constructor(
99 | message: string,
100 | public command?: string, // The axe command that failed
101 | public axeOutput?: string, // Output from axe
102 | public simulatorId?: string,
103 | ) {
104 | super(message);
105 | this.name = 'AxeError';
106 | Object.setPrototypeOf(this, AxeError.prototype);
107 | }
108 | }
109 |
110 | // Helper to create a standard error response
111 | export function createErrorResponse(message: string, details?: string): ToolResponse {
112 | const detailText = details ? `\nDetails: ${details}` : '';
113 | return {
114 | content: [
115 | {
116 | type: 'text',
117 | text: `Error: ${message}${detailText}`,
118 | },
119 | ],
120 | isError: true,
121 | };
122 | }
123 |
124 | /**
125 | * Error class for missing dependencies
126 | */
127 | export class DependencyError extends ConfigurationError {
128 | constructor(
129 | message: string,
130 | public details?: string,
131 | ) {
132 | super(message);
133 | this.name = 'DependencyError';
134 | Object.setPrototypeOf(this, DependencyError.prototype);
135 | }
136 | }
137 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator/install_app_sim.ts:
--------------------------------------------------------------------------------
```typescript
1 | import * as z from 'zod';
2 | import { ToolResponse } from '../../../types/common.ts';
3 | import { log } from '../../../utils/logging/index.ts';
4 | import { validateFileExists } from '../../../utils/validation/index.ts';
5 | import type { CommandExecutor, FileSystemExecutor } from '../../../utils/execution/index.ts';
6 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
7 | import {
8 | createSessionAwareTool,
9 | getSessionAwareToolSchemaShape,
10 | } from '../../../utils/typed-tool-factory.ts';
11 |
12 | const installAppSimSchemaObject = z.object({
13 | simulatorId: z.string().describe('UUID of the simulator to use (obtained from list_sims)'),
14 | appPath: z
15 | .string()
16 | .describe('Path to the .app bundle to install (full path to the .app directory)'),
17 | });
18 |
19 | type InstallAppSimParams = z.infer<typeof installAppSimSchemaObject>;
20 |
21 | const publicSchemaObject = z.strictObject(
22 | installAppSimSchemaObject.omit({
23 | simulatorId: true,
24 | } as const).shape,
25 | );
26 |
27 | export async function install_app_simLogic(
28 | params: InstallAppSimParams,
29 | executor: CommandExecutor,
30 | fileSystem?: FileSystemExecutor,
31 | ): Promise<ToolResponse> {
32 | const appPathExistsValidation = validateFileExists(params.appPath, fileSystem);
33 | if (!appPathExistsValidation.isValid) {
34 | return appPathExistsValidation.errorResponse!;
35 | }
36 |
37 | log('info', `Starting xcrun simctl install request for simulator ${params.simulatorId}`);
38 |
39 | try {
40 | const command = ['xcrun', 'simctl', 'install', params.simulatorId, params.appPath];
41 | const result = await executor(command, 'Install App in Simulator', true, undefined);
42 |
43 | if (!result.success) {
44 | return {
45 | content: [
46 | {
47 | type: 'text',
48 | text: `Install app in simulator operation failed: ${result.error}`,
49 | },
50 | ],
51 | };
52 | }
53 |
54 | let bundleId = '';
55 | try {
56 | const bundleIdResult = await executor(
57 | ['defaults', 'read', `${params.appPath}/Info`, 'CFBundleIdentifier'],
58 | 'Extract Bundle ID',
59 | false,
60 | undefined,
61 | );
62 | if (bundleIdResult.success) {
63 | bundleId = bundleIdResult.output.trim();
64 | }
65 | } catch (error) {
66 | log('warning', `Could not extract bundle ID from app: ${error}`);
67 | }
68 |
69 | return {
70 | content: [
71 | {
72 | type: 'text',
73 | text: `App installed successfully in simulator ${params.simulatorId}`,
74 | },
75 | {
76 | type: 'text',
77 | text: `Next Steps:
78 | 1. Open the Simulator app: open_sim({})
79 | 2. Launch the app: launch_app_sim({ simulatorId: "${params.simulatorId}"${
80 | bundleId ? `, bundleId: "${bundleId}"` : ', bundleId: "YOUR_APP_BUNDLE_ID"'
81 | } })`,
82 | },
83 | ],
84 | };
85 | } catch (error) {
86 | const errorMessage = error instanceof Error ? error.message : String(error);
87 | log('error', `Error during install app in simulator operation: ${errorMessage}`);
88 | return {
89 | content: [
90 | {
91 | type: 'text',
92 | text: `Install app in simulator operation failed: ${errorMessage}`,
93 | },
94 | ],
95 | };
96 | }
97 | }
98 |
99 | export default {
100 | name: 'install_app_sim',
101 | description: 'Installs an app in an iOS simulator.',
102 | schema: getSessionAwareToolSchemaShape({
103 | sessionAware: publicSchemaObject,
104 | legacy: installAppSimSchemaObject,
105 | }),
106 | annotations: {
107 | title: 'Install App Simulator',
108 | destructiveHint: true,
109 | },
110 | handler: createSessionAwareTool<InstallAppSimParams>({
111 | internalSchema: installAppSimSchemaObject as unknown as z.ZodType<InstallAppSimParams, unknown>,
112 | logicFunction: install_app_simLogic,
113 | getExecutor: getDefaultCommandExecutor,
114 | requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
115 | }),
116 | };
117 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator-management/sim_statusbar.ts:
--------------------------------------------------------------------------------
```typescript
1 | import * as z from 'zod';
2 | import { ToolResponse } from '../../../types/common.ts';
3 | import { log } from '../../../utils/logging/index.ts';
4 | import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
5 | import {
6 | createSessionAwareTool,
7 | getSessionAwareToolSchemaShape,
8 | } from '../../../utils/typed-tool-factory.ts';
9 |
10 | // Define schema as ZodObject
11 | const simStatusbarSchema = z.object({
12 | simulatorId: z.uuid().describe('UUID of the simulator to use (obtained from list_simulators)'),
13 | dataNetwork: z
14 | .enum([
15 | 'clear',
16 | 'hide',
17 | 'wifi',
18 | '3g',
19 | '4g',
20 | 'lte',
21 | 'lte-a',
22 | 'lte+',
23 | '5g',
24 | '5g+',
25 | '5g-uwb',
26 | '5g-uc',
27 | ])
28 | .describe(
29 | '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.',
30 | ),
31 | });
32 |
33 | // Use z.infer for type safety
34 | type SimStatusbarParams = z.infer<typeof simStatusbarSchema>;
35 |
36 | export async function sim_statusbarLogic(
37 | params: SimStatusbarParams,
38 | executor: CommandExecutor,
39 | ): Promise<ToolResponse> {
40 | log(
41 | 'info',
42 | `Setting simulator ${params.simulatorId} status bar data network to ${params.dataNetwork}`,
43 | );
44 |
45 | try {
46 | let command: string[];
47 | let successMessage: string;
48 |
49 | if (params.dataNetwork === 'clear') {
50 | command = ['xcrun', 'simctl', 'status_bar', params.simulatorId, 'clear'];
51 | successMessage = `Successfully cleared status bar overrides for simulator ${params.simulatorId}`;
52 | } else {
53 | command = [
54 | 'xcrun',
55 | 'simctl',
56 | 'status_bar',
57 | params.simulatorId,
58 | 'override',
59 | '--dataNetwork',
60 | params.dataNetwork,
61 | ];
62 | successMessage = `Successfully set simulator ${params.simulatorId} status bar data network to ${params.dataNetwork}`;
63 | }
64 |
65 | const result = await executor(command, 'Set Status Bar', true, undefined);
66 |
67 | if (!result.success) {
68 | const failureMessage = `Failed to set status bar: ${result.error}`;
69 | log('error', `${failureMessage} (simulator: ${params.simulatorId})`);
70 | return {
71 | content: [{ type: 'text', text: failureMessage }],
72 | isError: true,
73 | };
74 | }
75 |
76 | log('info', `${successMessage} (simulator: ${params.simulatorId})`);
77 | return {
78 | content: [{ type: 'text', text: successMessage }],
79 | };
80 | } catch (error) {
81 | const errorMessage = error instanceof Error ? error.message : String(error);
82 | const failureMessage = `Failed to set status bar: ${errorMessage}`;
83 | log('error', `Error setting status bar for simulator ${params.simulatorId}: ${errorMessage}`);
84 | return {
85 | content: [{ type: 'text', text: failureMessage }],
86 | isError: true,
87 | };
88 | }
89 | }
90 |
91 | const publicSchemaObject = z.strictObject(
92 | simStatusbarSchema.omit({ simulatorId: true } as const).shape,
93 | );
94 |
95 | export default {
96 | name: 'sim_statusbar',
97 | description:
98 | '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).',
99 | schema: getSessionAwareToolSchemaShape({
100 | sessionAware: publicSchemaObject,
101 | legacy: simStatusbarSchema,
102 | }), // MCP SDK compatibility
103 | annotations: {
104 | title: 'Simulator Statusbar',
105 | destructiveHint: true,
106 | },
107 | handler: createSessionAwareTool<SimStatusbarParams>({
108 | internalSchema: simStatusbarSchema as unknown as z.ZodType<SimStatusbarParams, unknown>,
109 | logicFunction: sim_statusbarLogic,
110 | getExecutor: getDefaultCommandExecutor,
111 | requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
112 | }),
113 | };
114 |
```
--------------------------------------------------------------------------------
/src/core/__tests__/resources.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, beforeEach } from 'vitest';
2 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3 |
4 | import { registerResources, getAvailableResources, loadResources } from '../resources.ts';
5 |
6 | describe('resources', () => {
7 | let mockServer: McpServer;
8 | let registeredResources: Array<{
9 | name: string;
10 | uri: string;
11 | metadata: { mimeType: string; title: string };
12 | handler: any;
13 | }>;
14 |
15 | beforeEach(() => {
16 | registeredResources = [];
17 | // Create a mock MCP server using simple object structure
18 | mockServer = {
19 | resource: (
20 | name: string,
21 | uri: string,
22 | metadata: { mimeType: string; title: string },
23 | handler: any,
24 | ) => {
25 | registeredResources.push({ name, uri, metadata, handler });
26 | },
27 | } as unknown as McpServer;
28 | });
29 |
30 | describe('Exports', () => {
31 | it('should export registerResources function', () => {
32 | expect(typeof registerResources).toBe('function');
33 | });
34 |
35 | it('should export getAvailableResources function', () => {
36 | expect(typeof getAvailableResources).toBe('function');
37 | });
38 |
39 | it('should export loadResources function', () => {
40 | expect(typeof loadResources).toBe('function');
41 | });
42 | });
43 |
44 | describe('loadResources', () => {
45 | it('should load resources from generated loaders', async () => {
46 | const resources = await loadResources();
47 |
48 | // Should have at least the simulators resource
49 | expect(resources.size).toBeGreaterThan(0);
50 | expect(resources.has('xcodebuildmcp://simulators')).toBe(true);
51 | });
52 |
53 | it('should validate resource structure', async () => {
54 | const resources = await loadResources();
55 |
56 | for (const [uri, resource] of resources) {
57 | expect(resource.uri).toBe(uri);
58 | expect(typeof resource.description).toBe('string');
59 | expect(typeof resource.mimeType).toBe('string');
60 | expect(typeof resource.handler).toBe('function');
61 | }
62 | });
63 | });
64 |
65 | describe('registerResources', () => {
66 | it('should register all loaded resources with the server and return true', async () => {
67 | const result = await registerResources(mockServer);
68 |
69 | expect(result).toBe(true);
70 |
71 | // Should have registered at least one resource
72 | expect(registeredResources.length).toBeGreaterThan(0);
73 |
74 | // Check simulators resource was registered
75 | const simulatorsResource = registeredResources.find(
76 | (r) => r.uri === 'xcodebuildmcp://simulators',
77 | );
78 | expect(typeof simulatorsResource?.handler).toBe('function');
79 | expect(simulatorsResource?.metadata.title).toBe(
80 | 'Available iOS simulators with their UUIDs and states',
81 | );
82 | expect(simulatorsResource?.metadata.mimeType).toBe('text/plain');
83 | expect(simulatorsResource?.name).toBe('simulators');
84 | });
85 |
86 | it('should register resources with correct handlers', async () => {
87 | const result = await registerResources(mockServer);
88 |
89 | expect(result).toBe(true);
90 |
91 | const simulatorsResource = registeredResources.find(
92 | (r) => r.uri === 'xcodebuildmcp://simulators',
93 | );
94 | expect(typeof simulatorsResource?.handler).toBe('function');
95 | });
96 | });
97 |
98 | describe('getAvailableResources', () => {
99 | it('should return array of available resource URIs', async () => {
100 | const resources = await getAvailableResources();
101 |
102 | expect(Array.isArray(resources)).toBe(true);
103 | expect(resources.length).toBeGreaterThan(0);
104 | expect(resources).toContain('xcodebuildmcp://simulators');
105 | });
106 |
107 | it('should return unique URIs', async () => {
108 | const resources = await getAvailableResources();
109 | const uniqueResources = [...new Set(resources)];
110 |
111 | expect(resources.length).toBe(uniqueResources.length);
112 | });
113 | });
114 | });
115 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator-management/set_sim_appearance.ts:
--------------------------------------------------------------------------------
```typescript
1 | import * as z from 'zod';
2 | import { ToolResponse } from '../../../types/common.ts';
3 | import { log } from '../../../utils/logging/index.ts';
4 | import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
5 | import {
6 | createSessionAwareTool,
7 | getSessionAwareToolSchemaShape,
8 | } from '../../../utils/typed-tool-factory.ts';
9 |
10 | // Define schema as ZodObject
11 | const setSimAppearanceSchema = z.object({
12 | simulatorId: z.uuid().describe('UUID of the simulator to use (obtained from list_simulators)'),
13 | mode: z.enum(['dark', 'light']).describe('The appearance mode to set (either "dark" or "light")'),
14 | });
15 |
16 | // Use z.infer for type safety
17 | type SetSimAppearanceParams = z.infer<typeof setSimAppearanceSchema>;
18 |
19 | // Helper function to execute simctl commands and handle responses
20 | async function executeSimctlCommandAndRespond(
21 | params: SetSimAppearanceParams,
22 | simctlSubCommand: string[],
23 | operationDescriptionForXcodeCommand: string,
24 | successMessage: string,
25 | failureMessagePrefix: string,
26 | operationLogContext: string,
27 | extraValidation?: () => ToolResponse | undefined,
28 | executor: CommandExecutor = getDefaultCommandExecutor(),
29 | ): Promise<ToolResponse> {
30 | if (extraValidation) {
31 | const validationResult = extraValidation();
32 | if (validationResult) {
33 | return validationResult;
34 | }
35 | }
36 |
37 | try {
38 | const command = ['xcrun', 'simctl', ...simctlSubCommand];
39 | const result = await executor(command, operationDescriptionForXcodeCommand, true, undefined);
40 |
41 | if (!result.success) {
42 | const fullFailureMessage = `${failureMessagePrefix}: ${result.error}`;
43 | log(
44 | 'error',
45 | `${fullFailureMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorId})`,
46 | );
47 | return {
48 | content: [{ type: 'text', text: fullFailureMessage }],
49 | };
50 | }
51 |
52 | log(
53 | 'info',
54 | `${successMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorId})`,
55 | );
56 | return {
57 | content: [{ type: 'text', text: successMessage }],
58 | };
59 | } catch (error) {
60 | const errorMessage = error instanceof Error ? error.message : String(error);
61 | const fullFailureMessage = `${failureMessagePrefix}: ${errorMessage}`;
62 | log(
63 | 'error',
64 | `Error during ${operationLogContext} for simulator ${params.simulatorId}: ${errorMessage}`,
65 | );
66 | return {
67 | content: [{ type: 'text', text: fullFailureMessage }],
68 | };
69 | }
70 | }
71 |
72 | export async function set_sim_appearanceLogic(
73 | params: SetSimAppearanceParams,
74 | executor: CommandExecutor,
75 | ): Promise<ToolResponse> {
76 | log('info', `Setting simulator ${params.simulatorId} appearance to ${params.mode} mode`);
77 |
78 | return executeSimctlCommandAndRespond(
79 | params,
80 | ['ui', params.simulatorId, 'appearance', params.mode],
81 | 'Set Simulator Appearance',
82 | `Successfully set simulator ${params.simulatorId} appearance to ${params.mode} mode`,
83 | 'Failed to set simulator appearance',
84 | 'set simulator appearance',
85 | undefined,
86 | executor,
87 | );
88 | }
89 |
90 | const publicSchemaObject = z.strictObject(
91 | setSimAppearanceSchema.omit({ simulatorId: true } as const).shape,
92 | );
93 |
94 | export default {
95 | name: 'set_sim_appearance',
96 | description: 'Sets the appearance mode (dark/light) of an iOS simulator.',
97 | schema: getSessionAwareToolSchemaShape({
98 | sessionAware: publicSchemaObject,
99 | legacy: setSimAppearanceSchema,
100 | }),
101 | annotations: {
102 | title: 'Set Simulator Appearance',
103 | destructiveHint: true,
104 | },
105 | handler: createSessionAwareTool<SetSimAppearanceParams>({
106 | internalSchema: setSimAppearanceSchema as unknown as z.ZodType<SetSimAppearanceParams, unknown>,
107 | logicFunction: set_sim_appearanceLogic,
108 | getExecutor: getDefaultCommandExecutor,
109 | requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
110 | }),
111 | };
112 |
```
--------------------------------------------------------------------------------
/example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/CalculatorButton.swift:
--------------------------------------------------------------------------------
```swift
1 | import SwiftUI
2 |
3 | // MARK: - Calculator Button Component
4 | struct CalculatorButton: View {
5 | let title: String
6 | let buttonType: CalculatorButtonType
7 | let isWideButton: Bool
8 | let action: () -> Void
9 |
10 | @State private var isPressed = false
11 |
12 | var body: some View {
13 | if buttonType == .hidden {
14 | // Empty space for layout
15 | Color.clear
16 | .frame(height: 80)
17 | } else {
18 | Button(action: {
19 | withAnimation(.easeInOut(duration: 0.1)) {
20 | isPressed = true
21 | }
22 | action()
23 |
24 | Task {
25 | try await Task.sleep(for: .seconds(0.1))
26 | await MainActor.run {
27 | withAnimation(.easeInOut(duration: 0.1)) {
28 | isPressed = false
29 | }
30 | }
31 | }
32 | }) {
33 | ZStack {
34 | // Frosted glass background
35 | RoundedRectangle(cornerRadius: 20)
36 | .fill(.ultraThinMaterial)
37 | .overlay(
38 | RoundedRectangle(cornerRadius: 20)
39 | .stroke(buttonType.borderColor, lineWidth: 1)
40 | )
41 | .overlay(
42 | // Subtle inner glow
43 | RoundedRectangle(cornerRadius: 20)
44 | .fill(
45 | RadialGradient(
46 | colors: [buttonType.glowColor.opacity(0.3), Color.clear],
47 | center: .topLeading,
48 | startRadius: 0,
49 | endRadius: 50
50 | )
51 | )
52 | )
53 | .scaleEffect(isPressed ? 0.95 : 1.0)
54 | .shadow(color: buttonType.shadowColor.opacity(0.3), radius: isPressed ? 2 : 8, x: 0, y: isPressed ? 1 : 4)
55 |
56 | // Button text
57 | Text(title)
58 | .font(.system(size: 32, weight: .medium, design: .rounded))
59 | .foregroundColor(buttonType.textColor)
60 | .scaleEffect(isPressed ? 0.9 : 1.0)
61 | }
62 | }
63 | .frame(height: 80)
64 | .gridCellColumns(isWideButton ? 2 : 1)
65 | .buttonStyle(PlainButtonStyle())
66 | }
67 | }
68 | }
69 |
70 | // MARK: - Button Type Configuration
71 | enum CalculatorButtonType {
72 | case number, operation, function, hidden
73 |
74 | var textColor: Color {
75 | switch self {
76 | case .number:
77 | return .white
78 | case .operation:
79 | return .white
80 | case .function:
81 | return .white
82 | case .hidden:
83 | return .clear
84 | }
85 | }
86 |
87 | var borderColor: Color {
88 | switch self {
89 | case .number:
90 | return .white.opacity(0.3)
91 | case .operation:
92 | return .orange.opacity(0.6)
93 | case .function:
94 | return .gray.opacity(0.5)
95 | case .hidden:
96 | return .clear
97 | }
98 | }
99 |
100 | var glowColor: Color {
101 | switch self {
102 | case .number:
103 | return .blue
104 | case .operation:
105 | return .orange
106 | case .function:
107 | return .gray
108 | case .hidden:
109 | return .clear
110 | }
111 | }
112 |
113 | var shadowColor: Color {
114 | switch self {
115 | case .number:
116 | return .blue
117 | case .operation:
118 | return .orange
119 | case .function:
120 | return .gray
121 | case .hidden:
122 | return .clear
123 | }
124 | }
125 | }
```
--------------------------------------------------------------------------------
/src/mcp/tools/project-discovery/get_mac_bundle_id.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Project Discovery Plugin: Get macOS Bundle ID
3 | *
4 | * Extracts the bundle identifier from a macOS app bundle (.app).
5 | */
6 |
7 | import * as z from 'zod';
8 | import { log } from '../../../utils/logging/index.ts';
9 | import { ToolResponse } from '../../../types/common.ts';
10 | import {
11 | CommandExecutor,
12 | getDefaultFileSystemExecutor,
13 | getDefaultCommandExecutor,
14 | } from '../../../utils/command.ts';
15 | import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
16 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
17 |
18 | /**
19 | * Sync wrapper for CommandExecutor to handle synchronous commands
20 | */
21 | async function executeSyncCommand(command: string, executor: CommandExecutor): Promise<string> {
22 | const result = await executor(['/bin/sh', '-c', command], 'macOS Bundle ID Extraction');
23 | if (!result.success) {
24 | throw new Error(result.error ?? 'Command failed');
25 | }
26 | return result.output || '';
27 | }
28 |
29 | // Define schema as ZodObject
30 | const getMacBundleIdSchema = z.object({
31 | appPath: z
32 | .string()
33 | .describe(
34 | 'Path to the macOS .app bundle to extract bundle ID from (full path to the .app directory)',
35 | ),
36 | });
37 |
38 | // Use z.infer for type safety
39 | type GetMacBundleIdParams = z.infer<typeof getMacBundleIdSchema>;
40 |
41 | /**
42 | * Business logic for extracting macOS bundle ID
43 | */
44 | export async function get_mac_bundle_idLogic(
45 | params: GetMacBundleIdParams,
46 | executor: CommandExecutor,
47 | fileSystemExecutor: FileSystemExecutor,
48 | ): Promise<ToolResponse> {
49 | const appPath = params.appPath;
50 |
51 | if (!fileSystemExecutor.existsSync(appPath)) {
52 | return {
53 | content: [
54 | {
55 | type: 'text',
56 | text: `File not found: '${appPath}'. Please check the path and try again.`,
57 | },
58 | ],
59 | isError: true,
60 | };
61 | }
62 |
63 | log('info', `Starting bundle ID extraction for macOS app: ${appPath}`);
64 |
65 | try {
66 | let bundleId;
67 |
68 | try {
69 | bundleId = await executeSyncCommand(
70 | `defaults read "${appPath}/Contents/Info" CFBundleIdentifier`,
71 | executor,
72 | );
73 | } catch {
74 | try {
75 | bundleId = await executeSyncCommand(
76 | `/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${appPath}/Contents/Info.plist"`,
77 | executor,
78 | );
79 | } catch (innerError) {
80 | throw new Error(
81 | `Could not extract bundle ID from Info.plist: ${innerError instanceof Error ? innerError.message : String(innerError)}`,
82 | );
83 | }
84 | }
85 |
86 | log('info', `Extracted macOS bundle ID: ${bundleId}`);
87 |
88 | return {
89 | content: [
90 | {
91 | type: 'text',
92 | text: `✅ Bundle ID: ${bundleId}`,
93 | },
94 | {
95 | type: 'text',
96 | text: `Next Steps:
97 | - Launch: launch_mac_app({ appPath: "${appPath}" })
98 | - Build again: build_macos({ scheme: "SCHEME_NAME" })`,
99 | },
100 | ],
101 | isError: false,
102 | };
103 | } catch (error) {
104 | const errorMessage = error instanceof Error ? error.message : String(error);
105 | log('error', `Error extracting macOS bundle ID: ${errorMessage}`);
106 |
107 | return {
108 | content: [
109 | {
110 | type: 'text',
111 | text: `Error extracting macOS bundle ID: ${errorMessage}`,
112 | },
113 | {
114 | type: 'text',
115 | text: `Make sure the path points to a valid macOS app bundle (.app directory).`,
116 | },
117 | ],
118 | isError: true,
119 | };
120 | }
121 | }
122 |
123 | export default {
124 | name: 'get_mac_bundle_id',
125 | description:
126 | "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.",
127 | schema: getMacBundleIdSchema.shape, // MCP SDK compatibility
128 | annotations: {
129 | title: 'Get Mac Bundle ID',
130 | readOnlyHint: true,
131 | },
132 | handler: createTypedTool(
133 | getMacBundleIdSchema,
134 | (params: GetMacBundleIdParams) =>
135 | get_mac_bundle_idLogic(params, getDefaultCommandExecutor(), getDefaultFileSystemExecutor()),
136 | getDefaultCommandExecutor,
137 | ),
138 | };
139 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/project-discovery/get_app_bundle_id.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Project Discovery Plugin: Get App Bundle ID
3 | *
4 | * Extracts the bundle identifier from an app bundle (.app) for any Apple platform
5 | * (iOS, iPadOS, watchOS, tvOS, visionOS).
6 | */
7 |
8 | import * as z from 'zod';
9 | import { log } from '../../../utils/logging/index.ts';
10 | import { ToolResponse } from '../../../types/common.ts';
11 | import {
12 | CommandExecutor,
13 | getDefaultFileSystemExecutor,
14 | getDefaultCommandExecutor,
15 | } from '../../../utils/command.ts';
16 | import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
17 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
18 |
19 | // Define schema as ZodObject
20 | const getAppBundleIdSchema = z.object({
21 | appPath: z
22 | .string()
23 | .describe(
24 | 'Path to the .app bundle to extract bundle ID from (full path to the .app directory)',
25 | ),
26 | });
27 |
28 | // Use z.infer for type safety
29 | type GetAppBundleIdParams = z.infer<typeof getAppBundleIdSchema>;
30 |
31 | /**
32 | * Sync wrapper for CommandExecutor to handle synchronous commands
33 | */
34 | async function executeSyncCommand(command: string, executor: CommandExecutor): Promise<string> {
35 | const result = await executor(['/bin/sh', '-c', command], 'Bundle ID Extraction');
36 | if (!result.success) {
37 | throw new Error(result.error ?? 'Command failed');
38 | }
39 | return result.output || '';
40 | }
41 |
42 | /**
43 | * Business logic for extracting bundle ID from app.
44 | * Separated for testing and reusability.
45 | */
46 | export async function get_app_bundle_idLogic(
47 | params: GetAppBundleIdParams,
48 | executor: CommandExecutor,
49 | fileSystemExecutor: FileSystemExecutor,
50 | ): Promise<ToolResponse> {
51 | // Zod validation is handled by createTypedTool, so params.appPath is guaranteed to be a string
52 | const appPath = params.appPath;
53 |
54 | if (!fileSystemExecutor.existsSync(appPath)) {
55 | return {
56 | content: [
57 | {
58 | type: 'text',
59 | text: `File not found: '${appPath}'. Please check the path and try again.`,
60 | },
61 | ],
62 | isError: true,
63 | };
64 | }
65 |
66 | log('info', `Starting bundle ID extraction for app: ${appPath}`);
67 |
68 | try {
69 | let bundleId;
70 |
71 | try {
72 | bundleId = await executeSyncCommand(
73 | `defaults read "${appPath}/Info" CFBundleIdentifier`,
74 | executor,
75 | );
76 | } catch {
77 | try {
78 | bundleId = await executeSyncCommand(
79 | `/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${appPath}/Info.plist"`,
80 | executor,
81 | );
82 | } catch (innerError) {
83 | throw new Error(
84 | `Could not extract bundle ID from Info.plist: ${innerError instanceof Error ? innerError.message : String(innerError)}`,
85 | );
86 | }
87 | }
88 |
89 | log('info', `Extracted app bundle ID: ${bundleId}`);
90 |
91 | return {
92 | content: [
93 | {
94 | type: 'text',
95 | text: `✅ Bundle ID: ${bundleId}`,
96 | },
97 | {
98 | type: 'text',
99 | text: `Next Steps:
100 | - Simulator: install_app_sim + launch_app_sim
101 | - Device: install_app_device + launch_app_device`,
102 | },
103 | ],
104 | isError: false,
105 | };
106 | } catch (error) {
107 | const errorMessage = error instanceof Error ? error.message : String(error);
108 | log('error', `Error extracting app bundle ID: ${errorMessage}`);
109 |
110 | return {
111 | content: [
112 | {
113 | type: 'text',
114 | text: `Error extracting app bundle ID: ${errorMessage}`,
115 | },
116 | {
117 | type: 'text',
118 | text: `Make sure the path points to a valid app bundle (.app directory).`,
119 | },
120 | ],
121 | isError: true,
122 | };
123 | }
124 | }
125 |
126 | export default {
127 | name: 'get_app_bundle_id',
128 | description:
129 | "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' })",
130 | schema: getAppBundleIdSchema.shape, // MCP SDK compatibility
131 | annotations: {
132 | title: 'Get App Bundle ID',
133 | readOnlyHint: true,
134 | },
135 | handler: createTypedTool(
136 | getAppBundleIdSchema,
137 | (params: GetAppBundleIdParams) =>
138 | get_app_bundle_idLogic(params, getDefaultCommandExecutor(), getDefaultFileSystemExecutor()),
139 | getDefaultCommandExecutor,
140 | ),
141 | };
142 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/macos/build_macos.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * macOS Shared Plugin: Build macOS (Unified)
3 | *
4 | * Builds a macOS app using xcodebuild from a project or workspace.
5 | * Accepts mutually exclusive `projectPath` or `workspacePath`.
6 | */
7 |
8 | import * as z from 'zod';
9 | import { log } from '../../../utils/logging/index.ts';
10 | import { executeXcodeBuildCommand } from '../../../utils/build/index.ts';
11 | import { ToolResponse, XcodePlatform } from '../../../types/common.ts';
12 | import type { CommandExecutor } from '../../../utils/execution/index.ts';
13 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
14 | import {
15 | createSessionAwareTool,
16 | getSessionAwareToolSchemaShape,
17 | } from '../../../utils/typed-tool-factory.ts';
18 | import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
19 |
20 | // Types for dependency injection
21 | export interface BuildUtilsDependencies {
22 | executeXcodeBuildCommand: typeof executeXcodeBuildCommand;
23 | }
24 |
25 | // Default implementations
26 | const defaultBuildUtilsDependencies: BuildUtilsDependencies = {
27 | executeXcodeBuildCommand,
28 | };
29 |
30 | // Unified schema: XOR between projectPath and workspacePath
31 | const baseSchemaObject = z.object({
32 | projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
33 | workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
34 | scheme: z.string().describe('The scheme to use'),
35 | configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
36 | derivedDataPath: z
37 | .string()
38 | .optional()
39 | .describe('Path where build products and other derived data will go'),
40 | arch: z
41 | .enum(['arm64', 'x86_64'])
42 | .optional()
43 | .describe('Architecture to build for (arm64 or x86_64). For macOS only.'),
44 | extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'),
45 | preferXcodebuild: z
46 | .boolean()
47 | .optional()
48 | .describe('If true, prefers xcodebuild over the experimental incremental build system'),
49 | });
50 |
51 | const publicSchemaObject = baseSchemaObject.omit({
52 | projectPath: true,
53 | workspacePath: true,
54 | scheme: true,
55 | configuration: true,
56 | arch: true,
57 | } as const);
58 |
59 | const buildMacOSSchema = z.preprocess(
60 | nullifyEmptyStrings,
61 | baseSchemaObject
62 | .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
63 | message: 'Either projectPath or workspacePath is required.',
64 | })
65 | .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
66 | message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
67 | }),
68 | );
69 |
70 | export type BuildMacOSParams = z.infer<typeof buildMacOSSchema>;
71 |
72 | /**
73 | * Business logic for building macOS apps from project or workspace with dependency injection.
74 | * Exported for direct testing and reuse.
75 | */
76 | export async function buildMacOSLogic(
77 | params: BuildMacOSParams,
78 | executor: CommandExecutor,
79 | buildUtilsDeps: BuildUtilsDependencies = defaultBuildUtilsDependencies,
80 | ): Promise<ToolResponse> {
81 | log('info', `Starting macOS build for scheme ${params.scheme} (internal)`);
82 |
83 | const processedParams = {
84 | ...params,
85 | configuration: params.configuration ?? 'Debug',
86 | preferXcodebuild: params.preferXcodebuild ?? false,
87 | };
88 |
89 | return buildUtilsDeps.executeXcodeBuildCommand(
90 | processedParams,
91 | {
92 | platform: XcodePlatform.macOS,
93 | arch: params.arch,
94 | logPrefix: 'macOS Build',
95 | },
96 | processedParams.preferXcodebuild ?? false,
97 | 'build',
98 | executor,
99 | );
100 | }
101 |
102 | export default {
103 | name: 'build_macos',
104 | description: 'Builds a macOS app.',
105 | schema: getSessionAwareToolSchemaShape({
106 | sessionAware: publicSchemaObject,
107 | legacy: baseSchemaObject,
108 | }),
109 | annotations: {
110 | title: 'Build macOS',
111 | destructiveHint: true,
112 | },
113 | handler: createSessionAwareTool<BuildMacOSParams>({
114 | internalSchema: buildMacOSSchema as unknown as z.ZodType<BuildMacOSParams, unknown>,
115 | logicFunction: buildMacOSLogic,
116 | getExecutor: getDefaultCommandExecutor,
117 | requirements: [
118 | { allOf: ['scheme'], message: 'scheme is required' },
119 | { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
120 | ],
121 | exclusivePairs: [['projectPath', 'workspacePath']],
122 | }),
123 | };
124 |
```
--------------------------------------------------------------------------------
/src/utils/sentry.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Sentry instrumentation for XcodeBuildMCP
3 | *
4 | * This file initializes Sentry when explicitly called to avoid side effects
5 | * during module import (needed for Smithery's module-based entry).
6 | */
7 |
8 | import * as Sentry from '@sentry/node';
9 | import { execSync } from 'child_process';
10 | import { version } from '../version.ts';
11 |
12 | // Inlined system info functions to avoid circular dependencies
13 | function getXcodeInfo(): { version: string; path: string; selectedXcode: string; error?: string } {
14 | try {
15 | const xcodebuildOutput = execSync('xcodebuild -version', { encoding: 'utf8' }).trim();
16 | const version = xcodebuildOutput.split('\n').slice(0, 2).join(' - ');
17 | const path = execSync('xcode-select -p', { encoding: 'utf8' }).trim();
18 | const selectedXcode = execSync('xcrun --find xcodebuild', { encoding: 'utf8' }).trim();
19 |
20 | return { version, path, selectedXcode };
21 | } catch (error) {
22 | return {
23 | version: 'Not available',
24 | path: 'Not available',
25 | selectedXcode: 'Not available',
26 | error: error instanceof Error ? error.message : String(error),
27 | };
28 | }
29 | }
30 |
31 | function getEnvironmentVariables(): Record<string, string> {
32 | const relevantVars = [
33 | 'INCREMENTAL_BUILDS_ENABLED',
34 | 'PATH',
35 | 'DEVELOPER_DIR',
36 | 'HOME',
37 | 'USER',
38 | 'TMPDIR',
39 | 'NODE_ENV',
40 | 'SENTRY_DISABLED',
41 | ];
42 |
43 | const envVars: Record<string, string> = {};
44 | relevantVars.forEach((varName) => {
45 | envVars[varName] = process.env[varName] ?? '';
46 | });
47 |
48 | Object.keys(process.env).forEach((key) => {
49 | if (key.startsWith('XCODEBUILDMCP_')) {
50 | envVars[key] = process.env[key] ?? '';
51 | }
52 | });
53 |
54 | return envVars;
55 | }
56 |
57 | function checkBinaryAvailability(binary: string): { available: boolean; version?: string } {
58 | try {
59 | execSync(`which ${binary}`, { stdio: 'ignore' });
60 | } catch {
61 | return { available: false };
62 | }
63 |
64 | let version: string | undefined;
65 | const versionCommands: Record<string, string> = {
66 | axe: 'axe --version',
67 | mise: 'mise --version',
68 | };
69 |
70 | if (binary in versionCommands) {
71 | try {
72 | version = execSync(versionCommands[binary], {
73 | encoding: 'utf8',
74 | stdio: ['ignore', 'pipe', 'ignore'],
75 | }).trim();
76 | } catch {
77 | // Version command failed, but binary exists
78 | }
79 | }
80 |
81 | return { available: true, version };
82 | }
83 |
84 | let initialized = false;
85 |
86 | function isSentryDisabled(): boolean {
87 | return (
88 | process.env.SENTRY_DISABLED === 'true' || process.env.XCODEBUILDMCP_SENTRY_DISABLED === 'true'
89 | );
90 | }
91 |
92 | function isTestEnv(): boolean {
93 | return process.env.VITEST === 'true' || process.env.NODE_ENV === 'test';
94 | }
95 |
96 | export function initSentry(): void {
97 | if (initialized || isSentryDisabled() || isTestEnv()) {
98 | return;
99 | }
100 |
101 | initialized = true;
102 |
103 | Sentry.init({
104 | dsn:
105 | process.env.SENTRY_DSN ??
106 | 'https://798607831167c7b9fe2f2912f5d3c665@o4509258288332800.ingest.de.sentry.io/4509258293837904',
107 |
108 | // Setting this option to true will send default PII data to Sentry
109 | // For example, automatic IP address collection on events
110 | sendDefaultPii: true,
111 |
112 | // Set release version to match application version
113 | release: `xcodebuildmcp@${version}`,
114 |
115 | // Always report under production environment
116 | environment: 'production',
117 |
118 | // Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring
119 | // We recommend adjusting this value in production
120 | tracesSampleRate: 1.0,
121 | });
122 |
123 | const axeAvailable = checkBinaryAvailability('axe');
124 | const miseAvailable = checkBinaryAvailability('mise');
125 | const envVars = getEnvironmentVariables();
126 | const xcodeInfo = getXcodeInfo();
127 |
128 | // Add additional context that might be helpful for debugging
129 | const tags: Record<string, string> = {
130 | nodeVersion: process.version,
131 | platform: process.platform,
132 | arch: process.arch,
133 | axeAvailable: axeAvailable.available ? 'true' : 'false',
134 | axeVersion: axeAvailable.version ?? 'Unknown',
135 | miseAvailable: miseAvailable.available ? 'true' : 'false',
136 | miseVersion: miseAvailable.version ?? 'Unknown',
137 | ...Object.fromEntries(Object.entries(envVars).map(([k, v]) => [`env_${k}`, v ?? ''])),
138 | xcodeVersion: xcodeInfo.version ?? 'Unknown',
139 | xcodePath: xcodeInfo.path ?? 'Unknown',
140 | };
141 |
142 | Sentry.setTags(tags);
143 | }
144 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator-management/__tests__/set_sim_appearance.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from 'vitest';
2 | import * as z from 'zod';
3 | import setSimAppearancePlugin, { set_sim_appearanceLogic } from '../set_sim_appearance.ts';
4 | import {
5 | createMockCommandResponse,
6 | createMockExecutor,
7 | } from '../../../../test-utils/mock-executors.ts';
8 |
9 | describe('set_sim_appearance plugin', () => {
10 | describe('Export Field Validation (Literal)', () => {
11 | it('should have correct name field', () => {
12 | expect(setSimAppearancePlugin.name).toBe('set_sim_appearance');
13 | });
14 |
15 | it('should have correct description field', () => {
16 | expect(setSimAppearancePlugin.description).toBe(
17 | 'Sets the appearance mode (dark/light) of an iOS simulator.',
18 | );
19 | });
20 |
21 | it('should have handler function', () => {
22 | expect(typeof setSimAppearancePlugin.handler).toBe('function');
23 | });
24 |
25 | it('should expose public schema without simulatorId field', () => {
26 | const schema = z.object(setSimAppearancePlugin.schema);
27 |
28 | expect(schema.safeParse({ mode: 'dark' }).success).toBe(true);
29 | expect(schema.safeParse({ mode: 'light' }).success).toBe(true);
30 | expect(schema.safeParse({ mode: 'invalid' }).success).toBe(false);
31 |
32 | const withSimId = schema.safeParse({ simulatorId: 'abc123', mode: 'dark' });
33 | expect(withSimId.success).toBe(true);
34 | expect('simulatorId' in (withSimId.data as any)).toBe(false);
35 | });
36 | });
37 |
38 | describe('Handler Behavior (Complete Literal Returns)', () => {
39 | it('should handle successful appearance change', async () => {
40 | const mockExecutor = createMockExecutor({
41 | success: true,
42 | output: '',
43 | error: '',
44 | });
45 |
46 | const result = await set_sim_appearanceLogic(
47 | {
48 | simulatorId: 'test-uuid-123',
49 | mode: 'dark',
50 | },
51 | mockExecutor,
52 | );
53 |
54 | expect(result).toEqual({
55 | content: [
56 | {
57 | type: 'text',
58 | text: 'Successfully set simulator test-uuid-123 appearance to dark mode',
59 | },
60 | ],
61 | });
62 | });
63 |
64 | it('should handle appearance change failure', async () => {
65 | const mockExecutor = createMockExecutor({
66 | success: false,
67 | error: 'Invalid device: invalid-uuid',
68 | });
69 |
70 | const result = await set_sim_appearanceLogic(
71 | {
72 | simulatorId: 'invalid-uuid',
73 | mode: 'light',
74 | },
75 | mockExecutor,
76 | );
77 |
78 | expect(result).toEqual({
79 | content: [
80 | {
81 | type: 'text',
82 | text: 'Failed to set simulator appearance: Invalid device: invalid-uuid',
83 | },
84 | ],
85 | });
86 | });
87 |
88 | it('should surface session default requirement when simulatorId is missing', async () => {
89 | const result = await setSimAppearancePlugin.handler({ mode: 'dark' });
90 |
91 | const message = result.content?.[0]?.text ?? '';
92 | expect(message).toContain('Error: Missing required session defaults');
93 | expect(message).toContain('simulatorId is required');
94 | expect(result.isError).toBe(true);
95 | });
96 |
97 | it('should handle exception during execution', async () => {
98 | const mockExecutor = createMockExecutor(new Error('Network error'));
99 |
100 | const result = await set_sim_appearanceLogic(
101 | {
102 | simulatorId: 'test-uuid-123',
103 | mode: 'dark',
104 | },
105 | mockExecutor,
106 | );
107 |
108 | expect(result).toEqual({
109 | content: [
110 | {
111 | type: 'text',
112 | text: 'Failed to set simulator appearance: Network error',
113 | },
114 | ],
115 | });
116 | });
117 |
118 | it('should call correct command', async () => {
119 | const commandCalls: any[] = [];
120 | const mockExecutor = (...args: any[]) => {
121 | commandCalls.push(args);
122 | return Promise.resolve(
123 | createMockCommandResponse({
124 | success: true,
125 | output: '',
126 | error: '',
127 | }),
128 | );
129 | };
130 |
131 | await set_sim_appearanceLogic(
132 | {
133 | simulatorId: 'test-uuid-123',
134 | mode: 'dark',
135 | },
136 | mockExecutor,
137 | );
138 |
139 | expect(commandCalls).toEqual([
140 | [
141 | ['xcrun', 'simctl', 'ui', 'test-uuid-123', 'appearance', 'dark'],
142 | 'Set Simulator Appearance',
143 | true,
144 | undefined,
145 | ],
146 | ]);
147 | });
148 | });
149 | });
150 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator-management/set_sim_location.ts:
--------------------------------------------------------------------------------
```typescript
1 | import * as z from 'zod';
2 | import { ToolResponse } from '../../../types/common.ts';
3 | import { log } from '../../../utils/logging/index.ts';
4 | import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
5 | import {
6 | createSessionAwareTool,
7 | getSessionAwareToolSchemaShape,
8 | } from '../../../utils/typed-tool-factory.ts';
9 |
10 | // Define schema as ZodObject
11 | const setSimulatorLocationSchema = z.object({
12 | simulatorId: z.uuid().describe('UUID of the simulator to use (obtained from list_simulators)'),
13 | latitude: z.number().describe('The latitude for the custom location.'),
14 | longitude: z.number().describe('The longitude for the custom location.'),
15 | });
16 |
17 | // Use z.infer for type safety
18 | type SetSimulatorLocationParams = z.infer<typeof setSimulatorLocationSchema>;
19 |
20 | // Helper function to execute simctl commands and handle responses
21 | async function executeSimctlCommandAndRespond(
22 | params: SetSimulatorLocationParams,
23 | simctlSubCommand: string[],
24 | operationDescriptionForXcodeCommand: string,
25 | successMessage: string,
26 | failureMessagePrefix: string,
27 | operationLogContext: string,
28 | executor: CommandExecutor = getDefaultCommandExecutor(),
29 | extraValidation?: () => ToolResponse | null,
30 | ): Promise<ToolResponse> {
31 | if (extraValidation) {
32 | const validationResult = extraValidation();
33 | if (validationResult) {
34 | return validationResult;
35 | }
36 | }
37 |
38 | try {
39 | const command = ['xcrun', 'simctl', ...simctlSubCommand];
40 | const result = await executor(command, operationDescriptionForXcodeCommand, true, {});
41 |
42 | if (!result.success) {
43 | const fullFailureMessage = `${failureMessagePrefix}: ${result.error}`;
44 | log(
45 | 'error',
46 | `${fullFailureMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorId})`,
47 | );
48 | return {
49 | content: [{ type: 'text', text: fullFailureMessage }],
50 | };
51 | }
52 |
53 | log(
54 | 'info',
55 | `${successMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorId})`,
56 | );
57 | return {
58 | content: [{ type: 'text', text: successMessage }],
59 | };
60 | } catch (error) {
61 | const errorMessage = error instanceof Error ? error.message : String(error);
62 | const fullFailureMessage = `${failureMessagePrefix}: ${errorMessage}`;
63 | log(
64 | 'error',
65 | `Error during ${operationLogContext} for simulator ${params.simulatorId}: ${errorMessage}`,
66 | );
67 | return {
68 | content: [{ type: 'text', text: fullFailureMessage }],
69 | };
70 | }
71 | }
72 |
73 | export async function set_sim_locationLogic(
74 | params: SetSimulatorLocationParams,
75 | executor: CommandExecutor,
76 | ): Promise<ToolResponse> {
77 | const extraValidation = (): ToolResponse | null => {
78 | if (params.latitude < -90 || params.latitude > 90) {
79 | return {
80 | content: [
81 | {
82 | type: 'text',
83 | text: 'Latitude must be between -90 and 90 degrees',
84 | },
85 | ],
86 | };
87 | }
88 | if (params.longitude < -180 || params.longitude > 180) {
89 | return {
90 | content: [
91 | {
92 | type: 'text',
93 | text: 'Longitude must be between -180 and 180 degrees',
94 | },
95 | ],
96 | };
97 | }
98 | return null;
99 | };
100 |
101 | log(
102 | 'info',
103 | `Setting simulator ${params.simulatorId} location to ${params.latitude},${params.longitude}`,
104 | );
105 |
106 | return executeSimctlCommandAndRespond(
107 | params,
108 | ['location', params.simulatorId, 'set', `${params.latitude},${params.longitude}`],
109 | 'Set Simulator Location',
110 | `Successfully set simulator ${params.simulatorId} location to ${params.latitude},${params.longitude}`,
111 | 'Failed to set simulator location',
112 | 'set simulator location',
113 | executor,
114 | extraValidation,
115 | );
116 | }
117 |
118 | const publicSchemaObject = z.strictObject(
119 | setSimulatorLocationSchema.omit({ simulatorId: true } as const).shape,
120 | );
121 |
122 | export default {
123 | name: 'set_sim_location',
124 | description: 'Sets a custom GPS location for the simulator.',
125 | schema: getSessionAwareToolSchemaShape({
126 | sessionAware: publicSchemaObject,
127 | legacy: setSimulatorLocationSchema,
128 | }),
129 | annotations: {
130 | title: 'Set Simulator Location',
131 | destructiveHint: true,
132 | },
133 | handler: createSessionAwareTool<SetSimulatorLocationParams>({
134 | internalSchema: setSimulatorLocationSchema as unknown as z.ZodType<
135 | SetSimulatorLocationParams,
136 | unknown
137 | >,
138 | logicFunction: set_sim_locationLogic,
139 | getExecutor: getDefaultCommandExecutor,
140 | requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
141 | }),
142 | };
143 |
```
--------------------------------------------------------------------------------
/src/utils/simulator-utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Simulator utility functions for name to UUID resolution
3 | */
4 |
5 | import type { CommandExecutor } from './execution/index.ts';
6 | import { ToolResponse } from '../types/common.ts';
7 | import { log } from './logging/index.ts';
8 | import { createErrorResponse } from './responses/index.ts';
9 |
10 | /**
11 | * UUID regex pattern to check if a string looks like a UUID
12 | */
13 | const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14 |
15 | /**
16 | * Determines the simulator UUID from either a UUID or name.
17 | *
18 | * Behavior:
19 | * - If simulatorUuid provided: return it directly
20 | * - Else if simulatorName looks like a UUID (regex): treat it as UUID and return it
21 | * - Else: resolve name → UUID via simctl and return the match (isAvailable === true)
22 | *
23 | * @param params Object containing optional simulatorUuid or simulatorName
24 | * @param executor Command executor for running simctl commands
25 | * @returns Object with uuid, optional warning, or error
26 | */
27 | export async function determineSimulatorUuid(
28 | params: { simulatorUuid?: string; simulatorId?: string; simulatorName?: string },
29 | executor: CommandExecutor,
30 | ): Promise<{ uuid?: string; warning?: string; error?: ToolResponse }> {
31 | const directUuid = params.simulatorUuid ?? params.simulatorId;
32 |
33 | // If UUID is provided directly, use it
34 | if (directUuid) {
35 | log('info', `Using provided simulator UUID: ${directUuid}`);
36 | return { uuid: directUuid };
37 | }
38 |
39 | // If name is provided, check if it's actually a UUID
40 | if (params.simulatorName) {
41 | // Check if the "name" is actually a UUID string
42 | if (UUID_REGEX.test(params.simulatorName)) {
43 | log(
44 | 'info',
45 | `Simulator name '${params.simulatorName}' appears to be a UUID, using it directly`,
46 | );
47 | return {
48 | uuid: params.simulatorName,
49 | warning: `The simulatorName '${params.simulatorName}' appears to be a UUID. Consider using simulatorUuid parameter instead.`,
50 | };
51 | }
52 |
53 | // Resolve name to UUID via simctl
54 | log('info', `Looking up simulator UUID for name: ${params.simulatorName}`);
55 |
56 | const listResult = await executor(
57 | ['xcrun', 'simctl', 'list', 'devices', 'available', '-j'],
58 | 'List available simulators',
59 | );
60 |
61 | if (!listResult.success) {
62 | return {
63 | error: createErrorResponse(
64 | 'Failed to list simulators',
65 | listResult.error ?? 'Unknown error',
66 | ),
67 | };
68 | }
69 |
70 | try {
71 | interface SimulatorDevice {
72 | udid: string;
73 | name: string;
74 | isAvailable: boolean;
75 | }
76 |
77 | interface DevicesData {
78 | devices: Record<string, SimulatorDevice[]>;
79 | }
80 |
81 | const devicesData = JSON.parse(listResult.output ?? '{}') as DevicesData;
82 |
83 | // Search through all runtime sections for the named device
84 | for (const runtime of Object.keys(devicesData.devices)) {
85 | const devices = devicesData.devices[runtime];
86 | if (!Array.isArray(devices)) continue;
87 |
88 | // Look for exact name match with isAvailable === true
89 | const device = devices.find(
90 | (d) => d.name === params.simulatorName && d.isAvailable === true,
91 | );
92 |
93 | if (device) {
94 | log('info', `Found simulator '${params.simulatorName}' with UUID: ${device.udid}`);
95 | return { uuid: device.udid };
96 | }
97 | }
98 |
99 | // If no available device found, check if device exists but is unavailable
100 | for (const runtime of Object.keys(devicesData.devices)) {
101 | const devices = devicesData.devices[runtime];
102 | if (!Array.isArray(devices)) continue;
103 |
104 | const unavailableDevice = devices.find(
105 | (d) => d.name === params.simulatorName && d.isAvailable === false,
106 | );
107 |
108 | if (unavailableDevice) {
109 | return {
110 | error: createErrorResponse(
111 | `Simulator '${params.simulatorName}' exists but is not available`,
112 | 'The simulator may need to be downloaded or is incompatible with the current Xcode version',
113 | ),
114 | };
115 | }
116 | }
117 |
118 | // Device not found at all
119 | return {
120 | error: createErrorResponse(
121 | `Simulator '${params.simulatorName}' not found`,
122 | 'Please check the simulator name or use "xcrun simctl list devices" to see available simulators',
123 | ),
124 | };
125 | } catch (parseError) {
126 | return {
127 | error: createErrorResponse(
128 | 'Failed to parse simulator list',
129 | parseError instanceof Error ? parseError.message : String(parseError),
130 | ),
131 | };
132 | }
133 | }
134 |
135 | // Neither UUID nor name provided
136 | return {
137 | error: createErrorResponse(
138 | 'No simulator identifier provided',
139 | 'Either simulatorUuid or simulatorName is required',
140 | ),
141 | };
142 | }
143 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator/__tests__/open_sim.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Tests for open_sim plugin
3 | * Following CLAUDE.md testing standards with literal validation
4 | * Using dependency injection for deterministic testing
5 | */
6 |
7 | import { describe, it, expect } from 'vitest';
8 | import * as z from 'zod';
9 | import {
10 | createMockCommandResponse,
11 | createMockExecutor,
12 | type CommandExecutor,
13 | } from '../../../../test-utils/mock-executors.ts';
14 | import openSim, { open_simLogic } from '../open_sim.ts';
15 |
16 | describe('open_sim tool', () => {
17 | describe('Export Field Validation (Literal)', () => {
18 | it('should have correct name field', () => {
19 | expect(openSim.name).toBe('open_sim');
20 | });
21 |
22 | it('should have correct description field', () => {
23 | expect(openSim.description).toBe('Opens the iOS Simulator app.');
24 | });
25 |
26 | it('should have handler function', () => {
27 | expect(typeof openSim.handler).toBe('function');
28 | });
29 |
30 | it('should have correct schema validation', () => {
31 | const schema = z.object(openSim.schema);
32 |
33 | // Schema is empty, so any object should pass
34 | expect(schema.safeParse({}).success).toBe(true);
35 |
36 | expect(
37 | schema.safeParse({
38 | anyProperty: 'value',
39 | }).success,
40 | ).toBe(true);
41 |
42 | // Empty schema should accept anything
43 | expect(
44 | schema.safeParse({
45 | enabled: true,
46 | }).success,
47 | ).toBe(true);
48 | });
49 | });
50 |
51 | describe('Handler Behavior (Complete Literal Returns)', () => {
52 | it('should return exact successful open simulator response', async () => {
53 | const mockExecutor = createMockExecutor({
54 | success: true,
55 | output: '',
56 | });
57 |
58 | const result = await open_simLogic({}, mockExecutor);
59 |
60 | expect(result).toEqual({
61 | content: [
62 | {
63 | type: 'text',
64 | text: 'Simulator app opened successfully',
65 | },
66 | {
67 | type: 'text',
68 | text: `Next Steps:
69 | 1. Boot a simulator if needed: boot_sim({ simulatorId: 'UUID_FROM_LIST_SIMULATORS' })
70 | 2. Launch your app and interact with it
71 | 3. Log capture options:
72 | - Option 1: Capture structured logs only (app continues running):
73 | start_sim_log_cap({ simulatorId: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID' })
74 | - Option 2: Capture both console and structured logs (app will restart):
75 | start_sim_log_cap({ simulatorId: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID', captureConsole: true })
76 | - Option 3: Launch app with logs in one step:
77 | launch_app_logs_sim({ simulatorId: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID' })`,
78 | },
79 | ],
80 | });
81 | });
82 |
83 | it('should return exact command failure response', async () => {
84 | const mockExecutor = createMockExecutor({
85 | success: false,
86 | error: 'Command failed',
87 | });
88 |
89 | const result = await open_simLogic({}, mockExecutor);
90 |
91 | expect(result).toEqual({
92 | content: [
93 | {
94 | type: 'text',
95 | text: 'Open simulator operation failed: Command failed',
96 | },
97 | ],
98 | });
99 | });
100 |
101 | it('should return exact exception handling response', async () => {
102 | const mockExecutor: CommandExecutor = async () => {
103 | throw new Error('Test error');
104 | };
105 |
106 | const result = await open_simLogic({}, mockExecutor);
107 |
108 | expect(result).toEqual({
109 | content: [
110 | {
111 | type: 'text',
112 | text: 'Open simulator operation failed: Test error',
113 | },
114 | ],
115 | });
116 | });
117 |
118 | it('should return exact string error handling response', async () => {
119 | const mockExecutor: CommandExecutor = async () => {
120 | throw 'String error';
121 | };
122 |
123 | const result = await open_simLogic({}, mockExecutor);
124 |
125 | expect(result).toEqual({
126 | content: [
127 | {
128 | type: 'text',
129 | text: 'Open simulator operation failed: String error',
130 | },
131 | ],
132 | });
133 | });
134 |
135 | it('should verify command generation with mock executor', async () => {
136 | const calls: Array<{
137 | command: string[];
138 | description?: string;
139 | hideOutput?: boolean;
140 | opts?: { cwd?: string };
141 | }> = [];
142 |
143 | const mockExecutor: CommandExecutor = async (
144 | command,
145 | description,
146 | hideOutput,
147 | opts,
148 | detached,
149 | ) => {
150 | calls.push({ command, description, hideOutput, opts });
151 | void detached;
152 | return createMockCommandResponse({
153 | success: true,
154 | output: '',
155 | error: undefined,
156 | });
157 | };
158 |
159 | await open_simLogic({}, mockExecutor);
160 |
161 | expect(calls).toHaveLength(1);
162 | expect(calls[0]).toEqual({
163 | command: ['open', '-a', 'Simulator'],
164 | description: 'Open Simulator',
165 | hideOutput: true,
166 | opts: undefined,
167 | });
168 | });
169 | });
170 | });
171 |
```
--------------------------------------------------------------------------------
/docs/dev/session-aware-migration-todo.md:
--------------------------------------------------------------------------------
```markdown
1 | # Session-Aware Migration TODO
2 |
3 | _Audit date: October 6, 2025_
4 |
5 | Reference: [session_management_plan.md](session_management_plan.md)
6 |
7 | ## Utilities
8 | - [x] `src/mcp/tools/utilities/clean.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
9 |
10 | ## Project Discovery
11 | - [x] `src/mcp/tools/project-discovery/list_schemes.ts` — session defaults: `projectPath`, `workspacePath`.
12 | - [x] `src/mcp/tools/project-discovery/show_build_settings.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`.
13 |
14 | ## Device Workflows
15 | - [x] `src/mcp/tools/device/build_device.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
16 | - [x] `src/mcp/tools/device/test_device.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `deviceId`, `configuration`.
17 | - [x] `src/mcp/tools/device/get_device_app_path.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
18 | - [x] `src/mcp/tools/device/install_app_device.ts` — session defaults: `deviceId`.
19 | - [x] `src/mcp/tools/device/launch_app_device.ts` — session defaults: `deviceId`.
20 | - [x] `src/mcp/tools/device/stop_app_device.ts` — session defaults: `deviceId`.
21 |
22 | ## Device Logging
23 | - [x] `src/mcp/tools/logging/start_device_log_cap.ts` — session defaults: `deviceId`.
24 |
25 | ## macOS Workflows
26 | - [x] `src/mcp/tools/macos/build_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`.
27 | - [x] `src/mcp/tools/macos/build_run_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`.
28 | - [x] `src/mcp/tools/macos/test_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
29 | - [x] `src/mcp/tools/macos/get_mac_app_path.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`.
30 |
31 | ## Simulator Build/Test/Path
32 | - [x] `src/mcp/tools/simulator/test_sim.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `simulatorId`, `simulatorName`, `configuration`, `useLatestOS`.
33 | - [x] `src/mcp/tools/simulator/get_sim_app_path.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `simulatorId`, `simulatorName`, `configuration`, `useLatestOS`, `arch`.
34 |
35 | ## Simulator Runtime Actions
36 | - [x] `src/mcp/tools/simulator/boot_sim.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
37 | - [x] `src/mcp/tools/simulator/install_app_sim.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
38 | - [x] `src/mcp/tools/simulator/launch_app_sim.ts` — session defaults: `simulatorId`, `simulatorName` (hydrate `simulatorUuid`).
39 | - [x] `src/mcp/tools/simulator/launch_app_logs_sim.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
40 | - [x] `src/mcp/tools/simulator/stop_app_sim.ts` — session defaults: `simulatorId`, `simulatorName` (hydrate `simulatorUuid`).
41 | - [x] `src/mcp/tools/simulator/record_sim_video.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
42 |
43 | ## Simulator Management
44 | - [x] `src/mcp/tools/simulator-management/erase_sims.ts` — session defaults: `simulatorId` (covers `simulatorUdid`).
45 | - [x] `src/mcp/tools/simulator-management/set_sim_location.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
46 | - [x] `src/mcp/tools/simulator-management/reset_sim_location.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
47 | - [x] `src/mcp/tools/simulator-management/set_sim_appearance.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
48 | - [x] `src/mcp/tools/simulator-management/sim_statusbar.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
49 |
50 | ## Simulator Logging
51 | - [x] `src/mcp/tools/logging/start_sim_log_cap.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
52 |
53 | ## AXe UI Testing Tools
54 | - [x] `src/mcp/tools/ui-testing/button.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
55 | - [x] `src/mcp/tools/ui-testing/describe_ui.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
56 | - [x] `src/mcp/tools/ui-testing/gesture.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
57 | - [x] `src/mcp/tools/ui-testing/key_press.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
58 | - [x] `src/mcp/tools/ui-testing/key_sequence.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
59 | - [x] `src/mcp/tools/ui-testing/long_press.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
60 | - [x] `src/mcp/tools/ui-testing/screenshot.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
61 | - [x] `src/mcp/tools/ui-testing/swipe.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
62 | - [x] `src/mcp/tools/ui-testing/tap.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
63 | - [x] `src/mcp/tools/ui-testing/touch.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
64 | - [x] `src/mcp/tools/ui-testing/type_text.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
65 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/project-discovery/show_build_settings.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Project Discovery Plugin: Show Build Settings (Unified)
3 | *
4 | * Shows build settings from either a project or workspace using xcodebuild.
5 | * Accepts mutually exclusive `projectPath` or `workspacePath`.
6 | */
7 |
8 | import * as z from 'zod';
9 | import { log } from '../../../utils/logging/index.ts';
10 | import type { CommandExecutor } from '../../../utils/execution/index.ts';
11 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
12 | import { createTextResponse } from '../../../utils/responses/index.ts';
13 | import { ToolResponse } from '../../../types/common.ts';
14 | import {
15 | createSessionAwareTool,
16 | getSessionAwareToolSchemaShape,
17 | } from '../../../utils/typed-tool-factory.ts';
18 | import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
19 |
20 | // Unified schema: XOR between projectPath and workspacePath
21 | const baseSchemaObject = z.object({
22 | projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
23 | workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
24 | scheme: z.string().describe('Scheme name to show build settings for (Required)'),
25 | });
26 |
27 | const showBuildSettingsSchema = z.preprocess(
28 | nullifyEmptyStrings,
29 | baseSchemaObject
30 | .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
31 | message: 'Either projectPath or workspacePath is required.',
32 | })
33 | .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
34 | message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
35 | }),
36 | );
37 |
38 | export type ShowBuildSettingsParams = z.infer<typeof showBuildSettingsSchema>;
39 |
40 | /**
41 | * Business logic for showing build settings from a project or workspace.
42 | * Exported for direct testing and reuse.
43 | */
44 | export async function showBuildSettingsLogic(
45 | params: ShowBuildSettingsParams,
46 | executor: CommandExecutor,
47 | ): Promise<ToolResponse> {
48 | log('info', `Showing build settings for scheme ${params.scheme}`);
49 |
50 | try {
51 | // Create the command array for xcodebuild
52 | const command = ['xcodebuild', '-showBuildSettings']; // -showBuildSettings as an option, not an action
53 |
54 | const hasProjectPath = typeof params.projectPath === 'string';
55 | const path = hasProjectPath ? params.projectPath : params.workspacePath;
56 |
57 | if (hasProjectPath) {
58 | command.push('-project', params.projectPath!);
59 | } else {
60 | command.push('-workspace', params.workspacePath!);
61 | }
62 |
63 | // Add the scheme
64 | command.push('-scheme', params.scheme);
65 |
66 | // Execute the command directly
67 | const result = await executor(command, 'Show Build Settings', true);
68 |
69 | if (!result.success) {
70 | return createTextResponse(`Failed to show build settings: ${result.error}`, true);
71 | }
72 |
73 | // Create response based on which type was used (similar to workspace version with next steps)
74 | const content: Array<{ type: 'text'; text: string }> = [
75 | {
76 | type: 'text',
77 | text: hasProjectPath
78 | ? `✅ Build settings for scheme ${params.scheme}:`
79 | : '✅ Build settings retrieved successfully',
80 | },
81 | {
82 | type: 'text',
83 | text: result.output || 'Build settings retrieved successfully.',
84 | },
85 | ];
86 |
87 | // Add next steps for workspace (similar to original workspace implementation)
88 | if (!hasProjectPath && path) {
89 | content.push({
90 | type: 'text',
91 | text: `Next Steps:
92 | - Build the workspace: build_macos({ workspacePath: "${path}", scheme: "${params.scheme}" })
93 | - For iOS: build_sim({ workspacePath: "${path}", scheme: "${params.scheme}", simulatorName: "iPhone 16" })
94 | - List schemes: list_schemes({ workspacePath: "${path}" })`,
95 | });
96 | }
97 |
98 | return {
99 | content,
100 | isError: false,
101 | };
102 | } catch (error) {
103 | const errorMessage = error instanceof Error ? error.message : String(error);
104 | log('error', `Error showing build settings: ${errorMessage}`);
105 | return createTextResponse(`Error showing build settings: ${errorMessage}`, true);
106 | }
107 | }
108 |
109 | const publicSchemaObject = baseSchemaObject.omit({
110 | projectPath: true,
111 | workspacePath: true,
112 | scheme: true,
113 | } as const);
114 |
115 | export default {
116 | name: 'show_build_settings',
117 | description: 'Shows xcodebuild build settings.',
118 | schema: getSessionAwareToolSchemaShape({
119 | sessionAware: publicSchemaObject,
120 | legacy: baseSchemaObject,
121 | }),
122 | annotations: {
123 | title: 'Show Build Settings',
124 | readOnlyHint: true,
125 | },
126 | handler: createSessionAwareTool<ShowBuildSettingsParams>({
127 | internalSchema: showBuildSettingsSchema as unknown as z.ZodType<
128 | ShowBuildSettingsParams,
129 | unknown
130 | >,
131 | logicFunction: showBuildSettingsLogic,
132 | getExecutor: getDefaultCommandExecutor,
133 | requirements: [
134 | { allOf: ['scheme'], message: 'scheme is required' },
135 | { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
136 | ],
137 | exclusivePairs: [['projectPath', 'workspacePath']],
138 | }),
139 | };
140 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator/__tests__/boot_sim.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Tests for boot_sim plugin (session-aware version)
3 | * Follows CLAUDE.md guidance: dependency injection, no vi-mocks, literal validation.
4 | */
5 |
6 | import { describe, it, expect, beforeEach } from 'vitest';
7 | import * as z from 'zod';
8 | import {
9 | createMockCommandResponse,
10 | createMockExecutor,
11 | } from '../../../../test-utils/mock-executors.ts';
12 | import { sessionStore } from '../../../../utils/session-store.ts';
13 | import bootSim, { boot_simLogic } from '../boot_sim.ts';
14 |
15 | describe('boot_sim tool', () => {
16 | beforeEach(() => {
17 | sessionStore.clear();
18 | });
19 |
20 | describe('Export Field Validation (Literal)', () => {
21 | it('should have correct name', () => {
22 | expect(bootSim.name).toBe('boot_sim');
23 | });
24 |
25 | it('should have concise description', () => {
26 | expect(bootSim.description).toBe('Boots an iOS simulator.');
27 | });
28 |
29 | it('should expose empty public schema', () => {
30 | const schema = z.object(bootSim.schema);
31 | expect(schema.safeParse({}).success).toBe(true);
32 | expect(Object.keys(bootSim.schema)).toHaveLength(0);
33 |
34 | const withSimId = schema.safeParse({ simulatorId: 'abc' });
35 | expect(withSimId.success).toBe(true);
36 | expect('simulatorId' in (withSimId.data as Record<string, unknown>)).toBe(false);
37 | });
38 | });
39 |
40 | describe('Handler Requirements', () => {
41 | it('should require simulatorId when not provided', async () => {
42 | const result = await bootSim.handler({});
43 |
44 | expect(result.isError).toBe(true);
45 | const message = result.content[0].text;
46 | expect(message).toContain('Missing required session defaults');
47 | expect(message).toContain('simulatorId is required');
48 | expect(message).toContain('session-set-defaults');
49 | });
50 | });
51 |
52 | describe('Logic Behavior (Literal Results)', () => {
53 | it('should handle successful boot', async () => {
54 | const mockExecutor = createMockExecutor({
55 | success: true,
56 | output: 'Simulator booted successfully',
57 | });
58 |
59 | const result = await boot_simLogic({ simulatorId: 'test-uuid-123' }, mockExecutor);
60 |
61 | expect(result).toEqual({
62 | content: [
63 | {
64 | type: 'text',
65 | 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" })`,
66 | },
67 | ],
68 | });
69 | });
70 |
71 | it('should handle command failure', async () => {
72 | const mockExecutor = createMockExecutor({
73 | success: false,
74 | error: 'Simulator not found',
75 | });
76 |
77 | const result = await boot_simLogic({ simulatorId: 'invalid-uuid' }, mockExecutor);
78 |
79 | expect(result).toEqual({
80 | content: [
81 | {
82 | type: 'text',
83 | text: 'Boot simulator operation failed: Simulator not found',
84 | },
85 | ],
86 | });
87 | });
88 |
89 | it('should handle exception with Error object', async () => {
90 | const mockExecutor = async () => {
91 | throw new Error('Connection failed');
92 | };
93 |
94 | const result = await boot_simLogic({ simulatorId: 'test-uuid-123' }, mockExecutor);
95 |
96 | expect(result).toEqual({
97 | content: [
98 | {
99 | type: 'text',
100 | text: 'Boot simulator operation failed: Connection failed',
101 | },
102 | ],
103 | });
104 | });
105 |
106 | it('should handle exception with string error', async () => {
107 | const mockExecutor = async () => {
108 | throw 'String error';
109 | };
110 |
111 | const result = await boot_simLogic({ simulatorId: 'test-uuid-123' }, mockExecutor);
112 |
113 | expect(result).toEqual({
114 | content: [
115 | {
116 | type: 'text',
117 | text: 'Boot simulator operation failed: String error',
118 | },
119 | ],
120 | });
121 | });
122 |
123 | it('should verify command generation with mock executor', async () => {
124 | const calls: Array<{
125 | command: string[];
126 | description?: string;
127 | allowStderr?: boolean;
128 | opts?: { cwd?: string };
129 | }> = [];
130 | const mockExecutor = async (
131 | command: string[],
132 | description?: string,
133 | allowStderr?: boolean,
134 | opts?: { cwd?: string },
135 | detached?: boolean,
136 | ) => {
137 | calls.push({ command, description, allowStderr, opts });
138 | void detached;
139 | return createMockCommandResponse({
140 | success: true,
141 | output: 'Simulator booted successfully',
142 | error: undefined,
143 | });
144 | };
145 |
146 | await boot_simLogic({ simulatorId: 'test-uuid-123' }, mockExecutor);
147 |
148 | expect(calls).toHaveLength(1);
149 | expect(calls[0]).toEqual({
150 | command: ['xcrun', 'simctl', 'boot', 'test-uuid-123'],
151 | description: 'Boot Simulator',
152 | allowStderr: true,
153 | opts: undefined,
154 | });
155 | });
156 | });
157 | });
158 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/device/launch_app_device.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Device Workspace Plugin: Launch App Device
3 | *
4 | * Launches an app on a physical Apple device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro).
5 | * Requires deviceId and bundleId.
6 | */
7 |
8 | import * as z from 'zod';
9 | import { ToolResponse } from '../../../types/common.ts';
10 | import { log } from '../../../utils/logging/index.ts';
11 | import type { CommandExecutor, FileSystemExecutor } from '../../../utils/execution/index.ts';
12 | import {
13 | getDefaultCommandExecutor,
14 | getDefaultFileSystemExecutor,
15 | } from '../../../utils/execution/index.ts';
16 | import {
17 | createSessionAwareTool,
18 | getSessionAwareToolSchemaShape,
19 | } from '../../../utils/typed-tool-factory.ts';
20 | import { join } from 'path';
21 |
22 | // Type for the launch JSON response
23 | type LaunchDataResponse = {
24 | result?: {
25 | process?: {
26 | processIdentifier?: number;
27 | };
28 | };
29 | };
30 |
31 | // Define schema as ZodObject
32 | const launchAppDeviceSchema = z.object({
33 | deviceId: z.string().describe('UDID of the device (obtained from list_devices)'),
34 | bundleId: z
35 | .string()
36 | .describe('Bundle identifier of the app to launch (e.g., "com.example.MyApp")'),
37 | });
38 |
39 | const publicSchemaObject = launchAppDeviceSchema.omit({ deviceId: true } as const);
40 |
41 | // Use z.infer for type safety
42 | type LaunchAppDeviceParams = z.infer<typeof launchAppDeviceSchema>;
43 |
44 | export async function launch_app_deviceLogic(
45 | params: LaunchAppDeviceParams,
46 | executor: CommandExecutor,
47 | fileSystem: FileSystemExecutor,
48 | ): Promise<ToolResponse> {
49 | const { deviceId, bundleId } = params;
50 |
51 | log('info', `Launching app ${bundleId} on device ${deviceId}`);
52 |
53 | try {
54 | // Use JSON output to capture process ID
55 | const tempJsonPath = join(fileSystem.tmpdir(), `launch-${Date.now()}.json`);
56 |
57 | const result = await executor(
58 | [
59 | 'xcrun',
60 | 'devicectl',
61 | 'device',
62 | 'process',
63 | 'launch',
64 | '--device',
65 | deviceId,
66 | '--json-output',
67 | tempJsonPath,
68 | '--terminate-existing',
69 | bundleId,
70 | ],
71 | 'Launch app on device',
72 | true, // useShell
73 | undefined, // env
74 | );
75 |
76 | if (!result.success) {
77 | return {
78 | content: [
79 | {
80 | type: 'text',
81 | text: `Failed to launch app: ${result.error}`,
82 | },
83 | ],
84 | isError: true,
85 | };
86 | }
87 |
88 | // Parse JSON to extract process ID
89 | let processId: number | undefined;
90 | try {
91 | const jsonContent = await fileSystem.readFile(tempJsonPath, 'utf8');
92 | const parsedData: unknown = JSON.parse(jsonContent);
93 |
94 | // Type guard to validate the parsed data structure
95 | if (
96 | parsedData &&
97 | typeof parsedData === 'object' &&
98 | 'result' in parsedData &&
99 | parsedData.result &&
100 | typeof parsedData.result === 'object' &&
101 | 'process' in parsedData.result &&
102 | parsedData.result.process &&
103 | typeof parsedData.result.process === 'object' &&
104 | 'processIdentifier' in parsedData.result.process &&
105 | typeof parsedData.result.process.processIdentifier === 'number'
106 | ) {
107 | const launchData = parsedData as LaunchDataResponse;
108 | processId = launchData.result?.process?.processIdentifier;
109 | }
110 |
111 | // Clean up temp file
112 | await fileSystem.rm(tempJsonPath, { force: true }).catch(() => {});
113 | } catch (error) {
114 | log('warn', `Failed to parse launch JSON output: ${error}`);
115 | }
116 |
117 | let responseText = `✅ App launched successfully\n\n${result.output}`;
118 |
119 | if (processId) {
120 | responseText += `\n\nProcess ID: ${processId}`;
121 | responseText += `\n\nNext Steps:`;
122 | responseText += `\n1. Interact with your app on the device`;
123 | responseText += `\n2. Stop the app: stop_app_device({ deviceId: "${deviceId}", processId: ${processId} })`;
124 | }
125 |
126 | return {
127 | content: [
128 | {
129 | type: 'text',
130 | text: responseText,
131 | },
132 | ],
133 | };
134 | } catch (error) {
135 | const errorMessage = error instanceof Error ? error.message : String(error);
136 | log('error', `Error launching app on device: ${errorMessage}`);
137 | return {
138 | content: [
139 | {
140 | type: 'text',
141 | text: `Failed to launch app on device: ${errorMessage}`,
142 | },
143 | ],
144 | isError: true,
145 | };
146 | }
147 | }
148 |
149 | export default {
150 | name: 'launch_app_device',
151 | description: 'Launches an app on a connected device.',
152 | schema: getSessionAwareToolSchemaShape({
153 | sessionAware: publicSchemaObject,
154 | legacy: launchAppDeviceSchema,
155 | }),
156 | annotations: {
157 | title: 'Launch App Device',
158 | destructiveHint: true,
159 | },
160 | handler: createSessionAwareTool<LaunchAppDeviceParams>({
161 | internalSchema: launchAppDeviceSchema as unknown as z.ZodType<LaunchAppDeviceParams>,
162 | logicFunction: (params, executor) =>
163 | launch_app_deviceLogic(params, executor, getDefaultFileSystemExecutor()),
164 | getExecutor: getDefaultCommandExecutor,
165 | requirements: [{ allOf: ['deviceId'], message: 'deviceId is required' }],
166 | }),
167 | };
168 |
```
--------------------------------------------------------------------------------
/src/utils/debugger/dap/__tests__/transport-framing.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { ChildProcess } from 'node:child_process';
2 | import { EventEmitter } from 'node:events';
3 | import { PassThrough } from 'node:stream';
4 | import type { InteractiveProcess, InteractiveSpawner } from '../../../execution/index.ts';
5 | import { describe, expect, it } from 'vitest';
6 |
7 | import { DapTransport } from '../transport.ts';
8 | import type { DapEvent, DapResponse } from '../types.ts';
9 | type TestSession = {
10 | stdout: PassThrough;
11 | stderr: PassThrough;
12 | stdin: PassThrough;
13 | emitExit: (code?: number | null, signal?: NodeJS.Signals | null) => void;
14 | emitError: (error: Error) => void;
15 | };
16 |
17 | function encodeMessage(message: Record<string, unknown>): string {
18 | const payload = JSON.stringify(message);
19 | return `Content-Length: ${Buffer.byteLength(payload, 'utf8')}\r\n\r\n${payload}`;
20 | }
21 |
22 | function buildResponse(
23 | requestSeq: number,
24 | command: string,
25 | body?: Record<string, unknown>,
26 | ): DapResponse {
27 | return {
28 | seq: requestSeq + 100,
29 | type: 'response',
30 | request_seq: requestSeq,
31 | success: true,
32 | command,
33 | body,
34 | };
35 | }
36 |
37 | function createTestSpawner(): { spawner: InteractiveSpawner; session: TestSession } {
38 | const stdout = new PassThrough();
39 | const stderr = new PassThrough();
40 | const stdin = new PassThrough();
41 | const emitter = new EventEmitter();
42 | const mockProcess = emitter as unknown as ChildProcess;
43 | const mutableProcess = mockProcess as unknown as {
44 | stdout: PassThrough | null;
45 | stderr: PassThrough | null;
46 | stdin: PassThrough | null;
47 | killed: boolean;
48 | exitCode: number | null;
49 | signalCode: NodeJS.Signals | null;
50 | spawnargs: string[];
51 | spawnfile: string;
52 | pid: number;
53 | };
54 |
55 | mutableProcess.stdout = stdout;
56 | mutableProcess.stderr = stderr;
57 | mutableProcess.stdin = stdin;
58 | mutableProcess.killed = false;
59 | mutableProcess.exitCode = null;
60 | mutableProcess.signalCode = null;
61 | mutableProcess.spawnargs = [];
62 | mutableProcess.spawnfile = 'mock';
63 | mutableProcess.pid = 12345;
64 | mockProcess.kill = ((signal?: NodeJS.Signals): boolean => {
65 | mutableProcess.killed = true;
66 | emitter.emit('exit', 0, signal ?? null);
67 | return true;
68 | }) as ChildProcess['kill'];
69 |
70 | const session: TestSession = {
71 | stdout,
72 | stderr,
73 | stdin,
74 | emitExit: (code = 0, signal = null) => {
75 | emitter.emit('exit', code, signal);
76 | },
77 | emitError: (error) => {
78 | emitter.emit('error', error);
79 | },
80 | };
81 |
82 | const spawner: InteractiveSpawner = (): InteractiveProcess => ({
83 | process: mockProcess,
84 | write(data: string): void {
85 | stdin.write(data);
86 | },
87 | kill(signal?: NodeJS.Signals): void {
88 | mockProcess.kill?.(signal);
89 | },
90 | dispose(): void {
91 | stdout.end();
92 | stderr.end();
93 | stdin.end();
94 | emitter.removeAllListeners();
95 | },
96 | });
97 |
98 | return { spawner, session };
99 | }
100 |
101 | describe('DapTransport framing', () => {
102 | it('parses responses across chunk boundaries', async () => {
103 | const { spawner, session } = createTestSpawner();
104 |
105 | const transport = new DapTransport({ spawner, adapterCommand: ['lldb-dap'] });
106 |
107 | const responsePromise = transport.sendRequest<undefined, { ok: boolean }>(
108 | 'initialize',
109 | undefined,
110 | { timeoutMs: 1_000 },
111 | );
112 |
113 | const response = encodeMessage(buildResponse(1, 'initialize', { ok: true }));
114 | session.stdout.write(response.slice(0, 12));
115 | session.stdout.write(response.slice(12));
116 |
117 | await expect(responsePromise).resolves.toEqual({ ok: true });
118 | transport.dispose();
119 | });
120 |
121 | it('handles multiple messages in a single chunk', async () => {
122 | const { spawner, session } = createTestSpawner();
123 |
124 | const transport = new DapTransport({ spawner, adapterCommand: ['lldb-dap'] });
125 | const events: DapEvent[] = [];
126 | transport.onEvent((event) => events.push(event));
127 |
128 | const responsePromise = transport.sendRequest<undefined, { ok: boolean }>(
129 | 'threads',
130 | undefined,
131 | { timeoutMs: 1_000 },
132 | );
133 |
134 | const eventMessage = encodeMessage({
135 | seq: 55,
136 | type: 'event',
137 | event: 'output',
138 | body: { output: 'hello' },
139 | });
140 | const responseMessage = encodeMessage(buildResponse(1, 'threads', { ok: true }));
141 |
142 | session.stdout.write(`${eventMessage}${responseMessage}`);
143 |
144 | await expect(responsePromise).resolves.toEqual({ ok: true });
145 | expect(events).toHaveLength(1);
146 | expect(events[0]?.event).toBe('output');
147 | transport.dispose();
148 | });
149 |
150 | it('continues after invalid headers', async () => {
151 | const { spawner, session } = createTestSpawner();
152 |
153 | const transport = new DapTransport({ spawner, adapterCommand: ['lldb-dap'] });
154 |
155 | const responsePromise = transport.sendRequest<undefined, { ok: boolean }>(
156 | 'stackTrace',
157 | undefined,
158 | { timeoutMs: 1_000 },
159 | );
160 |
161 | session.stdout.write('Content-Length: nope\r\n\r\n');
162 | const responseMessage = encodeMessage(buildResponse(1, 'stackTrace', { ok: true }));
163 | session.stdout.write(responseMessage);
164 |
165 | await expect(responsePromise).resolves.toEqual({ ok: true });
166 | transport.dispose();
167 | });
168 | });
169 |
```
--------------------------------------------------------------------------------
/src/utils/__tests__/typed-tool-factory.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Tests for the createTypedTool factory
3 | */
4 |
5 | import { describe, it, expect } from 'vitest';
6 | import * as z from 'zod';
7 | import { createTypedTool } from '../typed-tool-factory.ts';
8 | import { createMockExecutor } from '../../test-utils/mock-executors.ts';
9 | import { ToolResponse } from '../../types/common.ts';
10 |
11 | // Test schema and types
12 | const testSchema = z.object({
13 | requiredParam: z.string().describe('A required string parameter'),
14 | optionalParam: z.number().optional().describe('An optional number parameter'),
15 | });
16 |
17 | type TestParams = z.infer<typeof testSchema>;
18 |
19 | // Mock logic function for testing
20 | async function testLogic(params: TestParams): Promise<ToolResponse> {
21 | return {
22 | content: [{ type: 'text', text: `Logic executed with: ${params.requiredParam}` }],
23 | isError: false,
24 | };
25 | }
26 |
27 | describe('createTypedTool', () => {
28 | describe('Type Safety and Validation', () => {
29 | it('should accept valid parameters and call logic function', async () => {
30 | const mockExecutor = createMockExecutor({ success: true, output: 'test' });
31 | const handler = createTypedTool(testSchema, testLogic, () => mockExecutor);
32 |
33 | const result = await handler({
34 | requiredParam: 'valid-value',
35 | optionalParam: 42,
36 | });
37 |
38 | expect(result.isError).toBe(false);
39 | expect(result.content[0].text).toContain('Logic executed with: valid-value');
40 | });
41 |
42 | it('should reject parameters with missing required fields', async () => {
43 | const mockExecutor = createMockExecutor({ success: true, output: 'test' });
44 | const handler = createTypedTool(testSchema, testLogic, () => mockExecutor);
45 |
46 | const result = await handler({
47 | // Missing requiredParam
48 | optionalParam: 42,
49 | });
50 |
51 | expect(result.isError).toBe(true);
52 | expect(result.content[0].text).toContain('Parameter validation failed');
53 | expect(result.content[0].text).toContain('requiredParam');
54 | });
55 |
56 | it('should reject parameters with wrong types', async () => {
57 | const mockExecutor = createMockExecutor({ success: true, output: 'test' });
58 | const handler = createTypedTool(testSchema, testLogic, () => mockExecutor);
59 |
60 | const result = await handler({
61 | requiredParam: 123, // Should be string, not number
62 | optionalParam: 42,
63 | });
64 |
65 | expect(result.isError).toBe(true);
66 | expect(result.content[0].text).toContain('Parameter validation failed');
67 | expect(result.content[0].text).toContain('requiredParam');
68 | });
69 |
70 | it('should accept parameters with only required fields', async () => {
71 | const mockExecutor = createMockExecutor({ success: true, output: 'test' });
72 | const handler = createTypedTool(testSchema, testLogic, () => mockExecutor);
73 |
74 | const result = await handler({
75 | requiredParam: 'valid-value',
76 | // optionalParam omitted
77 | });
78 |
79 | expect(result.isError).toBe(false);
80 | expect(result.content[0].text).toContain('Logic executed with: valid-value');
81 | });
82 |
83 | it('should provide detailed validation error messages', async () => {
84 | const mockExecutor = createMockExecutor({ success: true, output: 'test' });
85 | const handler = createTypedTool(testSchema, testLogic, () => mockExecutor);
86 |
87 | const result = await handler({
88 | requiredParam: 123, // Wrong type
89 | optionalParam: 'should-be-number', // Wrong type
90 | });
91 |
92 | expect(result.isError).toBe(true);
93 | const errorText = result.content[0].text;
94 | expect(errorText).toContain('Parameter validation failed');
95 | expect(errorText).toContain('requiredParam');
96 | expect(errorText).toContain('optionalParam');
97 | });
98 | });
99 |
100 | describe('Error Handling', () => {
101 | it('should re-throw non-Zod errors from logic function', async () => {
102 | const mockExecutor = createMockExecutor({ success: true, output: 'test' });
103 |
104 | // Logic function that throws a non-Zod error
105 | async function errorLogic(): Promise<ToolResponse> {
106 | throw new Error('Unexpected error');
107 | }
108 |
109 | const handler = createTypedTool(testSchema, errorLogic, () => mockExecutor);
110 |
111 | await expect(handler({ requiredParam: 'valid' })).rejects.toThrow('Unexpected error');
112 | });
113 | });
114 |
115 | describe('Executor Integration', () => {
116 | it('should pass the provided executor to logic function', async () => {
117 | const mockExecutor = createMockExecutor({ success: true, output: 'test' });
118 |
119 | async function executorTestLogic(params: TestParams, executor: any): Promise<ToolResponse> {
120 | // Verify executor is passed correctly
121 | expect(executor).toBe(mockExecutor);
122 | return {
123 | content: [{ type: 'text', text: 'Executor passed correctly' }],
124 | isError: false,
125 | };
126 | }
127 |
128 | const handler = createTypedTool(testSchema, executorTestLogic, () => mockExecutor);
129 |
130 | const result = await handler({ requiredParam: 'valid' });
131 |
132 | expect(result.isError).toBe(false);
133 | expect(result.content[0].text).toBe('Executor passed correctly');
134 | });
135 | });
136 | });
137 |
```
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
1 | import eslint from '@eslint/js';
2 | import tseslint from 'typescript-eslint';
3 | import prettierPlugin from 'eslint-plugin-prettier';
4 |
5 | export default [
6 | eslint.configs.recommended,
7 | ...tseslint.configs.recommended,
8 | {
9 | ignores: ['node_modules/**', 'build/**', 'dist/**', 'coverage/**', 'src/core/generated-plugins.ts', 'src/core/generated-resources.ts'],
10 | },
11 | {
12 | // TypeScript files in src/ directory (covered by tsconfig.json)
13 | files: ['src/**/*.ts'],
14 | languageOptions: {
15 | ecmaVersion: 2020,
16 | sourceType: 'module',
17 | parser: tseslint.parser,
18 | parserOptions: {
19 | project: ['./tsconfig.json'],
20 | },
21 | },
22 | plugins: {
23 | '@typescript-eslint': tseslint.plugin,
24 | 'prettier': prettierPlugin,
25 | },
26 | rules: {
27 | 'prettier/prettier': 'error',
28 | '@typescript-eslint/explicit-function-return-type': 'warn',
29 | '@typescript-eslint/no-explicit-any': 'error',
30 | '@typescript-eslint/no-unused-vars': ['error', {
31 | argsIgnorePattern: 'never',
32 | varsIgnorePattern: 'never'
33 | }],
34 | 'no-console': ['warn', { allow: ['error'] }],
35 |
36 | // Prevent dangerous type casting anti-patterns (errors)
37 | '@typescript-eslint/consistent-type-assertions': ['error', {
38 | assertionStyle: 'as',
39 | objectLiteralTypeAssertions: 'never'
40 | }],
41 | '@typescript-eslint/no-unsafe-argument': 'error',
42 | '@typescript-eslint/no-unsafe-assignment': 'error',
43 | '@typescript-eslint/no-unsafe-call': 'error',
44 | '@typescript-eslint/no-unsafe-member-access': 'error',
45 | '@typescript-eslint/no-unsafe-return': 'error',
46 |
47 | // Prevent specific anti-patterns we found
48 | '@typescript-eslint/ban-ts-comment': ['error', {
49 | 'ts-expect-error': 'allow-with-description',
50 | 'ts-ignore': true,
51 | 'ts-nocheck': true,
52 | 'ts-check': false,
53 | }],
54 |
55 | // Encourage best practices (warnings - can be gradually fixed)
56 | '@typescript-eslint/prefer-as-const': 'warn',
57 | '@typescript-eslint/prefer-nullish-coalescing': 'warn',
58 | '@typescript-eslint/prefer-optional-chain': 'warn',
59 |
60 | // Prevent barrel imports to maintain architectural improvements
61 | 'no-restricted-imports': ['error', {
62 | patterns: [
63 | {
64 | group: ['**/utils/index.js', '../utils/index.js', '../../utils/index.js', '../../../utils/index.js', '**/utils/index.ts', '../utils/index.ts', '../../utils/index.ts', '../../../utils/index.ts'],
65 | message: 'Barrel imports from utils/index are prohibited. Use focused facade imports instead (e.g., utils/logging/index.ts, utils/execution/index.ts).'
66 | },
67 | {
68 | group: ['./**/*.js', '../**/*.js'],
69 | message: 'Import TypeScript files with .ts extension, not .js. This ensures compatibility with native TypeScript runtimes like Bun and Deno. Change .js to .ts in your import path.'
70 | }
71 | ]
72 | }],
73 | },
74 | },
75 | {
76 | // JavaScript and TypeScript files outside the main project (scripts/, etc.)
77 | files: ['**/*.{js,ts}'],
78 | ignores: ['src/**/*', '**/*.test.ts'],
79 | languageOptions: {
80 | ecmaVersion: 2020,
81 | sourceType: 'module',
82 | parser: tseslint.parser,
83 | // No project reference for scripts - use standalone parsing
84 | },
85 | plugins: {
86 | '@typescript-eslint': tseslint.plugin,
87 | 'prettier': prettierPlugin,
88 | },
89 | rules: {
90 | 'prettier/prettier': 'error',
91 | // Relaxed TypeScript rules for scripts since they're not in the main project
92 | '@typescript-eslint/explicit-function-return-type': 'off',
93 | '@typescript-eslint/no-explicit-any': 'warn',
94 | '@typescript-eslint/no-unused-vars': ['warn', {
95 | argsIgnorePattern: 'never',
96 | varsIgnorePattern: 'never'
97 | }],
98 | 'no-console': 'off', // Scripts are allowed to use console
99 |
100 | // Disable project-dependent rules for scripts
101 | '@typescript-eslint/no-unsafe-argument': 'off',
102 | '@typescript-eslint/no-unsafe-assignment': 'off',
103 | '@typescript-eslint/no-unsafe-call': 'off',
104 | '@typescript-eslint/no-unsafe-member-access': 'off',
105 | '@typescript-eslint/no-unsafe-return': 'off',
106 | '@typescript-eslint/prefer-nullish-coalescing': 'off',
107 | '@typescript-eslint/prefer-optional-chain': 'off',
108 | },
109 | },
110 | {
111 | files: ['**/*.test.ts'],
112 | languageOptions: {
113 | parser: tseslint.parser,
114 | parserOptions: {
115 | project: './tsconfig.test.json',
116 | },
117 | },
118 | rules: {
119 | '@typescript-eslint/no-explicit-any': 'off',
120 | '@typescript-eslint/no-unused-vars': 'off',
121 | '@typescript-eslint/explicit-function-return-type': 'off',
122 | 'prefer-const': 'off',
123 |
124 | // Relax unsafe rules for tests - tests often need more flexibility
125 | '@typescript-eslint/no-unsafe-argument': 'off',
126 | '@typescript-eslint/no-unsafe-assignment': 'off',
127 | '@typescript-eslint/no-unsafe-call': 'off',
128 | '@typescript-eslint/no-unsafe-member-access': 'off',
129 | '@typescript-eslint/no-unsafe-return': 'off',
130 | },
131 | },
132 | ];
133 |
```
--------------------------------------------------------------------------------
/src/utils/axe-helpers.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * AXe Helper Functions
3 | *
4 | * This utility module provides functions to resolve and execute AXe.
5 | * Prefers bundled AXe when present, but allows env and PATH fallback.
6 | */
7 |
8 | import { accessSync, constants, existsSync } from 'fs';
9 | import { dirname, join, resolve, delimiter } from 'path';
10 | import { createTextResponse } from './validation.ts';
11 | import { ToolResponse } from '../types/common.ts';
12 | import type { CommandExecutor } from './execution/index.ts';
13 | import { getDefaultCommandExecutor } from './execution/index.ts';
14 |
15 | const AXE_PATH_ENV_VARS = ['XCODEBUILDMCP_AXE_PATH', 'AXE_PATH'] as const;
16 |
17 | export type AxeBinarySource = 'env' | 'bundled' | 'path';
18 |
19 | export type AxeBinary = {
20 | path: string;
21 | source: AxeBinarySource;
22 | };
23 |
24 | function getPackageRoot(): string {
25 | const entry = process.argv[1];
26 | if (entry) {
27 | const entryDir = dirname(entry);
28 | return dirname(entryDir);
29 | }
30 | return process.cwd();
31 | }
32 |
33 | // In the npm package, build/index.js is at the same level as bundled/
34 | // So we go up one level from build/ to get to the package root
35 | const bundledAxePath = join(getPackageRoot(), 'bundled', 'axe');
36 |
37 | function isExecutable(path: string): boolean {
38 | try {
39 | accessSync(path, constants.X_OK);
40 | return true;
41 | } catch {
42 | return false;
43 | }
44 | }
45 |
46 | function resolveAxePathFromEnv(): string | null {
47 | for (const envVar of AXE_PATH_ENV_VARS) {
48 | const value = process.env[envVar];
49 | if (!value) continue;
50 | const resolved = resolve(value);
51 | if (isExecutable(resolved)) {
52 | return resolved;
53 | }
54 | }
55 | return null;
56 | }
57 |
58 | function resolveBundledAxePath(): string | null {
59 | const entry = process.argv[1];
60 | const candidates = new Set<string>();
61 | if (entry) {
62 | const entryDir = dirname(entry);
63 | candidates.add(join(dirname(entryDir), 'bundled', 'axe'));
64 | candidates.add(join(entryDir, 'bundled', 'axe'));
65 | }
66 | candidates.add(bundledAxePath);
67 | candidates.add(join(process.cwd(), 'bundled', 'axe'));
68 |
69 | for (const candidate of candidates) {
70 | if (existsSync(candidate)) {
71 | return candidate;
72 | }
73 | }
74 | return null;
75 | }
76 |
77 | function resolveAxePathFromPath(): string | null {
78 | const pathValue = process.env.PATH ?? '';
79 | const entries = pathValue.split(delimiter).filter(Boolean);
80 | for (const entry of entries) {
81 | const candidate = join(entry, 'axe');
82 | if (isExecutable(candidate)) {
83 | return candidate;
84 | }
85 | }
86 | return null;
87 | }
88 |
89 | export function resolveAxeBinary(): AxeBinary | null {
90 | const envPath = resolveAxePathFromEnv();
91 | if (envPath) {
92 | return { path: envPath, source: 'env' };
93 | }
94 |
95 | const bundledPath = resolveBundledAxePath();
96 | if (bundledPath) {
97 | return { path: bundledPath, source: 'bundled' };
98 | }
99 |
100 | const pathBinary = resolveAxePathFromPath();
101 | if (pathBinary) {
102 | return { path: pathBinary, source: 'path' };
103 | }
104 |
105 | return null;
106 | }
107 |
108 | /**
109 | * Get the path to the available axe binary
110 | */
111 | export function getAxePath(): string | null {
112 | return resolveAxeBinary()?.path ?? null;
113 | }
114 |
115 | /**
116 | * Get environment variables needed for bundled AXe to run
117 | */
118 | export function getBundledAxeEnvironment(): Record<string, string> {
119 | // No special environment variables needed - bundled AXe binary
120 | // has proper @rpath configuration to find frameworks
121 | return {};
122 | }
123 |
124 | /**
125 | * Check if axe tool is available (bundled, env override, or PATH)
126 | */
127 | export function areAxeToolsAvailable(): boolean {
128 | return getAxePath() !== null;
129 | }
130 |
131 | export function createAxeNotAvailableResponse(): ToolResponse {
132 | return createTextResponse(
133 | 'AXe tool not found. UI automation features are not available.\n\n' +
134 | 'Install AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\n' +
135 | 'If you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
136 | true,
137 | );
138 | }
139 |
140 | /**
141 | * Compare two semver strings a and b.
142 | * Returns 1 if a > b, -1 if a < b, 0 if equal.
143 | */
144 | function compareSemver(a: string, b: string): number {
145 | const pa = a.split('.').map((n) => parseInt(n, 10));
146 | const pb = b.split('.').map((n) => parseInt(n, 10));
147 | const len = Math.max(pa.length, pb.length);
148 | for (let i = 0; i < len; i++) {
149 | const da = Number.isFinite(pa[i]) ? pa[i] : 0;
150 | const db = Number.isFinite(pb[i]) ? pb[i] : 0;
151 | if (da > db) return 1;
152 | if (da < db) return -1;
153 | }
154 | return 0;
155 | }
156 |
157 | /**
158 | * Determine whether the bundled AXe meets a minimum version requirement.
159 | * Runs `axe --version` and parses a semantic version (e.g., "1.1.0").
160 | * If AXe is missing or the version cannot be parsed, returns false.
161 | */
162 | export async function isAxeAtLeastVersion(
163 | required: string,
164 | executor?: CommandExecutor,
165 | ): Promise<boolean> {
166 | const axePath = getAxePath();
167 | if (!axePath) return false;
168 |
169 | const exec = executor ?? getDefaultCommandExecutor();
170 | try {
171 | const res = await exec([axePath, '--version'], 'AXe Version', true);
172 | if (!res.success) return false;
173 |
174 | const output = res.output ?? '';
175 | const versionMatch = output.match(/(\d+\.\d+\.\d+)/);
176 | if (!versionMatch) return false;
177 |
178 | const current = versionMatch[1];
179 | return compareSemver(current, required) >= 0;
180 | } catch {
181 | return false;
182 | }
183 | }
184 |
```
--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Logger Utility - Simple logging implementation for the application
3 | *
4 | * This utility module provides a lightweight logging system that directs log
5 | * messages to stderr rather than stdout, ensuring they don't interfere with
6 | * the MCP protocol communication which uses stdout.
7 | *
8 | * Responsibilities:
9 | * - Formatting log messages with timestamps and level indicators
10 | * - Directing all logs to stderr to avoid MCP protocol interference
11 | * - Supporting different log levels (info, warning, error, debug)
12 | * - Providing a simple, consistent logging interface throughout the application
13 | * - Sending error-level logs to Sentry for monitoring and alerting
14 | *
15 | * While intentionally minimal, this logger provides the essential functionality
16 | * needed for operational monitoring and debugging throughout the application.
17 | * It's used by virtually all other modules for status reporting and error logging.
18 | */
19 |
20 | import { createRequire } from 'node:module';
21 | import { resolve } from 'node:path';
22 | // Note: Removed "import * as Sentry from '@sentry/node'" to prevent native module loading at import time
23 |
24 | const SENTRY_ENABLED =
25 | process.env.SENTRY_DISABLED !== 'true' && process.env.XCODEBUILDMCP_SENTRY_DISABLED !== 'true';
26 |
27 | // Log levels in order of severity (lower number = more severe)
28 | const LOG_LEVELS = {
29 | emergency: 0,
30 | alert: 1,
31 | critical: 2,
32 | error: 3,
33 | warning: 4,
34 | notice: 5,
35 | info: 6,
36 | debug: 7,
37 | } as const;
38 |
39 | export type LogLevel = keyof typeof LOG_LEVELS;
40 |
41 | /**
42 | * Optional context for logging to control Sentry capture
43 | */
44 | export interface LogContext {
45 | sentry?: boolean;
46 | }
47 |
48 | // Client-requested log level (null means no filtering)
49 | let clientLogLevel: LogLevel | null = null;
50 |
51 | function isTestEnv(): boolean {
52 | return (
53 | process.env.VITEST === 'true' ||
54 | process.env.NODE_ENV === 'test' ||
55 | process.env.XCODEBUILDMCP_SILENCE_LOGS === 'true'
56 | );
57 | }
58 |
59 | type SentryModule = typeof import('@sentry/node');
60 |
61 | const require = createRequire(
62 | typeof __filename === 'string' ? __filename : resolve(process.cwd(), 'package.json'),
63 | );
64 | let cachedSentry: SentryModule | null = null;
65 |
66 | function loadSentrySync(): SentryModule | null {
67 | if (!SENTRY_ENABLED || isTestEnv()) return null;
68 | if (cachedSentry) return cachedSentry;
69 | try {
70 | cachedSentry = require('@sentry/node') as SentryModule;
71 | return cachedSentry;
72 | } catch {
73 | // If @sentry/node is not installed in some environments, fail silently.
74 | return null;
75 | }
76 | }
77 |
78 | function withSentry(cb: (s: SentryModule) => void): void {
79 | const s = loadSentrySync();
80 | if (!s) return;
81 | try {
82 | cb(s);
83 | } catch {
84 | // no-op: avoid throwing inside logger
85 | }
86 | }
87 |
88 | if (!SENTRY_ENABLED) {
89 | if (process.env.SENTRY_DISABLED === 'true') {
90 | log('info', 'Sentry disabled due to SENTRY_DISABLED environment variable');
91 | } else if (process.env.XCODEBUILDMCP_SENTRY_DISABLED === 'true') {
92 | log('info', 'Sentry disabled due to XCODEBUILDMCP_SENTRY_DISABLED environment variable');
93 | }
94 | }
95 |
96 | /**
97 | * Set the minimum log level for client-requested filtering
98 | * @param level The minimum log level to output
99 | */
100 | export function setLogLevel(level: LogLevel): void {
101 | clientLogLevel = level;
102 | log('debug', `Log level set to: ${level}`);
103 | }
104 |
105 | /**
106 | * Get the current client-requested log level
107 | * @returns The current log level or null if no filtering is active
108 | */
109 | export function getLogLevel(): LogLevel | null {
110 | return clientLogLevel;
111 | }
112 |
113 | /**
114 | * Check if a log level should be output based on client settings
115 | * @param level The log level to check
116 | * @returns true if the message should be logged
117 | */
118 | function shouldLog(level: string): boolean {
119 | // Suppress logging during tests to keep test output clean
120 | if (isTestEnv()) {
121 | return false;
122 | }
123 |
124 | // If no client level set, log everything
125 | if (clientLogLevel === null) {
126 | return true;
127 | }
128 |
129 | // Check if the level is valid
130 | const levelKey = level.toLowerCase() as LogLevel;
131 | if (!(levelKey in LOG_LEVELS)) {
132 | return true; // Log unknown levels
133 | }
134 |
135 | // Only log if the message level is at or above the client's requested level
136 | return LOG_LEVELS[levelKey] <= LOG_LEVELS[clientLogLevel];
137 | }
138 |
139 | /**
140 | * Log a message with the specified level
141 | * @param level The log level (emergency, alert, critical, error, warning, notice, info, debug)
142 | * @param message The message to log
143 | * @param context Optional context to control Sentry capture and other behavior
144 | */
145 | export function log(level: string, message: string, context?: LogContext): void {
146 | // Check if we should log this level
147 | if (!shouldLog(level)) {
148 | return;
149 | }
150 |
151 | const timestamp = new Date().toISOString();
152 | const logMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
153 |
154 | // Default: error level goes to Sentry
155 | // But respect explicit override from context
156 | const captureToSentry = SENTRY_ENABLED && (context?.sentry ?? level === 'error');
157 |
158 | if (captureToSentry) {
159 | withSentry((s) => s.captureMessage(logMessage));
160 | }
161 |
162 | // It's important to use console.error here to ensure logs don't interfere with MCP protocol communication
163 | // see https://modelcontextprotocol.io/docs/tools/debugging#server-side-logging
164 | console.error(logMessage);
165 | }
166 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/project-discovery/list_schemes.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Project Discovery Plugin: List Schemes (Unified)
3 | *
4 | * Lists available schemes for either a project or workspace using xcodebuild.
5 | * Accepts mutually exclusive `projectPath` or `workspacePath`.
6 | */
7 |
8 | import * as z from 'zod';
9 | import { log } from '../../../utils/logging/index.ts';
10 | import type { CommandExecutor } from '../../../utils/execution/index.ts';
11 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
12 | import { createTextResponse } from '../../../utils/responses/index.ts';
13 | import { ToolResponse } from '../../../types/common.ts';
14 | import {
15 | createSessionAwareTool,
16 | getSessionAwareToolSchemaShape,
17 | } from '../../../utils/typed-tool-factory.ts';
18 | import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
19 |
20 | // Unified schema: XOR between projectPath and workspacePath
21 | const baseSchemaObject = z.object({
22 | projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
23 | workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
24 | });
25 |
26 | const listSchemesSchema = z.preprocess(
27 | nullifyEmptyStrings,
28 | baseSchemaObject
29 | .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
30 | message: 'Either projectPath or workspacePath is required.',
31 | })
32 | .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
33 | message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
34 | }),
35 | );
36 |
37 | export type ListSchemesParams = z.infer<typeof listSchemesSchema>;
38 |
39 | const createTextBlock = (text: string) => ({ type: 'text', text }) as const;
40 |
41 | /**
42 | * Business logic for listing schemes in a project or workspace.
43 | * Exported for direct testing and reuse.
44 | */
45 | export async function listSchemesLogic(
46 | params: ListSchemesParams,
47 | executor: CommandExecutor,
48 | ): Promise<ToolResponse> {
49 | log('info', 'Listing schemes');
50 |
51 | try {
52 | // For listing schemes, we can't use executeXcodeBuild directly since it's not a standard action
53 | // We need to create a custom command with -list flag
54 | const command = ['xcodebuild', '-list'];
55 |
56 | const hasProjectPath = typeof params.projectPath === 'string';
57 | const projectOrWorkspace = hasProjectPath ? 'project' : 'workspace';
58 | const path = hasProjectPath ? params.projectPath : params.workspacePath;
59 |
60 | if (hasProjectPath) {
61 | command.push('-project', params.projectPath!);
62 | } else {
63 | command.push('-workspace', params.workspacePath!);
64 | }
65 |
66 | const result = await executor(command, 'List Schemes', true);
67 |
68 | if (!result.success) {
69 | return createTextResponse(`Failed to list schemes: ${result.error}`, true);
70 | }
71 |
72 | // Extract schemes from the output
73 | const schemesMatch = result.output.match(/Schemes:([\s\S]*?)(?=\n\n|$)/);
74 |
75 | if (!schemesMatch) {
76 | return createTextResponse('No schemes found in the output', true);
77 | }
78 |
79 | const schemeLines = schemesMatch[1].trim().split('\n');
80 | const schemes = schemeLines.map((line) => line.trim()).filter((line) => line);
81 |
82 | // Prepare next steps with the first scheme if available
83 | let nextStepsText = '';
84 | let hintText = '';
85 | if (schemes.length > 0) {
86 | const firstScheme = schemes[0];
87 |
88 | // Note: After Phase 2, these will be unified tool names too
89 | nextStepsText = `Next Steps:
90 | 1. Build the app: build_macos({ ${projectOrWorkspace}Path: "${path}", scheme: "${firstScheme}" })
91 | or for iOS: build_sim({ ${projectOrWorkspace}Path: "${path}", scheme: "${firstScheme}", simulatorName: "iPhone 16" })
92 | 2. Show build settings: show_build_settings({ ${projectOrWorkspace}Path: "${path}", scheme: "${firstScheme}" })`;
93 |
94 | hintText =
95 | `Hint: Consider saving a default scheme with session-set-defaults ` +
96 | `{ scheme: "${firstScheme}" } to avoid repeating it.`;
97 | }
98 |
99 | const content = [
100 | createTextBlock('✅ Available schemes:'),
101 | createTextBlock(schemes.join('\n')),
102 | createTextBlock(nextStepsText),
103 | ];
104 | if (hintText.length > 0) {
105 | content.push(createTextBlock(hintText));
106 | }
107 |
108 | return {
109 | content,
110 | isError: false,
111 | };
112 | } catch (error) {
113 | const errorMessage = error instanceof Error ? error.message : String(error);
114 | log('error', `Error listing schemes: ${errorMessage}`);
115 | return createTextResponse(`Error listing schemes: ${errorMessage}`, true);
116 | }
117 | }
118 |
119 | const publicSchemaObject = baseSchemaObject.omit({
120 | projectPath: true,
121 | workspacePath: true,
122 | } as const);
123 |
124 | export default {
125 | name: 'list_schemes',
126 | description: 'Lists schemes for a project or workspace.',
127 | schema: getSessionAwareToolSchemaShape({
128 | sessionAware: publicSchemaObject,
129 | legacy: baseSchemaObject,
130 | }),
131 | annotations: {
132 | title: 'List Schemes',
133 | readOnlyHint: true,
134 | },
135 | handler: createSessionAwareTool<ListSchemesParams>({
136 | internalSchema: listSchemesSchema as unknown as z.ZodType<ListSchemesParams, unknown>,
137 | logicFunction: listSchemesLogic,
138 | getExecutor: getDefaultCommandExecutor,
139 | requirements: [
140 | { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
141 | ],
142 | exclusivePairs: [['projectPath', 'workspacePath']],
143 | }),
144 | };
145 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/simulator/stop_app_sim.ts:
--------------------------------------------------------------------------------
```typescript
1 | import * as z from 'zod';
2 | import { ToolResponse } from '../../../types/common.ts';
3 | import { log } from '../../../utils/logging/index.ts';
4 | import type { CommandExecutor } from '../../../utils/execution/index.ts';
5 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
6 | import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
7 | import {
8 | createSessionAwareTool,
9 | getSessionAwareToolSchemaShape,
10 | } from '../../../utils/typed-tool-factory.ts';
11 |
12 | const baseSchemaObject = z.object({
13 | simulatorId: z
14 | .string()
15 | .optional()
16 | .describe(
17 | 'UUID of the simulator to use (obtained from list_sims). Provide EITHER this OR simulatorName, not both',
18 | ),
19 | simulatorName: z
20 | .string()
21 | .optional()
22 | .describe(
23 | "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both",
24 | ),
25 | bundleId: z.string().describe("Bundle identifier of the app to stop (e.g., 'com.example.MyApp')"),
26 | });
27 |
28 | const stopAppSimSchema = z.preprocess(
29 | nullifyEmptyStrings,
30 | baseSchemaObject
31 | .refine((val) => val.simulatorId !== undefined || val.simulatorName !== undefined, {
32 | message: 'Either simulatorId or simulatorName is required.',
33 | })
34 | .refine((val) => !(val.simulatorId !== undefined && val.simulatorName !== undefined), {
35 | message: 'simulatorId and simulatorName are mutually exclusive. Provide only one.',
36 | }),
37 | );
38 |
39 | export type StopAppSimParams = z.infer<typeof stopAppSimSchema>;
40 |
41 | export async function stop_app_simLogic(
42 | params: StopAppSimParams,
43 | executor: CommandExecutor,
44 | ): Promise<ToolResponse> {
45 | let simulatorId = params.simulatorId;
46 | let simulatorDisplayName = simulatorId ?? '';
47 |
48 | if (params.simulatorName && !simulatorId) {
49 | log('info', `Looking up simulator by name: ${params.simulatorName}`);
50 |
51 | const simulatorListResult = await executor(
52 | ['xcrun', 'simctl', 'list', 'devices', 'available', '--json'],
53 | 'List Simulators',
54 | true,
55 | );
56 | if (!simulatorListResult.success) {
57 | return {
58 | content: [
59 | {
60 | type: 'text',
61 | text: `Failed to list simulators: ${simulatorListResult.error}`,
62 | },
63 | ],
64 | isError: true,
65 | };
66 | }
67 |
68 | const simulatorsData = JSON.parse(simulatorListResult.output) as {
69 | devices: Record<string, Array<{ udid: string; name: string }>>;
70 | };
71 |
72 | let foundSimulator: { udid: string; name: string } | null = null;
73 | for (const runtime in simulatorsData.devices) {
74 | const devices = simulatorsData.devices[runtime];
75 | const simulator = devices.find((device) => device.name === params.simulatorName);
76 | if (simulator) {
77 | foundSimulator = simulator;
78 | break;
79 | }
80 | }
81 |
82 | if (!foundSimulator) {
83 | return {
84 | content: [
85 | {
86 | type: 'text',
87 | text: `Simulator named "${params.simulatorName}" not found. Use list_sims to see available simulators.`,
88 | },
89 | ],
90 | isError: true,
91 | };
92 | }
93 |
94 | simulatorId = foundSimulator.udid;
95 | simulatorDisplayName = `"${params.simulatorName}" (${foundSimulator.udid})`;
96 | }
97 |
98 | if (!simulatorId) {
99 | return {
100 | content: [
101 | {
102 | type: 'text',
103 | text: 'No simulator identifier provided',
104 | },
105 | ],
106 | isError: true,
107 | };
108 | }
109 |
110 | log('info', `Stopping app ${params.bundleId} in simulator ${simulatorId}`);
111 |
112 | try {
113 | const command = ['xcrun', 'simctl', 'terminate', simulatorId, params.bundleId];
114 | const result = await executor(command, 'Stop App in Simulator', true, undefined);
115 |
116 | if (!result.success) {
117 | return {
118 | content: [
119 | {
120 | type: 'text',
121 | text: `Stop app in simulator operation failed: ${result.error}`,
122 | },
123 | ],
124 | isError: true,
125 | };
126 | }
127 |
128 | return {
129 | content: [
130 | {
131 | type: 'text',
132 | text: `✅ App ${params.bundleId} stopped successfully in simulator ${simulatorDisplayName || simulatorId}`,
133 | },
134 | ],
135 | };
136 | } catch (error) {
137 | const errorMessage = error instanceof Error ? error.message : String(error);
138 | log('error', `Error stopping app in simulator: ${errorMessage}`);
139 | return {
140 | content: [
141 | {
142 | type: 'text',
143 | text: `Stop app in simulator operation failed: ${errorMessage}`,
144 | },
145 | ],
146 | isError: true,
147 | };
148 | }
149 | }
150 |
151 | const publicSchemaObject = z.strictObject(
152 | baseSchemaObject.omit({
153 | simulatorId: true,
154 | simulatorName: true,
155 | } as const).shape,
156 | );
157 |
158 | export default {
159 | name: 'stop_app_sim',
160 | description: 'Stops an app running in an iOS simulator.',
161 | schema: getSessionAwareToolSchemaShape({
162 | sessionAware: publicSchemaObject,
163 | legacy: baseSchemaObject,
164 | }),
165 | annotations: {
166 | title: 'Stop App Simulator',
167 | destructiveHint: true,
168 | },
169 | handler: createSessionAwareTool<StopAppSimParams>({
170 | internalSchema: stopAppSimSchema as unknown as z.ZodType<StopAppSimParams, unknown>,
171 | logicFunction: stop_app_simLogic,
172 | getExecutor: getDefaultCommandExecutor,
173 | requirements: [
174 | { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
175 | ],
176 | exclusivePairs: [['simulatorId', 'simulatorName']],
177 | }),
178 | };
179 |
```