#
tokens: 47009/50000 7/393 files (page 11/12)
lines: off (toggle) GitHub
raw markdown copy
This is page 11 of 12. Use http://codebase.md/cameroncooke/xcodebuildmcp?lines=false&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

--------------------------------------------------------------------------------
/scripts/tools-cli.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

/**
 * XcodeBuildMCP Tools CLI
 *
 * A unified command-line tool that provides comprehensive information about
 * XcodeBuildMCP tools and resources. Supports both runtime inspection
 * (actual server state) and static analysis (source file analysis).
 *
 * Usage:
 *   npm run tools [command] [options]
 *   npx tsx src/cli/tools-cli.ts [command] [options]
 *
 * Commands:
 *   count, c        Show tool and workflow counts
 *   list, l         List all tools and resources
 *   static, s       Show static source file analysis
 *   help, h         Show this help message
 *
 * Options:
 *   --runtime, -r        Use runtime inspection (respects env config)
 *   --static, -s         Use static file analysis (development mode)
 *   --tools, -t          Include tools in output
 *   --resources          Include resources in output
 *   --workflows, -w      Include workflow information
 *   --verbose, -v        Show detailed information
 *   --json               Output JSON format
 *   --help              Show help for specific command
 *
 * Examples:
 *   npm run tools                         # Runtime summary with workflows
 *   npm run tools:count                   # Runtime tool count
 *   npm run tools:static                  # Static file analysis
 *   npm run tools:list                    # List runtime tools
 *   npx tsx src/cli/tools-cli.ts --json   # JSON output
 */

import { spawn } from 'child_process';
import * as path from 'path';
import { fileURLToPath } from 'url';
import * as fs from 'fs';
import { getStaticToolAnalysis, type StaticAnalysisResult } from './analysis/tools-analysis.js';

// Get project paths
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// ANSI color codes
const colors = {
  reset: '\x1b[0m',
  bright: '\x1b[1m',
  red: '\x1b[31m',
  green: '\x1b[32m',
  yellow: '\x1b[33m',
  blue: '\x1b[34m',
  cyan: '\x1b[36m',
  magenta: '\x1b[35m',
} as const;

// Types
interface CLIOptions {
  runtime: boolean;
  static: boolean;
  tools: boolean;
  resources: boolean;
  workflows: boolean;
  verbose: boolean;
  json: boolean;
  help: boolean;
}

interface RuntimeTool {
  name: string;
  description: string;
}

interface RuntimeResource {
  uri: string;
  name: string;
  description: string;
}

interface RuntimeData {
  tools: RuntimeTool[];
  resources: RuntimeResource[];
  toolCount: number;
  resourceCount: number;
  mode: 'runtime';
}

// CLI argument parsing
const args = process.argv.slice(2);

// Find the command (first non-flag argument)
let command = 'count'; // default
for (const arg of args) {
  if (!arg.startsWith('-')) {
    command = arg;
    break;
  }
}

const options: CLIOptions = {
  runtime: args.includes('--runtime') || args.includes('-r'),
  static: args.includes('--static') || args.includes('-s'),
  tools: args.includes('--tools') || args.includes('-t'),
  resources: args.includes('--resources'),
  workflows: args.includes('--workflows') || args.includes('-w'),
  verbose: args.includes('--verbose') || args.includes('-v'),
  json: args.includes('--json'),
  help: args.includes('--help') || args.includes('-h'),
};

// Set sensible defaults for each command
if (!options.runtime && !options.static) {
  if (command === 'static' || command === 's') {
    options.static = true;
  } else {
    // Default to static analysis for development-friendly usage
    options.static = true;
  }
}

// Set sensible content defaults
if (command === 'list' || command === 'l') {
  if (!options.tools && !options.resources && !options.workflows) {
    options.tools = true; // Default to showing tools for list command
  }
} else if (!command || command === 'count' || command === 'c') {
  // For no command or count, show comprehensive summary
  if (!options.tools && !options.resources && !options.workflows) {
    options.workflows = true; // Show workflows by default for summary
  }
}

// Help text
const helpText = {
  main: `
${colors.bright}${colors.blue}XcodeBuildMCP Tools CLI${colors.reset}

A unified command-line tool for XcodeBuildMCP tool and resource information.

${colors.bright}COMMANDS:${colors.reset}
  count, c        Show tool and workflow counts
  list, l         List all tools and resources  
  static, s       Show static source file analysis
  help, h         Show this help message

${colors.bright}OPTIONS:${colors.reset}
  --runtime, -r        Use runtime inspection (respects env config)
  --static, -s         Use static file analysis (default, development mode)
  --tools, -t          Include tools in output
  --resources          Include resources in output
  --workflows, -w      Include workflow information
  --verbose, -v        Show detailed information
  --json               Output JSON format

${colors.bright}EXAMPLES:${colors.reset}
  ${colors.cyan}npm run tools${colors.reset}                         # Static summary with workflows (default)
  ${colors.cyan}npm run tools list${colors.reset}                    # List tools
  ${colors.cyan}npm run tools --runtime${colors.reset}               # Runtime analysis (requires build)
  ${colors.cyan}npm run tools static${colors.reset}                  # Static analysis summary
  ${colors.cyan}npm run tools count --json${colors.reset}            # JSON output

${colors.bright}ANALYSIS MODES:${colors.reset}
  ${colors.green}Runtime${colors.reset}  Uses actual server inspection via Reloaderoo
           - Respects XCODEBUILDMCP_ENABLED_WORKFLOWS environment variable
           - Shows tools actually enabled at runtime
           - Requires built server (npm run build)
           
  ${colors.yellow}Static${colors.reset}   Scans source files directly using AST parsing
           - Shows all tools in codebase regardless of config
           - Development-time analysis with reliable description extraction
           - No server build required
`,

  count: `
${colors.bright}COUNT COMMAND${colors.reset}

Shows tool and workflow counts using runtime or static analysis.

${colors.bright}Usage:${colors.reset} npx tsx scripts/tools-cli.ts count [options]

${colors.bright}Options:${colors.reset}
  --runtime, -r        Count tools from running server
  --static, -s         Count tools from source files
  --workflows, -w      Include workflow directory counts
  --json               Output JSON format

${colors.bright}Examples:${colors.reset}
  ${colors.cyan}npx tsx scripts/tools-cli.ts count${colors.reset}                    # Runtime count
  ${colors.cyan}npx tsx scripts/tools-cli.ts count --static${colors.reset}          # Static count
  ${colors.cyan}npx tsx scripts/tools-cli.ts count --workflows${colors.reset}       # Include workflows
`,

  list: `
${colors.bright}LIST COMMAND${colors.reset}

Lists tools and resources with optional details.

${colors.bright}Usage:${colors.reset} npx tsx scripts/tools-cli.ts list [options]

${colors.bright}Options:${colors.reset}
  --runtime, -r        List from running server
  --static, -s         List from source files
  --tools, -t          Show tool names
  --resources          Show resource URIs
  --verbose, -v        Show detailed information
  --json               Output JSON format

${colors.bright}Examples:${colors.reset}
  ${colors.cyan}npx tsx scripts/tools-cli.ts list --tools${colors.reset}            # Runtime tool list
  ${colors.cyan}npx tsx scripts/tools-cli.ts list --resources${colors.reset}        # Runtime resource list
  ${colors.cyan}npx tsx scripts/tools-cli.ts list --static --verbose${colors.reset} # Static detailed list
`,

  static: `
${colors.bright}STATIC COMMAND${colors.reset}

Performs detailed static analysis of source files using AST parsing.

${colors.bright}Usage:${colors.reset} npx tsx scripts/tools-cli.ts static [options]

${colors.bright}Options:${colors.reset}
  --tools, -t          Show canonical tool details
  --workflows, -w      Show workflow directory analysis
  --verbose, -v        Show detailed file information
  --json               Output JSON format

${colors.bright}Examples:${colors.reset}
  ${colors.cyan}npx tsx scripts/tools-cli.ts static${colors.reset}                  # Basic static analysis
  ${colors.cyan}npx tsx scripts/tools-cli.ts static --verbose${colors.reset}        # Detailed analysis
  ${colors.cyan}npx tsx scripts/tools-cli.ts static --workflows${colors.reset}      # Include workflow info
`,
};

if (options.help) {
  console.log(helpText[command as keyof typeof helpText] || helpText.main);
  process.exit(0);
}

if (command === 'help' || command === 'h') {
  const helpCommand = args[1];
  console.log(helpText[helpCommand as keyof typeof helpText] || helpText.main);
  process.exit(0);
}

/**
 * Execute reloaderoo command and parse JSON response
 */
async function executeReloaderoo(reloaderooArgs: string[]): Promise<unknown> {
  const buildPath = path.resolve(__dirname, '..', 'build', 'index.js');

  if (!fs.existsSync(buildPath)) {
    throw new Error('Build not found. Please run "npm run build" first.');
  }

  const tempFile = `/tmp/reloaderoo-output-${Date.now()}.json`;
  const command = `npx -y reloaderoo@latest inspect ${reloaderooArgs.join(' ')} -- node "${buildPath}"`;

  return new Promise((resolve, reject) => {
    const child = spawn('bash', ['-c', `${command} > "${tempFile}"`], {
      stdio: 'inherit',
    });

    child.on('close', (code) => {
      try {
        if (code !== 0) {
          reject(new Error(`Command failed with code ${code}`));
          return;
        }

        const content = fs.readFileSync(tempFile, 'utf8');

        // Remove stderr log lines and find JSON
        const lines = content.split('\n');
        const cleanLines: string[] = [];

        for (const line of lines) {
          if (
            line.match(/^\[\d{4}-\d{2}-\d{2}T/) ||
            line.includes('[INFO]') ||
            line.includes('[DEBUG]') ||
            line.includes('[ERROR]')
          ) {
            continue;
          }

          const trimmed = line.trim();
          if (trimmed) {
            cleanLines.push(line);
          }
        }

        // Find JSON start
        let jsonStartIndex = -1;
        for (let i = 0; i < cleanLines.length; i++) {
          if (cleanLines[i].trim().startsWith('{')) {
            jsonStartIndex = i;
            break;
          }
        }

        if (jsonStartIndex === -1) {
          reject(
            new Error(`No JSON response found in output.\nOutput: ${content.substring(0, 500)}...`),
          );
          return;
        }

        const jsonText = cleanLines.slice(jsonStartIndex).join('\n');
        const response = JSON.parse(jsonText);
        resolve(response);
      } catch (error) {
        reject(new Error(`Failed to parse JSON response: ${(error as Error).message}`));
      } finally {
        try {
          fs.unlinkSync(tempFile);
        } catch {
          // Ignore cleanup errors
        }
      }
    });

    child.on('error', (error) => {
      reject(new Error(`Failed to spawn process: ${error.message}`));
    });
  });
}

/**
 * Get runtime server information
 */
async function getRuntimeInfo(): Promise<RuntimeData> {
  try {
    const toolsResponse = (await executeReloaderoo(['list-tools'])) as {
      tools?: { name: string; description: string }[];
    };
    const resourcesResponse = (await executeReloaderoo(['list-resources'])) as {
      resources?: { uri: string; name: string; description?: string; title?: string }[];
    };

    let tools: RuntimeTool[] = [];
    let toolCount = 0;

    if (toolsResponse.tools && Array.isArray(toolsResponse.tools)) {
      toolCount = toolsResponse.tools.length;
      tools = toolsResponse.tools.map((tool) => ({
        name: tool.name,
        description: tool.description,
      }));
    }

    let resources: RuntimeResource[] = [];
    let resourceCount = 0;

    if (resourcesResponse.resources && Array.isArray(resourcesResponse.resources)) {
      resourceCount = resourcesResponse.resources.length;
      resources = resourcesResponse.resources.map((resource) => ({
        uri: resource.uri,
        name: resource.name,
        description: resource.title ?? resource.description ?? 'No description available',
      }));
    }

    return {
      tools,
      resources,
      toolCount,
      resourceCount,
      mode: 'runtime',
    };
  } catch (error) {
    throw new Error(`Runtime analysis failed: ${(error as Error).message}`);
  }
}

/**
 * Display summary information
 */
function displaySummary(
  runtimeData: RuntimeData | null,
  staticData: StaticAnalysisResult | null,
): void {
  if (options.json) {
    return; // JSON output handled separately
  }

  console.log(`${colors.bright}${colors.blue}📊 XcodeBuildMCP Tools Summary${colors.reset}`);
  console.log('═'.repeat(60));

  if (runtimeData) {
    console.log(`${colors.green}🚀 Runtime Analysis:${colors.reset}`);
    console.log(`   Tools: ${runtimeData.toolCount}`);
    console.log(`   Resources: ${runtimeData.resourceCount}`);
    console.log(`   Total: ${runtimeData.toolCount + runtimeData.resourceCount}`);
    console.log();
  }

  if (staticData) {
    console.log(`${colors.cyan}📁 Static Analysis:${colors.reset}`);
    console.log(`   Workflow directories: ${staticData.stats.workflowCount}`);
    console.log(`   Canonical tools: ${staticData.stats.canonicalTools}`);
    console.log(`   Re-export files: ${staticData.stats.reExportTools}`);
    console.log(`   Total tool files: ${staticData.stats.totalTools}`);
    console.log();
  }
}

/**
 * Display workflow information
 */
function displayWorkflows(staticData: StaticAnalysisResult | null): void {
  if (!options.workflows || !staticData || options.json) return;

  console.log(`${colors.bright}📂 Workflow Directories:${colors.reset}`);
  console.log('─'.repeat(40));

  for (const workflow of staticData.workflows) {
    const totalTools = workflow.toolCount;
    console.log(`${colors.green}• ${workflow.displayName}${colors.reset} (${totalTools} tools)`);

    if (options.verbose) {
      const canonicalTools = workflow.tools.filter((t) => t.isCanonical).map((t) => t.name);
      const reExportTools = workflow.tools.filter((t) => !t.isCanonical).map((t) => t.name);

      if (canonicalTools.length > 0) {
        console.log(`  ${colors.cyan}Canonical:${colors.reset} ${canonicalTools.join(', ')}`);
      }
      if (reExportTools.length > 0) {
        console.log(`  ${colors.yellow}Re-exports:${colors.reset} ${reExportTools.join(', ')}`);
      }
    }
  }
  console.log();
}

/**
 * Display tool lists
 */
function displayTools(
  runtimeData: RuntimeData | null,
  staticData: StaticAnalysisResult | null,
): void {
  if (!options.tools || options.json) return;

  if (runtimeData) {
    console.log(`${colors.bright}🛠️  Runtime Tools (${runtimeData.toolCount}):${colors.reset}`);
    console.log('─'.repeat(40));

    if (runtimeData.tools.length === 0) {
      console.log('   No tools available');
    } else {
      runtimeData.tools.forEach((tool) => {
        if (options.verbose && tool.description) {
          console.log(
            `   ${colors.green}•${colors.reset} ${colors.bright}${tool.name}${colors.reset}`,
          );
          console.log(`     ${tool.description}`);
        } else {
          console.log(`   ${colors.green}•${colors.reset} ${tool.name}`);
        }
      });
    }
    console.log();
  }

  if (staticData && options.static) {
    const canonicalTools = staticData.tools.filter((tool) => tool.isCanonical);
    console.log(`${colors.bright}📁 Static Tools (${canonicalTools.length}):${colors.reset}`);
    console.log('─'.repeat(40));

    if (canonicalTools.length === 0) {
      console.log('   No tools found');
    } else {
      canonicalTools
        .sort((a, b) => a.name.localeCompare(b.name))
        .forEach((tool) => {
          if (options.verbose) {
            console.log(
              `   ${colors.green}•${colors.reset} ${colors.bright}${tool.name}${colors.reset} (${tool.workflow})`,
            );
            console.log(`     ${tool.description}`);
            console.log(`     ${colors.cyan}${tool.relativePath}${colors.reset}`);
          } else {
            console.log(`   ${colors.green}•${colors.reset} ${tool.name}`);
          }
        });
    }
    console.log();
  }
}

/**
 * Display resource lists
 */
function displayResources(runtimeData: RuntimeData | null): void {
  if (!options.resources || !runtimeData || options.json) return;

  console.log(`${colors.bright}📚 Resources (${runtimeData.resourceCount}):${colors.reset}`);
  console.log('─'.repeat(40));

  if (runtimeData.resources.length === 0) {
    console.log('   No resources available');
  } else {
    runtimeData.resources.forEach((resource) => {
      if (options.verbose) {
        console.log(
          `   ${colors.magenta}•${colors.reset} ${colors.bright}${resource.uri}${colors.reset}`,
        );
        console.log(`     ${resource.description}`);
      } else {
        console.log(`   ${colors.magenta}•${colors.reset} ${resource.uri}`);
      }
    });
  }
  console.log();
}

/**
 * Output JSON format - matches the structure of human-readable output
 */
function outputJSON(
  runtimeData: RuntimeData | null,
  staticData: StaticAnalysisResult | null,
): void {
  const output: Record<string, unknown> = {};

  // Add summary stats (equivalent to the summary table)
  if (runtimeData) {
    output.runtime = {
      toolCount: runtimeData.toolCount,
      resourceCount: runtimeData.resourceCount,
      totalCount: runtimeData.toolCount + runtimeData.resourceCount,
    };
  }

  if (staticData) {
    output.static = {
      workflowCount: staticData.stats.workflowCount,
      canonicalTools: staticData.stats.canonicalTools,
      reExportTools: staticData.stats.reExportTools,
      totalTools: staticData.stats.totalTools,
    };
  }

  // Add detailed data only if requested
  if (options.workflows && staticData) {
    output.workflows = staticData.workflows.map((w) => ({
      name: w.displayName,
      toolCount: w.toolCount,
      canonicalCount: w.canonicalCount,
      reExportCount: w.reExportCount,
    }));
  }

  if (options.tools) {
    if (runtimeData) {
      output.runtimeTools = runtimeData.tools.map((t) => t.name);
    }
    if (staticData) {
      output.staticTools = staticData.tools
        .filter((t) => t.isCanonical)
        .map((t) => t.name)
        .sort();
    }
  }

  if (options.resources && runtimeData) {
    output.resources = runtimeData.resources.map((r) => r.uri);
  }

  console.log(JSON.stringify(output, null, 2));
}

/**
 * Main execution function
 */
async function main(): Promise<void> {
  try {
    let runtimeData: RuntimeData | null = null;
    let staticData: StaticAnalysisResult | null = null;

    // Gather data based on options
    if (options.runtime) {
      if (!options.json) {
        console.log(`${colors.cyan}🔍 Gathering runtime information...${colors.reset}`);
      }
      runtimeData = await getRuntimeInfo();
    }

    if (options.static) {
      if (!options.json) {
        console.log(`${colors.cyan}📁 Performing static analysis...${colors.reset}`);
      }
      staticData = await getStaticToolAnalysis();
    }

    // For default command or workflows option, always gather static data for workflow info
    if (options.workflows && !staticData) {
      if (!options.json) {
        console.log(`${colors.cyan}📁 Gathering workflow information...${colors.reset}`);
      }
      staticData = await getStaticToolAnalysis();
    }

    if (!options.json) {
      console.log(); // Blank line after gathering
    }

    // Handle JSON output
    if (options.json) {
      outputJSON(runtimeData, staticData);
      return;
    }

    // Display based on command
    switch (command) {
      case 'count':
      case 'c':
        displaySummary(runtimeData, staticData);
        displayWorkflows(staticData);
        break;

      case 'list':
      case 'l':
        displaySummary(runtimeData, staticData);
        displayTools(runtimeData, staticData);
        displayResources(runtimeData);
        break;

      case 'static':
      case 's':
        if (!staticData) {
          console.log(`${colors.cyan}📁 Performing static analysis...${colors.reset}\n`);
          staticData = await getStaticToolAnalysis();
        }
        displaySummary(null, staticData);
        displayWorkflows(staticData);

        if (options.verbose) {
          displayTools(null, staticData);
          const reExportTools = staticData.tools.filter((t) => !t.isCanonical);
          console.log(
            `${colors.bright}🔄 Re-export Files (${reExportTools.length}):${colors.reset}`,
          );
          console.log('─'.repeat(40));
          reExportTools.forEach((file) => {
            console.log(`   ${colors.yellow}•${colors.reset} ${file.name} (${file.workflow})`);
            console.log(`     ${file.relativePath}`);
          });
        }
        break;

      default:
        // Default case (no command) - show runtime summary with workflows
        displaySummary(runtimeData, staticData);
        displayWorkflows(staticData);
        break;
    }

    if (!options.json) {
      console.log(`${colors.green}✅ Analysis complete!${colors.reset}`);
    }
  } catch (error) {
    if (options.json) {
      console.error(
        JSON.stringify(
          {
            success: false,
            error: (error as Error).message,
            timestamp: new Date().toISOString(),
          },
          null,
          2,
        ),
      );
    } else {
      console.error(`${colors.red}❌ Error: ${(error as Error).message}${colors.reset}`);
    }
    process.exit(1);
  }
}

// Run the CLI
main();

```

--------------------------------------------------------------------------------
/docs/dev/ARCHITECTURE.md:
--------------------------------------------------------------------------------

```markdown
# XcodeBuildMCP Architecture

## Table of Contents

1. [Overview](#overview)
2. [Core Architecture](#core-architecture)
3. [Design Principles](#design-principles)
4. [Component Details](#component-details)
5. [Registration System](#registration-system)
6. [Tool Naming Conventions & Glossary](#tool-naming-conventions--glossary)
7. [Testing Architecture](#testing-architecture)
8. [Build and Deployment](#build-and-deployment)
9. [Extension Guidelines](#extension-guidelines)
10. [Performance Considerations](#performance-considerations)
11. [Security Considerations](#security-considerations)

## Overview

XcodeBuildMCP is a Model Context Protocol (MCP) server that exposes Xcode operations as tools for AI assistants. The architecture emphasizes modularity, type safety, and selective enablement to support diverse development workflows.

### High-Level Objectives

- Expose Xcode-related tools (build, test, deploy, UI automation, etc.) through MCP
- Run as a long-lived stdio-based server for LLM agents, CLIs, or editors
- Enable fine-grained, opt-in activation of individual tools or tool groups
- Support incremental builds via experimental xcodemake with xcodebuild fallback

## Core Architecture

### Runtime Flow

1. **Initialization**
   - The `xcodebuildmcp` executable, as defined in `package.json`, points to the compiled `build/index.js` which executes the main logic from `src/index.ts`.
   - Sentry initialized for error tracking (optional)
   - Version information loaded from `package.json`

2. **Server Creation**
   - MCP server created with stdio transport
   - Plugin discovery system initialized

3. **Plugin Discovery (Build-Time)**
   - A build-time script (`build-plugins/plugin-discovery.ts`) scans the `src/mcp/tools/` and `src/mcp/resources/` directories
   - It generates `src/core/generated-plugins.ts` and `src/core/generated-resources.ts` with dynamic import maps
   - This approach improves startup performance by avoiding synchronous file system scans and enables code-splitting
   - Tool code is only loaded when needed, reducing initial memory footprint

4. **Plugin & Resource Loading (Runtime)**
   - At runtime, `loadPlugins()` and `loadResources()` use the generated loaders from the previous step
   - All workflow loaders are executed at startup to register tools
   - If `XCODEBUILDMCP_ENABLED_WORKFLOWS` is set, only those workflows (plus `session-management`) are registered

5. **Tool Registration**
   - Discovered tools automatically registered with server using pre-generated maps
   - No manual registration or configuration required
   - Environment variables control workflow selection behavior

5. **Request Handling**
   - MCP client calls tool → server routes to tool handler
   - Zod validates parameters before execution
   - Tool handler uses shared utilities (build, simctl, etc.)
   - Returns standardized `ToolResponse`

6. **Response Streaming**
   - Server streams response back to client
   - Consistent error handling with `isError` flag

## Design Principles

### 1. **Plugin Autonomy**
Tools are self-contained units that export a standardized interface. They don't know about the server implementation, ensuring loose coupling and high testability.

### 2. **Pure Functions vs Stateful Components**
- Most utilities are stateless pure functions
- Stateful components (e.g., process tracking) isolated in specific tool modules
- Clear separation between computation and side effects

### 3. **Single Source of Truth**
- Version from `package.json` drives all version references
- Tool directory structure is authoritative tool source
- Environment variables provide consistent configuration interface

### 4. **Feature Isolation**
- Experimental features behind environment flags
- Optional dependencies (Sentry, xcodemake) gracefully degrade
- Tool directory structure enables workflow-specific organization

### 5. **Type Safety Throughout**
- TypeScript strict mode enabled
- Zod schemas for runtime validation
- Generic type constraints ensure compile-time safety

## Module Organization and Import Strategy

### Focused Facades Pattern

XcodeBuildMCP has migrated from a traditional "barrel file" export pattern (`src/utils/index.ts`) to a more structured **focused facades** pattern. Each distinct area of functionality within `src/utils` is exposed through its own `index.ts` file in a dedicated subdirectory.

**Example Structure:**

```
src/utils/
├── execution/
│   └── index.ts  # Facade for CommandExecutor, FileSystemExecutor
├── logging/
│   └── index.ts  # Facade for the logger
├── responses/
│   └── index.ts  # Facade for error types and response creators
├── validation/
│   └── index.ts  # Facade for validation utilities
├── axe/
│   └── index.ts  # Facade for axe UI automation helpers
├── plugin-registry/
│   └── index.ts  # Facade for plugin system utilities
├── xcodemake/
│   └── index.ts  # Facade for xcodemake utilities
├── template/
│   └── index.ts  # Facade for template management utilities
├── version/
│   └── index.ts  # Facade for version information
├── test/
│   └── index.ts  # Facade for test utilities
├── log-capture/
│   └── index.ts  # Facade for log capture utilities
└── index.ts      # Deprecated barrel file (legacy/external use only)
```

This approach offers several architectural benefits:

- **Clear Dependencies**: It makes the dependency graph explicit. Importing from `utils/execution` clearly indicates a dependency on command execution logic
- **Reduced Coupling**: Modules only import the functionality they need, reducing coupling between unrelated utility components
- **Prevention of Circular Dependencies**: It's much harder to create circular dependencies, which were a risk with the large barrel file
- **Improved Tree-Shaking**: Bundlers can more effectively eliminate unused code
- **Performance**: Eliminates loading of unused modules, reducing startup time and memory usage

### ESLint Enforcement

To maintain this architecture, an ESLint rule in `eslint.config.js` explicitly forbids importing from the deprecated barrel file within the `src/` directory.

**ESLint Rule Snippet** (`eslint.config.js`):

```javascript
'no-restricted-imports': ['error', {
  patterns: [{
    group: ['**/utils/index.js', '../utils/index.js', '../../utils/index.js', '../../../utils/index.js'],
    message: 'Barrel imports from utils/index.js are prohibited. Use focused facade imports instead (e.g., utils/logging/index.js, utils/execution/index.js).'
  }]
}],
```

This rule prevents regression to the previous barrel import pattern and ensures all new code follows the focused facade architecture.

## Component Details

### Entry Points

#### `src/index.ts`
Main server entry point responsible for:
- Sentry initialization (if enabled)
- xcodemake availability check
- Server creation and startup
- Process lifecycle management (SIGTERM, SIGINT)
- Error handling and logging

#### `src/doctor-cli.ts`
Standalone doctor tool for:
- Environment validation
- Dependency checking
- Configuration verification
- Troubleshooting assistance

### Server Layer

#### `src/server/server.ts`
MCP server wrapper providing:
- Server instance creation
- stdio transport configuration
- Request/response handling
- Error boundary implementation

### Tool Discovery System

#### `src/core/plugin-registry.ts`
Runtime plugin loading system that leverages build-time generated code:
- Uses `WORKFLOW_LOADERS` and `WORKFLOW_METADATA` maps from the generated `src/core/generated-plugins.ts` file
- `loadWorkflowGroups()` iterates through the loaders, dynamically importing each workflow module using `await loader()`
- Validates that each imported module contains the required `workflow` metadata export
- Aggregates all tools from the loaded workflows into a single map
- This system eliminates runtime file system scanning, providing significant startup performance boost

#### `src/core/plugin-types.ts`
Plugin type definitions:
- `PluginMeta` interface for plugin structure
- `WorkflowMeta` interface for workflow metadata
- `WorkflowGroup` interface for directory organization

### Tool Implementation

Each tool is implemented in TypeScript and follows a standardized pattern that separates the core business logic from the MCP handler boilerplate. This is achieved using the `createTypedTool` factory, which provides compile-time and runtime type safety.

**Standard Tool Pattern** (`src/mcp/tools/some-workflow/some_tool.ts`):

```typescript
import { z } from 'zod';
import { createTypedTool } from '../../../utils/typed-tool-factory.js';
import type { CommandExecutor } from '../../../utils/execution/index.js';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.js';
import { log } from '../../../utils/logging/index.js';
import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.js';

// 1. Define the Zod schema for parameters
const someToolSchema = z.object({
  requiredParam: z.string().describe('Description for AI'),
  optionalParam: z.boolean().optional().describe('Optional parameter'),
});

// 2. Infer the parameter type from the schema
type SomeToolParams = z.infer<typeof someToolSchema>;

// 3. Implement the core logic in a separate, testable function
// This function receives strongly-typed parameters and an injected executor.
export async function someToolLogic(
  params: SomeToolParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  log('info', `Executing some_tool with param: ${params.requiredParam}`);

  try {
    const result = await executor(['some', 'command'], 'Some Tool Operation');

    if (!result.success) {
      return createErrorResponse('Operation failed', result.error);
    }

    return createTextResponse(`✅ Success: ${result.output}`);
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    return createErrorResponse('Tool execution failed', errorMessage);
  }
}

// 4. Export the tool definition for auto-discovery
export default {
  name: 'some_tool',
  description: 'Tool description for AI agents. Example: some_tool({ requiredParam: "value" })',
  schema: someToolSchema.shape, // Expose shape for MCP SDK

  // 5. Create the handler using the type-safe factory
  handler: createTypedTool(
    someToolSchema,
    someToolLogic,
    getDefaultCommandExecutor,
  ),
};
```

This pattern ensures that:
- The `someToolLogic` function is highly testable via dependency injection
- Zod handles all runtime parameter validation automatically
- The handler is type-safe, preventing unsafe access to parameters
- Import paths use focused facades for clear dependency management
```

### Debugger Subsystem

The debugging workflow relies on a long-lived, interactive LLDB subprocess. A `DebuggerManager` owns the session lifecycle and routes tool calls to a backend implementation. The default backend is the LLDB CLI (`xcrun lldb --no-lldbinit`) and configures a unique prompt sentinel to safely read command results. A stub DAP backend exists for future expansion.

Key elements:
- **Interactive execution**: Uses a dedicated interactive spawner with `stdin: 'pipe'` so LLDB commands can be streamed across multiple tool calls.
- **Session manager**: Tracks debug session metadata (session id, simulator id, pid, timestamps) and maintains a “current” session.
- **Backend abstraction**: `DebuggerBackend` keeps the tool contract stable while allowing future DAP support.

### MCP Resources System

XcodeBuildMCP provides dual interfaces: traditional MCP tools and efficient MCP resources for supported clients. Resources are located in `src/mcp/resources/` and are automatically discovered **at build time**. The build process generates `src/core/generated-resources.ts`, which contains dynamic loaders for each resource, improving startup performance. For more details on creating resources, see the [Plugin Development Guide](PLUGIN_DEVELOPMENT.md).

#### Resource Architecture

```
src/mcp/resources/
├── simulators.ts           # Simulator data resource
└── __tests__/              # Resource-specific tests
```

#### Client Capability Detection

The system automatically detects client MCP capabilities:

```typescript
// src/core/resources.ts
export function supportsResources(server?: unknown): boolean {
  // Detects client capabilities via getClientCapabilities()
  // Conservative fallback: assumes resource support
}
```

#### Resource Implementation Pattern

Resources can reuse existing tool logic for consistency:

```typescript
// src/mcp/resources/some_resource.ts
import { log } from '../../utils/logging/index.js';
import { getDefaultCommandExecutor, CommandExecutor } from '../../utils/execution/index.js';
import { getSomeResourceLogic } from '../tools/some-workflow/get_some_resource.js';

// Testable resource logic separated from MCP handler
export async function someResourceResourceLogic(
  executor: CommandExecutor = getDefaultCommandExecutor(),
): Promise<{ contents: Array<{ text: string }> }> {
  try {
    log('info', 'Processing some resource request');

    const result = await getSomeResourceLogic({}, executor);

    if (result.isError) {
      const errorText = result.content[0]?.text;
      throw new Error(
        typeof errorText === 'string' ? errorText : 'Failed to retrieve some resource data',
      );
    }

    return {
      contents: [
        {
          text:
            typeof result.content[0]?.text === 'string'
              ? result.content[0].text
              : 'No data for that resource is available',
        },
      ],
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error in some_resource resource handler: ${errorMessage}`);

    return {
      contents: [
        {
          text: `Error retrieving resource data: ${errorMessage}`,
        },
      ],
    };
  }
}

export default {
  uri: 'xcodebuildmcp://some_resource',
  name: 'some_resource',
  description: 'Returns some resource information',
  mimeType: 'text/plain',
  async handler(_uri: URL): Promise<{ contents: Array<{ text: string }> }> {
    return someResourceResourceLogic();
  },
};
```

## Registration System

XcodeBuildMCP registers tools at startup using the generated workflow loaders. Tool selection can be narrowed using the `XCODEBUILDMCP_ENABLED_WORKFLOWS` environment variable.

### Full Registration (Default)

- **Environment**: `XCODEBUILDMCP_ENABLED_WORKFLOWS` is not set.
- **Behavior**: All available tools are loaded and registered with the MCP server at startup.
- **Use Case**: Use this mode when you want the full suite of tools immediately available.

### Selective Workflow Registration

- **Environment**: `XCODEBUILDMCP_ENABLED_WORKFLOWS=simulator,device,project-discovery` (comma-separated)
- **Behavior**: Only tools from the selected workflows are registered, plus the required `session-management` workflow.
- **Use Case**: Use this mode to reduce tool surface area for focused workflows.

## Tool Naming Conventions & Glossary

Tools follow a consistent naming pattern to ensure predictability and clarity. Understanding this convention is crucial for both using and developing tools.

### Naming Pattern

The standard naming convention for tools is:

`{action}_{target}_{specifier}_{projectType}`

- **action**: The primary verb describing the tool's function (e.g., `build`, `test`, `get`, `list`).
- **target**: The main subject of the action (e.g., `sim` for simulator, `dev` for device, `mac` for macOS).
- **specifier**: A variant that specifies *how* the target is identified (e.g., `id` for UUID, `name` for by-name).
- **projectType**: The type of Xcode project the tool operates on (e.g., `ws` for workspace, `proj` for project).

Not all parts are required for every tool. For example, `swift_package_build` has an action and a target, but no specifier or project type.

### Examples

- `build_sim_id_ws`: **Build** for a **simulator** identified by its **ID (UUID)** from a **workspace**.
- `test_dev_proj`: **Test** on a **device** from a **project**.
- `get_mac_app_path_ws`: **Get** the app path for a **macOS** application from a **workspace**.
- `list_sims`: **List** all **simulators**.

### Glossary

| Term/Abbreviation | Meaning | Description |
|---|---|---|
| `ws` | Workspace | Refers to an `.xcworkspace` file. Used for projects with multiple `.xcodeproj` files or dependencies managed by CocoaPods or SPM. |
| `proj` | Project | Refers to an `.xcodeproj` file. Used for single-project setups. |
| `sim` | Simulator | Refers to the iOS, watchOS, tvOS, or visionOS simulator. |
| `dev` | Device | Refers to a physical Apple device (iPhone, iPad, etc.). |
| `mac` | macOS | Refers to a native macOS application target. |
| `id` | Identifier | Refers to the unique identifier (UUID/UDID) of a simulator or device. |
| `name` | Name | Refers to the human-readable name of a simulator (e.g., "iPhone 15 Pro"). |
| `cap` | Capture | Used in logging tools, e.g., `start_sim_log_cap`. |

## Testing Architecture

### Framework and Configuration

- **Test Runner**: Vitest 3.x
- **Environment**: Node.js
- **Configuration**: `vitest.config.ts`
- **Test Pattern**: `*.test.ts` files alongside implementation

### Testing Principles

XcodeBuildMCP uses a strict **Dependency Injection (DI)** pattern for testing, which completely bans the use of traditional mocking libraries like Vitest's `vi.mock` or `vi.fn`. This ensures that tests are robust, maintainable, and verify the actual integration between components.

For detailed guidelines, see the [Testing Guide](TESTING.md).

### Test Structure Example

Tests inject mock "executors" for external interactions like command-line execution or file system access. This allows for deterministic testing of tool logic without mocking the implementation itself. The project provides helper functions like `createMockExecutor` and `createMockFileSystemExecutor` in `src/test-utils/mock-executors.ts` to facilitate this pattern.

```typescript
import { describe, it, expect } from 'vitest';
import { someToolLogic } from '../tool-file.js'; // Import the logic function
import { createMockExecutor } from '../../../test-utils/mock-executors.js';

describe('Tool Name', () => {
  it('should execute successfully', async () => {
    // 1. Create a mock executor to simulate command-line results
    const mockExecutor = createMockExecutor({
      success: true,
      output: 'Command output'
    });

    // 2. Call the tool's logic function, injecting the mock executor
    const result = await someToolLogic({ requiredParam: 'value' }, mockExecutor);

    // 3. Assert the final result
    expect(result).toEqual({
      content: [{ type: 'text', text: 'Expected output' }],
      isError: false
    });
  });
});
```

## Build and Deployment

### Build Process

1. **Version Generation**
   ```bash
   npm run build
   ```
   - Reads version from `package.json`
   - Generates `src/version.ts`

2. **Plugin & Resource Loader Generation**
   - The `build-plugins/plugin-discovery.ts` script is executed
   - It scans `src/mcp/tools/` and `src/mcp/resources/` to find all workflows and resources
   - It generates `src/core/generated-plugins.ts` and `src/core/generated-resources.ts` with dynamic import maps
   - This eliminates runtime file system scanning and enables code-splitting

3. **TypeScript Compilation**
   - `tsup` compiles the TypeScript source, including the newly generated files, into JavaScript
   - Compiles TypeScript with tsup

4. **Build Configuration** (`tsup.config.ts`)
   - Entry points: `index.ts`, `doctor-cli.ts`
   - Output format: ESM
   - Target: Node 18+
   - Source maps enabled

5. **Distribution Structure**
   ```
   build/
   ├── index.js          # Main server executable
   ├── doctor-cli.js # Doctor tool
   └── *.js.map         # Source maps
   ```

### Smithery entrypoint

Smithery deployments build from source using the Smithery CLI. The CLI discovers the TypeScript entrypoint from `package.json#module`, so this repository intentionally points `module` to `src/smithery.ts`. The npm runtime entrypoint is defined by `exports` and targets the compiled `build/` output.

### npm Package

- **Name**: `xcodebuildmcp`
- **Executables**:
  - `xcodebuildmcp` → Main server
  - `xcodebuildmcp-doctor` → Doctor tool
- **Dependencies**: Minimal runtime dependencies
- **Platform**: macOS only (due to Xcode requirement)

### Bundled Resources

```
bundled/
├── axe              # UI automation binary
└── Frameworks/      # Facebook device frameworks
    ├── FBControlCore.framework
    ├── FBDeviceControl.framework
    └── FBSimulatorControl.framework
```

## Extension Guidelines

This project is designed to be extensible. For comprehensive instructions on creating new tools, workflow groups, and resources, please refer to the dedicated [**Plugin Development Guide**](PLUGIN_DEVELOPMENT.md).

The guide covers:
- The auto-discovery system architecture.
- The dependency injection pattern required for all new tools.
- How to organize tools into workflow groups.
- Testing guidelines and patterns.

## Performance Considerations

### Startup Performance

- **Build-Time Plugin Discovery**: The server avoids expensive and slow file system scans at startup by using pre-generated loader maps. This is the single most significant performance optimization
- **Code-Splitting**: Workflow modules are loaded via dynamic imports when registration occurs, reducing the initial memory footprint and parse time
- **Focused Facades**: Using targeted imports instead of a large barrel file improves module resolution speed for the Node.js runtime
- **Lazy Loading**: Tools only initialized when registered
- **Selective Registration**: Fewer tools = faster startup
- **Minimal Dependencies**: Fast module resolution

### Runtime Performance

- **Stateless Operations**: Most tools complete quickly
- **Process Management**: Long-running processes tracked separately
- **Incremental Builds**: xcodemake provides significant speedup
- **Parallel Execution**: Tools can run concurrently

### Memory Management

- **Process Cleanup**: Proper process termination handling
- **Log Rotation**: Captured logs have size limits
- **Resource Disposal**: Explicit cleanup in lifecycle hooks

### Optimization Strategies

1. **Use Tool Groups**: Enable only needed workflows
2. **Enable Incremental Builds**: Set `INCREMENTAL_BUILDS_ENABLED=true`
3. **Limit Log Capture**: Use structured logging when possible

## Security Considerations

### Input Validation

- All tool inputs validated with Zod schemas
- Command injection prevented via proper escaping
- Path traversal protection in file operations

### Process Isolation

- Tools run with user permissions
- No privilege escalation
- Sandboxed execution environment

### Error Handling

- Sensitive information scrubbed from errors
- Stack traces limited to application code
- Sentry integration respects privacy settings

```

--------------------------------------------------------------------------------
/src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Vitest test for scaffold_ios_project plugin
 *
 * Tests the plugin structure and iOS scaffold tool functionality
 * including parameter validation, file operations, template processing, and response formatting.
 *
 * Plugin location: plugins/utilities/scaffold_ios_project.js
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as z from 'zod';
import scaffoldIosProject, { scaffold_ios_projectLogic } from '../scaffold_ios_project.ts';
import {
  createMockExecutor,
  createMockFileSystemExecutor,
} from '../../../../test-utils/mock-executors.ts';

describe('scaffold_ios_project plugin', () => {
  let mockCommandExecutor: any;
  let mockFileSystemExecutor: any;
  let originalEnv: string | undefined;

  beforeEach(() => {
    // Create mock executor using approved utility
    mockCommandExecutor = createMockExecutor({
      success: true,
      output: 'Command executed successfully',
    });

    mockFileSystemExecutor = createMockFileSystemExecutor({
      existsSync: (path) => {
        // Mock template directories exist but project files don't
        return (
          path.includes('xcodebuild-mcp-template') ||
          path.includes('XcodeBuildMCP-iOS-Template') ||
          path.includes('/template') ||
          path.endsWith('template') ||
          path.includes('extracted') ||
          path.includes('/mock/template/path')
        );
      },
      readFile: async () => 'template content with MyProject placeholder',
      readdir: async () => [
        { name: 'Package.swift', isDirectory: () => false, isFile: () => true } as any,
        { name: 'MyProject.swift', isDirectory: () => false, isFile: () => true } as any,
      ],
      mkdir: async () => {},
      rm: async () => {},
      cp: async () => {},
      writeFile: async () => {},
      stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }),
    });

    // Store original environment for cleanup
    originalEnv = process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH;
    // Set local template path to avoid download and chdir issues
    process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH = '/mock/template/path';
  });

  afterEach(() => {
    // Restore original environment
    if (originalEnv !== undefined) {
      process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH = originalEnv;
    } else {
      delete process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH;
    }
  });

  describe('Export Field Validation (Literal)', () => {
    it('should have correct name field', () => {
      expect(scaffoldIosProject.name).toBe('scaffold_ios_project');
    });

    it('should have correct description field', () => {
      expect(scaffoldIosProject.description).toBe(
        'Scaffold a new iOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper iOS configuration.',
      );
    });

    it('should have handler as function', () => {
      expect(typeof scaffoldIosProject.handler).toBe('function');
    });

    it('should have valid schema with required fields', () => {
      const schema = z.object(scaffoldIosProject.schema);

      // Test valid input
      expect(
        schema.safeParse({
          projectName: 'MyTestApp',
          outputPath: '/path/to/output',
          bundleIdentifier: 'com.test.myapp',
          displayName: 'My Test App',
          marketingVersion: '1.0',
          currentProjectVersion: '1',
          customizeNames: true,
          deploymentTarget: '18.4',
          targetedDeviceFamily: ['iphone', 'ipad'],
          supportedOrientations: ['portrait', 'landscape-left'],
          supportedOrientationsIpad: ['portrait', 'landscape-left', 'landscape-right'],
        }).success,
      ).toBe(true);

      // Test minimal valid input
      expect(
        schema.safeParse({
          projectName: 'MyTestApp',
          outputPath: '/path/to/output',
        }).success,
      ).toBe(true);

      // Test invalid input - missing projectName
      expect(
        schema.safeParse({
          outputPath: '/path/to/output',
        }).success,
      ).toBe(false);

      // Test invalid input - missing outputPath
      expect(
        schema.safeParse({
          projectName: 'MyTestApp',
        }).success,
      ).toBe(false);

      // Test invalid input - wrong type for customizeNames
      expect(
        schema.safeParse({
          projectName: 'MyTestApp',
          outputPath: '/path/to/output',
          customizeNames: 'true',
        }).success,
      ).toBe(false);

      // Test invalid input - wrong enum value for targetedDeviceFamily
      expect(
        schema.safeParse({
          projectName: 'MyTestApp',
          outputPath: '/path/to/output',
          targetedDeviceFamily: ['invalid-device'],
        }).success,
      ).toBe(false);

      // Test invalid input - wrong enum value for supportedOrientations
      expect(
        schema.safeParse({
          projectName: 'MyTestApp',
          outputPath: '/path/to/output',
          supportedOrientations: ['invalid-orientation'],
        }).success,
      ).toBe(false);
    });
  });

  describe('Command Generation Tests', () => {
    it('should generate correct curl command for iOS template download', async () => {
      // Temporarily disable local template to force download
      delete process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH;

      // Track commands executed
      let capturedCommands: string[][] = [];
      const trackingCommandExecutor = createMockExecutor({
        success: true,
        output: 'Command executed successfully',
      });
      // Wrap to capture commands
      const capturingExecutor = async (command: string[], ...args: any[]) => {
        capturedCommands.push(command);
        return trackingCommandExecutor(command, ...args);
      };

      await scaffold_ios_projectLogic(
        {
          projectName: 'TestIOSApp',
          customizeNames: true,
          outputPath: '/tmp/test-projects',
        },
        capturingExecutor,
        mockFileSystemExecutor,
      );

      // Verify curl command was executed
      const curlCommand = capturedCommands.find((cmd) => cmd.includes('curl'));
      expect(curlCommand).toBeDefined();
      expect(curlCommand).toEqual([
        'curl',
        '-L',
        '-f',
        '-o',
        expect.stringMatching(/template\.zip$/),
        expect.stringMatching(
          /https:\/\/github\.com\/cameroncooke\/XcodeBuildMCP-iOS-Template\/releases\/download\/v\d+\.\d+\.\d+\/XcodeBuildMCP-iOS-Template-\d+\.\d+\.\d+\.zip/,
        ),
      ]);

      // Restore environment variable
      process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH = '/mock/template/path';
    });

    it.skip('should generate correct unzip command for iOS template extraction', async () => {
      // Temporarily disable local template to force download
      delete process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH;

      // Create a mock that returns false for local template paths to force download
      const downloadMockFileSystemExecutor = createMockFileSystemExecutor({
        existsSync: (path) => {
          // Only return true for extracted template directories, false for local template paths
          return (
            path.includes('xcodebuild-mcp-template') ||
            path.includes('XcodeBuildMCP-iOS-Template') ||
            path.includes('extracted')
          );
        },
        readFile: async () => 'template content with MyProject placeholder',
        readdir: async () => [
          { name: 'Package.swift', isDirectory: () => false, isFile: () => true } as any,
          { name: 'MyProject.swift', isDirectory: () => false, isFile: () => true } as any,
        ],
        mkdir: async () => {},
        rm: async () => {},
        cp: async () => {},
        writeFile: async () => {},
        stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }),
      });

      // Track commands executed
      let capturedCommands: string[][] = [];
      const trackingCommandExecutor = createMockExecutor({
        success: true,
        output: 'Command executed successfully',
      });
      // Wrap to capture commands
      const capturingExecutor = async (command: string[], ...args: any[]) => {
        capturedCommands.push(command);
        return trackingCommandExecutor(command, ...args);
      };

      await scaffold_ios_projectLogic(
        {
          projectName: 'TestIOSApp',
          customizeNames: true,
          outputPath: '/tmp/test-projects',
        },
        capturingExecutor,
        downloadMockFileSystemExecutor,
      );

      // Verify unzip command was executed
      const unzipCommand = capturedCommands.find((cmd) => cmd.includes('unzip'));
      expect(unzipCommand).toBeDefined();
      expect(unzipCommand).toEqual(['unzip', '-q', expect.stringMatching(/template\.zip$/)]);

      // Restore environment variable
      process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH = '/mock/template/path';
    });

    it('should generate correct commands when using custom template version', async () => {
      // Temporarily disable local template to force download
      delete process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH;

      // Set custom template version
      const originalVersion = process.env.XCODEBUILD_MCP_IOS_TEMPLATE_VERSION;
      process.env.XCODEBUILD_MCP_IOS_TEMPLATE_VERSION = 'v2.0.0';

      // Track commands executed
      let capturedCommands: string[][] = [];
      const trackingCommandExecutor = createMockExecutor({
        success: true,
        output: 'Command executed successfully',
      });
      // Wrap to capture commands
      const capturingExecutor = async (command: string[], ...args: any[]) => {
        capturedCommands.push(command);
        return trackingCommandExecutor(command, ...args);
      };

      await scaffold_ios_projectLogic(
        {
          projectName: 'TestIOSApp',
          customizeNames: true,
          outputPath: '/tmp/test-projects',
        },
        capturingExecutor,
        mockFileSystemExecutor,
      );

      // Verify curl command uses custom version
      const curlCommand = capturedCommands.find((cmd) => cmd.includes('curl'));
      expect(curlCommand).toBeDefined();
      expect(curlCommand).toEqual([
        'curl',
        '-L',
        '-f',
        '-o',
        expect.stringMatching(/template\.zip$/),
        'https://github.com/cameroncooke/XcodeBuildMCP-iOS-Template/releases/download/v2.0.0/XcodeBuildMCP-iOS-Template-2.0.0.zip',
      ]);

      // Restore original version
      if (originalVersion) {
        process.env.XCODEBUILD_MCP_IOS_TEMPLATE_VERSION = originalVersion;
      } else {
        delete process.env.XCODEBUILD_MCP_IOS_TEMPLATE_VERSION;
      }

      // Restore environment variable
      process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH = '/mock/template/path';
    });

    it.skip('should generate correct commands with no command executor passed', async () => {
      // Temporarily disable local template to force download
      delete process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH;

      // Create a mock that returns false for local template paths to force download
      const downloadMockFileSystemExecutor = createMockFileSystemExecutor({
        existsSync: (path) => {
          // Only return true for extracted template directories, false for local template paths
          return (
            path.includes('xcodebuild-mcp-template') ||
            path.includes('XcodeBuildMCP-iOS-Template') ||
            path.includes('extracted')
          );
        },
        readFile: async () => 'template content with MyProject placeholder',
        readdir: async () => [
          { name: 'Package.swift', isDirectory: () => false, isFile: () => true } as any,
          { name: 'MyProject.swift', isDirectory: () => false, isFile: () => true } as any,
        ],
        mkdir: async () => {},
        rm: async () => {},
        cp: async () => {},
        writeFile: async () => {},
        stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }),
      });

      // Track commands executed - using default executor path
      let capturedCommands: string[][] = [];
      const trackingCommandExecutor = createMockExecutor({
        success: true,
        output: 'Command executed successfully',
      });
      // Wrap to capture commands
      const capturingExecutor = async (command: string[], ...args: any[]) => {
        capturedCommands.push(command);
        return trackingCommandExecutor(command, ...args);
      };

      await scaffold_ios_projectLogic(
        {
          projectName: 'TestIOSApp',
          customizeNames: true,
          outputPath: '/tmp/test-projects',
        },
        capturingExecutor,
        downloadMockFileSystemExecutor,
      );

      // Verify both curl and unzip commands were executed in sequence
      expect(capturedCommands.length).toBeGreaterThanOrEqual(2);

      const curlCommand = capturedCommands.find((cmd) => cmd.includes('curl'));
      const unzipCommand = capturedCommands.find((cmd) => cmd.includes('unzip'));

      expect(curlCommand).toBeDefined();
      expect(unzipCommand).toBeDefined();
      if (!curlCommand || !unzipCommand) {
        throw new Error('Expected curl and unzip commands to be captured');
      }
      expect(curlCommand[0]).toBe('curl');
      expect(unzipCommand[0]).toBe('unzip');

      // Restore environment variable
      process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH = '/mock/template/path';
    });
  });

  describe('Handler Behavior (Complete Literal Returns)', () => {
    it('should return success response for valid scaffold iOS project request', async () => {
      const result = await scaffold_ios_projectLogic(
        {
          projectName: 'TestIOSApp',
          customizeNames: true,
          outputPath: '/tmp/test-projects',
          bundleIdentifier: 'com.test.iosapp',
        },
        mockCommandExecutor,
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify(
              {
                success: true,
                projectPath: '/tmp/test-projects',
                platform: 'iOS',
                message: 'Successfully scaffolded iOS project "TestIOSApp" in /tmp/test-projects',
                nextSteps: [
                  'Important: Before working on the project make sure to read the README.md file in the workspace root directory.',
                  'Build for simulator: build_sim({ workspacePath: "/tmp/test-projects/TestIOSApp.xcworkspace", scheme: "TestIOSApp", simulatorName: "iPhone 16" })',
                  'Build and run on simulator: build_run_sim({ workspacePath: "/tmp/test-projects/TestIOSApp.xcworkspace", scheme: "TestIOSApp", simulatorName: "iPhone 16" })',
                ],
              },
              null,
              2,
            ),
          },
        ],
      });
    });

    it('should return success response with all optional parameters', async () => {
      const result = await scaffold_ios_projectLogic(
        {
          projectName: 'TestIOSApp',
          customizeNames: true,
          outputPath: '/tmp/test-projects',
          bundleIdentifier: 'com.test.iosapp',
          displayName: 'Test iOS App',
          marketingVersion: '2.0',
          currentProjectVersion: '5',
          deploymentTarget: '17.0',
          targetedDeviceFamily: ['iphone'],
          supportedOrientations: ['portrait'],
          supportedOrientationsIpad: ['portrait', 'landscape-left'],
        },
        mockCommandExecutor,
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify(
              {
                success: true,
                projectPath: '/tmp/test-projects',
                platform: 'iOS',
                message: 'Successfully scaffolded iOS project "TestIOSApp" in /tmp/test-projects',
                nextSteps: [
                  'Important: Before working on the project make sure to read the README.md file in the workspace root directory.',
                  'Build for simulator: build_sim({ workspacePath: "/tmp/test-projects/TestIOSApp.xcworkspace", scheme: "TestIOSApp", simulatorName: "iPhone 16" })',
                  'Build and run on simulator: build_run_sim({ workspacePath: "/tmp/test-projects/TestIOSApp.xcworkspace", scheme: "TestIOSApp", simulatorName: "iPhone 16" })',
                ],
              },
              null,
              2,
            ),
          },
        ],
      });
    });

    it('should return success response with customizeNames false', async () => {
      const result = await scaffold_ios_projectLogic(
        {
          projectName: 'TestIOSApp',
          outputPath: '/tmp/test-projects',
          customizeNames: false,
        },
        mockCommandExecutor,
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify(
              {
                success: true,
                projectPath: '/tmp/test-projects',
                platform: 'iOS',
                message: 'Successfully scaffolded iOS project "TestIOSApp" in /tmp/test-projects',
                nextSteps: [
                  'Important: Before working on the project make sure to read the README.md file in the workspace root directory.',
                  'Build for simulator: build_sim({ workspacePath: "/tmp/test-projects/MyProject.xcworkspace", scheme: "MyProject", simulatorName: "iPhone 16" })',
                  'Build and run on simulator: build_run_sim({ workspacePath: "/tmp/test-projects/MyProject.xcworkspace", scheme: "MyProject", simulatorName: "iPhone 16" })',
                ],
              },
              null,
              2,
            ),
          },
        ],
      });
    });

    it('should return error response for invalid project name', async () => {
      const result = await scaffold_ios_projectLogic(
        {
          projectName: '123InvalidName',
          customizeNames: true,
          outputPath: '/tmp/test-projects',
        },
        mockCommandExecutor,
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify(
              {
                success: false,
                error:
                  'Project name must start with a letter and contain only letters, numbers, and underscores',
              },
              null,
              2,
            ),
          },
        ],
        isError: true,
      });
    });

    it('should return error response for existing project files', async () => {
      // Update mock to return true for existing files
      mockFileSystemExecutor = createMockFileSystemExecutor({
        existsSync: () => true,
        readFile: async () => 'template content with MyProject placeholder',
        readdir: async () => [
          { name: 'Package.swift', isDirectory: () => false, isFile: () => true } as any,
          { name: 'MyProject.swift', isDirectory: () => false, isFile: () => true } as any,
        ],
      });

      const result = await scaffold_ios_projectLogic(
        {
          projectName: 'TestIOSApp',
          customizeNames: true,
          outputPath: '/tmp/test-projects',
        },
        mockCommandExecutor,
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify(
              {
                success: false,
                error: 'Xcode project files already exist in /tmp/test-projects',
              },
              null,
              2,
            ),
          },
        ],
        isError: true,
      });
    });

    it('should return error response for template download failure', async () => {
      // Temporarily disable local template to force download
      delete process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH;

      // Mock command executor to fail for curl commands
      const failingMockCommandExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'Template download failed',
      });

      const result = await scaffold_ios_projectLogic(
        {
          projectName: 'TestIOSApp',
          customizeNames: true,
          outputPath: '/tmp/test-projects',
        },
        failingMockCommandExecutor,
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify(
              {
                success: false,
                error:
                  'Failed to get template for iOS: Failed to download template: Template download failed',
              },
              null,
              2,
            ),
          },
        ],
        isError: true,
      });

      // Restore environment variable
      process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH = '/mock/template/path';
    });

    it.skip('should return error response for template extraction failure', async () => {
      // Temporarily disable local template to force download
      delete process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH;

      // Create a mock that returns false for local template paths to force download
      const downloadMockFileSystemExecutor = createMockFileSystemExecutor({
        existsSync: (path) => {
          // Only return true for extracted template directories, false for local template paths
          return (
            path.includes('xcodebuild-mcp-template') ||
            path.includes('XcodeBuildMCP-iOS-Template') ||
            path.includes('extracted')
          );
        },
        readFile: async () => 'template content with MyProject placeholder',
        readdir: async () => [
          { name: 'Package.swift', isDirectory: () => false, isFile: () => true } as any,
          { name: 'MyProject.swift', isDirectory: () => false, isFile: () => true } as any,
        ],
        mkdir: async () => {},
        rm: async () => {},
        cp: async () => {},
        writeFile: async () => {},
        stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }),
      });

      // Mock command executor to fail for unzip commands
      const failingMockCommandExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'Extraction failed',
      });

      const result = await scaffold_ios_projectLogic(
        {
          projectName: 'TestIOSApp',
          customizeNames: true,
          outputPath: '/tmp/test-projects',
        },
        failingMockCommandExecutor,
        downloadMockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify(
              {
                success: false,
                error:
                  'Failed to get template for iOS: Failed to extract template: Extraction failed',
              },
              null,
              2,
            ),
          },
        ],
        isError: true,
      });

      // Restore environment variable
      process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH = '/mock/template/path';
    });
  });
});

```

--------------------------------------------------------------------------------
/src/mcp/tools/ui-testing/__tests__/touch.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tests for touch tool plugin
 * Following CLAUDE.md testing standards with dependency injection
 */

import { describe, it, expect, beforeEach } from 'vitest';
import * as z from 'zod';
import { createMockExecutor, mockProcess } from '../../../../test-utils/mock-executors.ts';
import { sessionStore } from '../../../../utils/session-store.ts';
import touchPlugin, { touchLogic } from '../touch.ts';

describe('Touch Plugin', () => {
  beforeEach(() => {
    sessionStore.clear();
  });

  describe('Export Field Validation (Literal)', () => {
    it('should have correct name', () => {
      expect(touchPlugin.name).toBe('touch');
    });

    it('should have correct description', () => {
      expect(touchPlugin.description).toBe(
        "Perform touch down/up events at specific coordinates. Use describe_ui for precise coordinates (don't guess from screenshots).",
      );
    });

    it('should have handler function', () => {
      expect(typeof touchPlugin.handler).toBe('function');
    });

    it('should validate schema fields with safeParse', () => {
      const schema = z.object(touchPlugin.schema);

      expect(
        schema.safeParse({
          x: 100,
          y: 200,
          down: true,
        }).success,
      ).toBe(true);

      expect(
        schema.safeParse({
          x: 100,
          y: 200,
          up: true,
        }).success,
      ).toBe(true);

      expect(
        schema.safeParse({
          x: 100.5,
          y: 200,
          down: true,
        }).success,
      ).toBe(false);

      expect(
        schema.safeParse({
          x: 100,
          y: 200.5,
          down: true,
        }).success,
      ).toBe(false);

      expect(
        schema.safeParse({
          x: 100,
          y: 200,
          down: true,
          delay: -1,
        }).success,
      ).toBe(false);

      const withSimId = schema.safeParse({
        simulatorId: '12345678-1234-4234-8234-123456789012',
        x: 100,
        y: 200,
        down: true,
      });
      expect(withSimId.success).toBe(true);
      expect('simulatorId' in (withSimId.data as Record<string, unknown>)).toBe(false);
    });
  });

  describe('Handler Requirements', () => {
    it('should require simulatorId session default', async () => {
      const result = await touchPlugin.handler({
        x: 100,
        y: 200,
        down: true,
      });

      expect(result.isError).toBe(true);
      const message = result.content[0].text;
      expect(message).toContain('Missing required session defaults');
      expect(message).toContain('simulatorId is required');
      expect(message).toContain('session-set-defaults');
    });

    it('should surface parameter validation errors when defaults exist', async () => {
      sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });

      const result = await touchPlugin.handler({
        y: 200,
        down: true,
      });

      expect(result.isError).toBe(true);
      const message = result.content[0].text;
      expect(message).toContain('Parameter validation failed');
      expect(message).toContain('x: Invalid input: expected number, received undefined');
    });
  });

  describe('Command Generation', () => {
    it('should generate correct axe command for touch down', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'touch completed',
          error: undefined,
          process: mockProcess,
        };
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text',
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      await touchLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          down: true,
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'touch',
        '-x',
        '100',
        '-y',
        '200',
        '--down',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });

    it('should generate correct axe command for touch up', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'touch completed',
          error: undefined,
          process: mockProcess,
        };
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text',
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      await touchLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 150,
          y: 250,
          up: true,
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'touch',
        '-x',
        '150',
        '-y',
        '250',
        '--up',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });

    it('should generate correct axe command for touch down+up', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'touch completed',
          error: undefined,
          process: mockProcess,
        };
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text',
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      await touchLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 300,
          y: 400,
          down: true,
          up: true,
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'touch',
        '-x',
        '300',
        '-y',
        '400',
        '--down',
        '--up',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });

    it('should generate correct axe command for touch with delay', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'touch completed',
          error: undefined,
          process: mockProcess,
        };
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text',
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      await touchLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 50,
          y: 75,
          down: true,
          up: true,
          delay: 1.5,
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'touch',
        '-x',
        '50',
        '-y',
        '75',
        '--down',
        '--up',
        '--delay',
        '1.5',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });

    it('should generate correct axe command with bundled axe path', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'touch completed',
          error: undefined,
          process: mockProcess,
        };
      };

      const mockAxeHelpers = {
        getAxePath: () => '/path/to/bundled/axe',
        getBundledAxeEnvironment: () => ({ AXE_PATH: '/some/path' }),
      };

      await touchLogic(
        {
          simulatorId: 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF',
          x: 0,
          y: 0,
          up: true,
          delay: 0.5,
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/path/to/bundled/axe',
        'touch',
        '-x',
        '0',
        '-y',
        '0',
        '--up',
        '--delay',
        '0.5',
        '--udid',
        'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF',
      ]);
    });
  });

  describe('Handler Behavior (Complete Literal Returns)', () => {
    it('should handle axe dependency error', async () => {
      const mockExecutor = createMockExecutor({ success: true });
      const mockAxeHelpers = {
        getAxePath: () => null,
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text',
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await touchLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          down: true,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
          },
        ],
        isError: true,
      });
    });

    it('should successfully perform touch down', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'Touch down completed' });
      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text',
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await touchLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          down: true,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Touch event (touch down) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
          },
        ],
        isError: false,
      });
    });

    it('should successfully perform touch up', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'Touch up completed' });
      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text',
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await touchLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          up: true,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Touch event (touch up) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
          },
        ],
        isError: false,
      });
    });

    it('should return error when neither down nor up is specified', async () => {
      const mockExecutor = createMockExecutor({ success: true });

      const result = await touchLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [{ type: 'text', text: 'Error: At least one of "down" or "up" must be true' }],
        isError: true,
      });
    });

    it('should return success for touch down event', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'touch completed',
        error: undefined,
      });

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text',
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await touchLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          down: true,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Touch event (touch down) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
          },
        ],
        isError: false,
      });
    });

    it('should return success for touch up event', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'touch completed',
        error: undefined,
      });

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text',
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await touchLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          up: true,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Touch event (touch up) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
          },
        ],
        isError: false,
      });
    });

    it('should return success for touch down+up event', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'touch completed',
        error: undefined,
      });

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text',
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await touchLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          down: true,
          up: true,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Touch event (touch down+up) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
          },
        ],
        isError: false,
      });
    });

    it('should handle DependencyError when axe is not available', async () => {
      const mockExecutor = createMockExecutor({ success: true });

      const mockAxeHelpers = {
        getAxePath: () => null,
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text',
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await touchLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          down: true,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
          },
        ],
        isError: true,
      });
    });

    it('should handle AxeError from failed command execution', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'axe command failed',
      });

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text',
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await touchLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          down: true,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: "Error: Failed to execute touch event: axe command 'touch' failed.\nDetails: axe command failed",
          },
        ],
        isError: true,
      });
    });

    it('should handle SystemError from command execution', async () => {
      const mockExecutor = async () => {
        throw new Error('System error occurred');
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text',
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await touchLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          down: true,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toMatchObject({
        content: [
          {
            type: 'text',
            text: expect.stringContaining(
              'Error: System error executing axe: Failed to execute axe command: System error occurred',
            ),
          },
        ],
        isError: true,
      });
    });

    it('should handle unexpected Error objects', async () => {
      const mockExecutor = async () => {
        throw new Error('Unexpected error');
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text',
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await touchLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          down: true,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toMatchObject({
        content: [
          {
            type: 'text',
            text: expect.stringContaining(
              'Error: System error executing axe: Failed to execute axe command: Unexpected error',
            ),
          },
        ],
        isError: true,
      });
    });

    it('should handle unexpected string errors', async () => {
      const mockExecutor = async () => {
        throw 'String error';
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text',
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await touchLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          down: true,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: System error executing axe: Failed to execute axe command: String error',
          },
        ],
        isError: true,
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/docs/dev/PLUGIN_DEVELOPMENT.md:
--------------------------------------------------------------------------------

```markdown
# XcodeBuildMCP Plugin Development Guide

This guide provides comprehensive instructions for creating new tools and workflow groups in XcodeBuildMCP using the filesystem-based auto-discovery system.

## Table of Contents

1. [Overview](#overview)
2. [Plugin Architecture](#plugin-architecture)
3. [Creating New Tools](#creating-new-tools)
4. [Creating New Workflow Groups](#creating-new-workflow-groups)
5. [Creating MCP Resources](#creating-mcp-resources)
6. [Auto-Discovery System](#auto-discovery-system)
7. [Testing Guidelines](#testing-guidelines)
8. [Development Workflow](#development-workflow)
9. [Best Practices](#best-practices)

## Overview

XcodeBuildMCP uses a **plugin-based architecture** with **filesystem-based auto-discovery**. Tools are automatically discovered and loaded without manual registration, and can be selectively enabled using `XCODEBUILDMCP_ENABLED_WORKFLOWS`.

### Key Features

- **Auto-Discovery**: Tools are automatically found by scanning `src/mcp/tools/` directory
- **Selective Workflow Loading**: Limit startup tool registration with `XCODEBUILDMCP_ENABLED_WORKFLOWS`
- **Dependency Injection**: All tools use testable patterns with mock-friendly executors
- **Workflow Organization**: Tools are grouped into end-to-end development workflows

## Plugin Architecture

### Directory Structure

```
src/mcp/tools/
├── simulator-workspace/        # iOS Simulator + Workspace tools
├── simulator-project/          # iOS Simulator + Project tools (re-exports)
├── simulator-shared/           # Shared simulator tools (canonical)
├── device-workspace/           # iOS Device + Workspace tools
├── device-project/             # iOS Device + Project tools (re-exports)
├── device-shared/              # Shared device tools (canonical)
├── macos-workspace/            # macOS + Workspace tools
├── macos-project/              # macOS + Project tools (re-exports)
├── macos-shared/               # Shared macOS tools (canonical)
├── swift-package/              # Swift Package Manager tools
├── ui-testing/                 # UI automation tools
├── project-discovery/          # Project analysis tools
├── utilities/                  # General utilities
├── doctor/                     # System health check tools
└── logging/                    # Log capture tools
```

### Plugin Tool Types

1. **Canonical Workflows**: Standalone workflow groups (e.g., `swift-package`, `ui-testing`) defined as folders in the `src/mcp/tools/` directory
2. **Shared Tools**: Common tools in `*-shared` directories (not exposed to clients)
3. **Re-exported Tools**: Share tools to other workflow groups by re-exporting them

## Creating New Tools

### 1. Tool File Structure

Every tool follows this standardized pattern:

```typescript
// src/mcp/tools/my-workflow/my_tool.ts
import { z } from 'zod';
import { ToolResponse } from '../../../types/common.js';
import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/command.js';
import { log, validateRequiredParam, createTextResponse, createErrorResponse } from '../../../utils/index.js';

// 1. Define parameters type for clarity
type MyToolParams = {
  requiredParam: string;
  optionalParam?: string;
};

// 2. Implement the core logic in a separate, testable function
export async function my_toolLogic(
  params: MyToolParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  // 3. Validate required parameters
  const requiredValidation = validateRequiredParam('requiredParam', params.requiredParam);
  if (!requiredValidation.isValid) {
    return requiredValidation.errorResponse;
  }

  log('info', `Executing my_tool with param: ${params.requiredParam}`);

  try {
    // 4. Build and execute the command using the injected executor
    const command = ['my-command', '--param', params.requiredParam];
    if (params.optionalParam) {
      command.push('--optional', params.optionalParam);
    }

    const result = await executor(command, 'My Tool Operation');

    if (!result.success) {
      return createErrorResponse('My Tool operation failed', result.error);
    }

    return createTextResponse(`✅ Success: ${result.output}`);
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `My Tool execution error: ${errorMessage}`);
    return createErrorResponse('Tool execution failed', errorMessage);
  }
}

// 5. Export the tool definition as the default export
export default {
  name: 'my_tool',
  description: 'A brief description of what my_tool does, with a usage example. e.g. my_tool({ requiredParam: "value" })',
  schema: {
    requiredParam: z.string().describe('Description of the required parameter.'),
    optionalParam: z.string().optional().describe('Description of the optional parameter.'),
  },
  // The handler wraps the logic function with the default executor for production use
  handler: async (args: Record<string, unknown>): Promise<ToolResponse> => {
    return my_toolLogic(args as MyToolParams, getDefaultCommandExecutor());
  },
};
```

### 2. Required Tool Plugin Properties

Every tool plugin **must** export a default object with these properties:

| Property | Type | Description |
|----------|------|-------------|
| `name` | `string` | Tool name (must match filename without extension) |
| `description` | `string` | Clear description with usage examples |
| `schema` | `Record<string, z.ZodTypeAny>` | Zod validation schema for parameters |
| `handler` | `function` | Async function: `(args) => Promise<ToolResponse>` |

### 3. Naming Conventions

Tools follow the pattern: `{action}_{target}_{specifier}_{projectType}`

**Examples:**
- `build_sim_id_ws` → Build + Simulator + ID + Workspace
- `build_sim_name_proj` → Build + Simulator + Name + Project  
- `test_device_ws` → Test + Device + Workspace
- `swift_package_build` → Swift Package + Build

**Project Type Suffixes:**
- `_ws` → Works with `.xcworkspace` files
- `_proj` → Works with `.xcodeproj` files
- No suffix → Generic or canonical tools

### 4. Parameter Validation Patterns

Use utility functions for consistent validation:

```typescript
// Required parameter validation
const pathValidation = validateRequiredParam('workspacePath', params.workspacePath);
if (!pathValidation.isValid) return pathValidation.errorResponse;

// At-least-one parameter validation
const identifierValidation = validateAtLeastOneParam(
  'simulatorId', params.simulatorId,
  'simulatorName', params.simulatorName
);
if (!identifierValidation.isValid) return identifierValidation.errorResponse;

// File existence validation
const fileValidation = validateFileExists(params.workspacePath as string);
if (!fileValidation.isValid) return fileValidation.errorResponse;
```

### 5. Response Patterns

Use utility functions for consistent responses:

```typescript
// Success responses
return createTextResponse('✅ Operation succeeded');
return createTextResponse('Operation completed', false); // Not an error

// Error responses  
return createErrorResponse('Operation failed', errorDetails);
return createErrorResponse('Validation failed', errorMessage, 'ValidationError');

// Complex responses
return {
  content: [
    { type: 'text', text: '✅ Build succeeded' },
    { type: 'text', text: 'Next steps: Run install_app_sim...' }
  ],
  isError: false
};
```

## Creating New Workflow Groups

### 1. Workflow Group Structure

Each workflow group requires:

1. **Directory**: Following naming convention
2. **Workflow Metadata**: `index.ts` file with workflow export
3. **Tool Files**: Individual tool implementations
4. **Tests**: Comprehensive test coverage

### 2. Directory Naming Convention

```
[platform]-[projectType]/     # e.g., simulator-workspace, device-project
[platform]-shared/            # e.g., simulator-shared, macos-shared
[workflow-name]/               # e.g., swift-package, ui-testing
```

### 3. Workflow Metadata (index.ts)

**Required for all workflow groups:**

```typescript
// Example: src/mcp/tools/simulator-workspace/index.ts
export const workflow = {
  name: 'iOS Simulator Workspace Development',
  description: 'Complete iOS development workflow for .xcworkspace files including build, test, deploy, and debug capabilities',
};
```

**Required Properties:**
- `name`: Human-readable workflow name
- `description`: Clear description of workflow purpose

### 4. Tool Organization Patterns

#### Canonical Workflow Groups
Self-contained workflows that don't re-export from other groups:

```
swift-package/
├── index.ts                    # Workflow metadata
├── swift_package_build.ts      # Build tool
├── swift_package_test.ts       # Test tool
├── swift_package_run.ts        # Run tool
└── __tests__/                  # Test directory
    ├── index.test.ts           # Workflow tests
    ├── swift_package_build.test.ts
    └── ...
```

#### Shared Workflow Groups  
Provide canonical tools for re-export by project/workspace variants:

```
simulator-shared/
├── boot_sim.ts                 # Canonical simulator boot tool
├── install_app_sim.ts          # Canonical app install tool
└── __tests__/                  # Test directory
    ├── boot_sim.test.ts
    └── ...
```

#### Project/Workspace Workflow Groups
Re-export shared tools and add variant-specific tools:

```
simulator-project/
├── index.ts                    # Workflow metadata
├── boot_sim.ts                 # Re-export: export { default } from '../simulator-shared/boot_sim.js';
├── build_sim_id_proj.ts        # Project-specific build tool
└── __tests__/                  # Test directory
    ├── index.test.ts           # Workflow tests  
    ├── re-exports.test.ts      # Re-export validation
    └── ...
```

### 5. Re-export Implementation

For project/workspace groups that share tools:

```typescript
// simulator-project/boot_sim.ts
export { default } from '../simulator-shared/boot_sim.js';
```

**Re-export Rules:**
1. Re-exports come from canonical `-shared` groups
2. No chained re-exports (re-exports from re-exports)
3. Each tool maintains project or workspace specificity
4. Implementation shared, interfaces remain unique

## Creating MCP Resources

MCP Resources provide efficient URI-based data access for clients that support the MCP resource specification

### 1. Resource Structure

Resources are located in `src/resources/` and follow this pattern:

```typescript
// src/resources/example.ts
import { log, getDefaultCommandExecutor, CommandExecutor } from '../../utils/index.js';

// Testable resource logic separated from MCP handler
export async function exampleResourceLogic(
  executor: CommandExecutor,
): Promise<{ contents: Array<{ text: string }> }> {
  try {
    log('info', 'Processing example resource request');
    
    // Use the executor to get data
    const result = await executor(['some', 'command'], 'Example Resource Operation');
    
    if (!result.success) {
      throw new Error(result.error || 'Failed to get resource data');
    }

    return {
      contents: [{ text: result.output || 'resource data' }]
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error in example resource handler: ${errorMessage}`);

    return {
      contents: [
        {
          text: `Error retrieving resource data: ${errorMessage}`,
        },
      ],
    };
  }
}

export default {
  uri: 'xcodebuildmcp://example',
  name: 'example',
  description: 'Description of the resource data',
  mimeType: 'text/plain',
  async handler(_uri: URL): Promise<{ contents: Array<{ text: string }> }> {
    return exampleResourceLogic(getDefaultCommandExecutor());
  },
};
```

### 2. Resource Implementation Guidelines

**Reuse Existing Logic**: Resources that mirror tools should reuse existing tool logic for consistency:

```typescript
// src/mcp/resources/simulators.ts (simplified example)
import { list_simsLogic } from '../tools/simulator-shared/list_sims.js';

export default {
  uri: 'xcodebuildmcp://simulators',
  name: 'simulators'
  description: 'Available iOS simulators with UUIDs and states',
  mimeType: 'text/plain',
  async handler(uri: URL): Promise<{ contents: Array<{ text: string }> }> {
    const executor = getDefaultCommandExecutor();
    const result = await list_simsLogic({}, executor);
    return {
      contents: [{ text: result.content[0].text }]
    };
  }
};
```

As not all clients support resources it important that resource content that would be ideally be served by resources be mirroed as a tool as well. This ensurew clients that don't support this capability continue to will still have access to that resource data via a simple tool call.

### 3. Resource Testing

Create tests in `src/mcp/resources/__tests__/`:

```typescript
// src/mcp/resources/__tests__/example.test.ts
import exampleResource, { exampleResourceLogic } from '../example.js';
import { createMockExecutor } from '../../utils/test-common.js';

describe('example resource', () => {
  describe('Export Field Validation', () => {
    it('should export correct uri', () => {
      expect(exampleResource.uri).toBe('xcodebuildmcp://example');
    });

    it('should export correct description', () => {
      expect(exampleResource.description).toBe('Description of the resource data');
    });

    it('should export correct mimeType', () => {
      expect(exampleResource.mimeType).toBe('text/plain');
    });

    it('should export handler function', () => {
      expect(typeof exampleResource.handler).toBe('function');
    });
  });

  describe('Resource Logic Functionality', () => {
    it('should return resource data successfully', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'test data'
      });
      
      // Test the logic function directly, not the handler
      const result = await exampleResourceLogic(mockExecutor);
      
      expect(result.contents).toHaveLength(1);
      expect(result.contents[0].text).toContain('expected data');
    });

    it('should handle command execution errors', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Command failed'
      });
      
      const result = await exampleResourceLogic(mockExecutor);
      
      expect(result.contents[0].text).toContain('Error retrieving');
    });
  });
});
```

### 4. Auto-Discovery

Resources are automatically discovered and loaded by the build system. After creating a resource:

1. Run `npm run build` to regenerate resource loaders
2. The resource will be available at its URI for supported clients

## Auto-Discovery System

### How Auto-Discovery Works

1. **Filesystem Scan**: `loadPlugins()` scans `src/mcp/tools/` directory
2. **Workflow Loading**: Each subdirectory is treated as a potential workflow group
3. **Metadata Validation**: `index.ts` files provide workflow metadata
4. **Tool Discovery**: All `.ts` files (except tests and index) are loaded as tools
5. **Registration**: Tools are automatically registered with the MCP server

### Discovery Process

```typescript
// Simplified discovery flow
const plugins = await loadPlugins();
for (const plugin of plugins.values()) {
  server.tool(plugin.name, plugin.description, plugin.schema, plugin.handler);
}
```

### Selective Workflow Loading

To limit which workflows are registered at startup, set `XCODEBUILDMCP_ENABLED_WORKFLOWS` to a comma-separated list of workflow directory names. The `session-management` workflow is always auto-included since other tools depend on it.

Example:
```bash
XCODEBUILDMCP_ENABLED_WORKFLOWS=simulator,device,project-discovery
```

`XCODEBUILDMCP_DEBUG=true` can still be used to increase logging verbosity.

## Testing Guidelines

### Test Organization

```
__tests__/
├── index.test.ts              # Workflow metadata tests (canonical groups only)
├── re-exports.test.ts         # Re-export validation (project/workspace groups)
└── tool_name.test.ts          # Individual tool tests
```

### Dependency Injection Testing

**✅ CORRECT Pattern:**
```typescript
import { createMockExecutor } from '../../../utils/test-common.js';

describe('build_sim_name_ws', () => {
  it('should build successfully', async () => {
    const mockExecutor = createMockExecutor({
      success: true,
      output: 'BUILD SUCCEEDED'
    });

    const result = await build_sim_name_wsLogic(params, mockExecutor);
    expect(result.isError).toBe(false);
  });
});
```

**❌ FORBIDDEN Pattern (Vitest Mocking Banned):**
```typescript
// ❌ ALL VITEST MOCKING IS COMPLETELY BANNED
vi.mock('child_process');
const mockSpawn = vi.fn();
```

### Three-Dimensional Testing

Every tool test must cover:

1. **Input Validation**: Parameter schema validation and error cases
2. **Command Generation**: Verify correct CLI commands are built
3. **Output Processing**: Test response formatting and error handling

### Test Template

```typescript
import { describe, it, expect } from 'vitest';
import { createMockExecutor } from '../../../utils/test-common.js';
import tool, { toolNameLogic } from '../tool_name.js';

describe('tool_name', () => {
  describe('Export Validation', () => {
    it('should export correct name', () => {
      expect(tool.name).toBe('tool_name');
    });

    it('should export correct description', () => {
      expect(tool.description).toContain('Expected description');
    });

    it('should export handler function', () => {
      expect(typeof tool.handler).toBe('function');
    });
  });

  describe('Parameter Validation', () => {
    it('should validate required parameters', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: '' });
      
      const result = await toolNameLogic({}, mockExecutor);
      
      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain("Required parameter");
    });
  });

  describe('Command Generation', () => {
    it('should generate correct command', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'SUCCESS' });
      
      await toolNameLogic({ param: 'value' }, mockExecutor);
      
      expect(mockExecutor).toHaveBeenCalledWith(
        expect.arrayContaining(['expected', 'command']),
        expect.any(String),
        expect.any(Boolean)
      );
    });
  });

  describe('Response Processing', () => {
    it('should handle successful execution', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'SUCCESS' });
      
      const result = await toolNameLogic({ param: 'value' }, mockExecutor);
      
      expect(result.isError).toBe(false);
      expect(result.content[0].text).toContain('✅');
    });

    it('should handle execution errors', async () => {
      const mockExecutor = createMockExecutor({ success: false, error: 'Command failed' });
      
      const result = await toolNameLogic({ param: 'value' }, mockExecutor);
      
      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Command failed');
    });
  });
});
```

## Development Workflow

### Adding a New Tool

1. **Choose Directory**: Select appropriate workflow group or create new one
2. **Create Tool File**: Follow naming convention and structure
3. **Implement Logic**: Use dependency injection pattern
4. **Define Schema**: Add comprehensive Zod validation
5. **Write Tests**: Cover all three dimensions
6. **Test Integration**: Build and verify auto-discovery

### Step-by-Step Tool Creation

```bash
# 1. Create tool file
touch src/mcp/tools/simulator-workspace/my_new_tool_ws.ts

# 2. Implement tool following patterns above

# 3. Create test file
touch src/mcp/tools/simulator-workspace/__tests__/my_new_tool_ws.test.ts

# 4. Build project
npm run build

# 5. Verify tool is discovered (should appear in tools list)
npm run inspect  # Use MCP Inspector to verify
```

### Adding a New Workflow Group

1. **Create Directory**: Follow naming convention
2. **Add Workflow Metadata**: Create `index.ts` with workflow export
3. **Implement Tools**: Add tool files following patterns
4. **Create Tests**: Add comprehensive test coverage
5. **Verify Discovery**: Test auto-discovery and tool registration

### Step-by-Step Workflow Creation

```bash
# 1. Create workflow directory
mkdir src/mcp/tools/my-new-workflow

# 2. Create workflow metadata
cat > src/mcp/tools/my-new-workflow/index.ts << 'EOF'
export const workflow = {
  name: 'My New Workflow',
  description: 'Description of workflow capabilities',
};
EOF

# 3. Create tools directory and test directory
mkdir src/mcp/tools/my-new-workflow/__tests__

# 4. Implement tools following patterns

# 5. Build and verify
npm run build
npm run inspect
```

## Best Practices

### Tool Design

1. **Single Responsibility**: Each tool should have one clear purpose
2. **Descriptive Names**: Follow naming conventions for discoverability
3. **Clear Descriptions**: Include usage examples in tool descriptions
4. **Comprehensive Validation**: Validate all parameters with helpful error messages
5. **Consistent Responses**: Use utility functions for response formatting

### Error Handling

1. **Graceful Failures**: Always return ToolResponse, never throw from handlers
2. **Descriptive Errors**: Provide actionable error messages
3. **Error Types**: Use appropriate error types for different scenarios
4. **Logging**: Log important events and errors for debugging

### Testing

1. **Dependency Injection**: Always test with mock executors
2. **Complete Coverage**: Test all input, command, and output scenarios
3. **Literal Assertions**: Use exact string expectations to catch changes
4. **Fast Execution**: Tests should complete quickly without real system calls

### Workflow Organization  

1. **End-to-End Workflows**: Groups should provide complete functionality
2. **Logical Grouping**: Group related tools together
3. **Clear Capabilities**: Document what each workflow can accomplish
4. **Consistent Patterns**: Follow established patterns for maintainability

### Workflow Metadata Considerations

1. **Workflow Completeness**: Each group should be self-sufficient
2. **Clear Descriptions**: Keep the `description` concise and user-focused

## Updating TOOLS.md Documentation

### Critical Documentation Maintenance

**Every time you add, change, move, edit, or delete a tool, you MUST review and update the [TOOLS.md](../TOOLS.md) file to reflect the current state of the codebase.**

### Documentation Update Process

#### 1. Use Tree CLI for Accurate Discovery

**Always use the `tree` command to get the actual filesystem representation of tools:**

```bash
# Get the definitive source of truth for all workflow groups and tools
tree src/mcp/tools/ -I "__tests__" -I "*.test.ts"
```

This command:
- Shows ALL workflow directories and their tools
- Excludes test files (`__tests__` directories and `*.test.ts` files)
- Provides the actual proof of what exists in the codebase
- Gives an accurate count of tools per workflow group

#### 2. Ignore Shared Groups in Documentation

When updating [TOOLS.md](../TOOLS.md):

- **Ignore `*-shared` directories** (e.g., `simulator-shared`, `device-shared`, `macos-shared`)
- These are implementation details, not user-facing workflow groups
- Only document the main workflow groups that users interact with
- The group count should exclude shared groups

#### 3. List Actual Tool Names

Instead of using generic descriptions like "Additional Tools: Simulator management, logging, UI testing tools":

**❌ Wrong:**
```markdown
- **Additional Tools**: Simulator management, logging, UI testing tools
```

**✅ Correct:**
```markdown
- `boot_sim`, `install_app_sim`, `launch_app_sim`, `list_sims`, `open_sim`
- `describe_ui`, `screenshot`, `start_sim_log_cap`, `stop_sim_log_cap`
```

#### 4. Systematic Documentation Update Steps

1. **Run the tree command** to get current filesystem state
2. **Identify all non-shared workflow directories** 
3. **Count actual tool files** in each directory (exclude `index.ts` and test files)
4. **List all tool names** explicitly in the documentation
5. **Update tool counts** to reflect actual numbers
6. **Verify consistency** between filesystem and documentation

#### 5. Documentation Formatting Requirements

**Format: One Tool Per Bullet Point with Description**

Each tool must be listed individually with its actual description from the tool file:

```markdown
### 1. My Awesome Workflow (`my-awesome-workflow`)
**Purpose**: A short description of what this workflow is for. (2 tools)
- `my_tool_one` - Description for my_tool_one from its definition file.
- `my_tool_two` - Description for my_tool_two from its definition file.
```

**Description Sources:**
- Use the actual `description` field from each tool's TypeScript file
- Descriptions should be concise but informative for end users
- Include platform/context information (iOS, macOS, simulator, device, etc.)
- Mention required parameters when critical for usage

#### 6. Validation Checklist

After updating [TOOLS.md](../TOOLS.md):

- [ ] Tool counts match actual filesystem counts (from tree command)
- [ ] Each tool has its own bullet point (one tool per line)
- [ ] Each tool includes its actual description from the tool file
- [ ] No generic descriptions like "Additional Tools: X, Y, Z"
- [ ] Descriptions are user-friendly and informative
- [ ] Shared groups (`*-shared`) are not included in main workflow list
- [ ] Workflow group count reflects only user-facing groups (15 groups)
- [ ] Tree command output was used as source of truth
- [ ] Documentation is user-focused, not implementation-focused
- [ ] Tool names are in alphabetical order within each workflow group

### Why This Process Matters

1. **Accuracy**: Tree command provides definitive proof of current state
2. **Maintainability**: Systematic process prevents documentation drift
3. **User Experience**: Accurate documentation helps users understand available tools
4. **Development Confidence**: Developers can trust the documentation reflects reality

**Remember**: The filesystem is the source of truth. Documentation must always reflect the actual codebase structure, and the tree command is the most reliable way to ensure accuracy.

```

--------------------------------------------------------------------------------
/src/mcp/tools/ui-testing/__tests__/tap.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tests for tap plugin
 */

import { describe, it, expect, beforeEach } from 'vitest';
import * as z from 'zod';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
import { sessionStore } from '../../../../utils/session-store.ts';

import tapPlugin, { AxeHelpers, tapLogic } from '../tap.ts';

// Helper function to create mock axe helpers
function createMockAxeHelpers(): AxeHelpers {
  return {
    getAxePath: () => '/mocked/axe/path',
    getBundledAxeEnvironment: () => ({ SOME_ENV: 'value' }),
    createAxeNotAvailableResponse: () => ({
      content: [
        {
          type: 'text',
          text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
        },
      ],
      isError: true,
    }),
  };
}

// Helper function to create mock axe helpers with null path (for dependency error tests)
function createMockAxeHelpersWithNullPath(): AxeHelpers {
  return {
    getAxePath: () => null,
    getBundledAxeEnvironment: () => ({ SOME_ENV: 'value' }),
    createAxeNotAvailableResponse: () => ({
      content: [
        {
          type: 'text',
          text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
        },
      ],
      isError: true,
    }),
  };
}

describe('Tap Plugin', () => {
  beforeEach(() => {
    sessionStore.clear();
  });

  describe('Export Field Validation (Literal)', () => {
    it('should have correct name', () => {
      expect(tapPlugin.name).toBe('tap');
    });

    it('should have correct description', () => {
      expect(tapPlugin.description).toBe(
        "Tap at specific coordinates or target elements by accessibility id or label. Use describe_ui to get precise element coordinates prior to using x/y parameters (don't guess from screenshots). Supports optional timing delays.",
      );
    });

    it('should have handler function', () => {
      expect(typeof tapPlugin.handler).toBe('function');
    });

    it('should validate schema fields with safeParse', () => {
      const schema = z.object(tapPlugin.schema);

      expect(schema.safeParse({ x: 100, y: 200 }).success).toBe(true);

      expect(schema.safeParse({ id: 'loginButton' }).success).toBe(true);

      expect(schema.safeParse({ label: 'Log in' }).success).toBe(true);

      expect(schema.safeParse({ x: 100, y: 200, id: 'loginButton' }).success).toBe(true);

      expect(schema.safeParse({ x: 100, y: 200, id: 'loginButton', label: 'Log in' }).success).toBe(
        true,
      );

      expect(
        schema.safeParse({
          x: 100,
          y: 200,
          preDelay: 0.5,
          postDelay: 1,
        }).success,
      ).toBe(true);

      expect(
        schema.safeParse({
          x: 3.14,
          y: 200,
        }).success,
      ).toBe(false);

      expect(
        schema.safeParse({
          x: 100,
          y: 3.14,
        }).success,
      ).toBe(false);

      expect(
        schema.safeParse({
          x: 100,
          y: 200,
          preDelay: -1,
        }).success,
      ).toBe(false);

      expect(
        schema.safeParse({
          x: 100,
          y: 200,
          postDelay: -1,
        }).success,
      ).toBe(false);

      const withSimId = schema.safeParse({
        simulatorId: '12345678-1234-4234-8234-123456789012',
        x: 100,
        y: 200,
      });
      expect(withSimId.success).toBe(true);
      expect('simulatorId' in (withSimId.data as Record<string, unknown>)).toBe(false);
    });
  });

  describe('Command Generation', () => {
    let callHistory: Array<{
      command: string[];
      logPrefix?: string;
      useShell?: boolean;
      opts?: { env?: Record<string, string>; cwd?: string };
    }>;

    beforeEach(() => {
      callHistory = [];
    });

    it('should generate correct axe command with minimal parameters', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Tap completed',
      });

      const wrappedExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        opts?: { env?: Record<string, string>; cwd?: string },
      ) => {
        callHistory.push({ command, logPrefix, useShell, opts });
        return mockExecutor(command, logPrefix, useShell, opts);
      };

      const mockAxeHelpers = createMockAxeHelpers();

      await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
        },
        wrappedExecutor,
        mockAxeHelpers,
      );

      expect(callHistory).toHaveLength(1);
      expect(callHistory[0]).toEqual({
        command: [
          '/mocked/axe/path',
          'tap',
          '-x',
          '100',
          '-y',
          '200',
          '--udid',
          '12345678-1234-4234-8234-123456789012',
        ],
        logPrefix: '[AXe]: tap',
        useShell: false,
        opts: { env: { SOME_ENV: 'value' } },
      });
    });

    it('should generate correct axe command with element id target', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Tap completed',
      });

      const wrappedExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        opts?: { env?: Record<string, string>; cwd?: string },
      ) => {
        callHistory.push({ command, logPrefix, useShell, opts });
        return mockExecutor(command, logPrefix, useShell, opts);
      };

      const mockAxeHelpers = createMockAxeHelpers();

      await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          id: 'loginButton',
        },
        wrappedExecutor,
        mockAxeHelpers,
      );

      expect(callHistory).toHaveLength(1);
      expect(callHistory[0]).toEqual({
        command: [
          '/mocked/axe/path',
          'tap',
          '--id',
          'loginButton',
          '--udid',
          '12345678-1234-4234-8234-123456789012',
        ],
        logPrefix: '[AXe]: tap',
        useShell: false,
        opts: { env: { SOME_ENV: 'value' } },
      });
    });

    it('should generate correct axe command with element label target', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Tap completed',
      });

      const wrappedExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        opts?: { env?: Record<string, string>; cwd?: string },
      ) => {
        callHistory.push({ command, logPrefix, useShell, opts });
        return mockExecutor(command, logPrefix, useShell, opts);
      };

      const mockAxeHelpers = createMockAxeHelpers();

      await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          label: 'Log in',
        },
        wrappedExecutor,
        mockAxeHelpers,
      );

      expect(callHistory).toHaveLength(1);
      expect(callHistory[0]).toEqual({
        command: [
          '/mocked/axe/path',
          'tap',
          '--label',
          'Log in',
          '--udid',
          '12345678-1234-4234-8234-123456789012',
        ],
        logPrefix: '[AXe]: tap',
        useShell: false,
        opts: { env: { SOME_ENV: 'value' } },
      });
    });

    it('should prefer coordinates over id/label when both are provided', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Tap completed',
      });

      const wrappedExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        opts?: { env?: Record<string, string>; cwd?: string },
      ) => {
        callHistory.push({ command, logPrefix, useShell, opts });
        return mockExecutor(command, logPrefix, useShell, opts);
      };

      const mockAxeHelpers = createMockAxeHelpers();

      await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 120,
          y: 240,
          id: 'loginButton',
        },
        wrappedExecutor,
        mockAxeHelpers,
      );

      expect(callHistory).toHaveLength(1);
      expect(callHistory[0]).toEqual({
        command: [
          '/mocked/axe/path',
          'tap',
          '-x',
          '120',
          '-y',
          '240',
          '--udid',
          '12345678-1234-4234-8234-123456789012',
        ],
        logPrefix: '[AXe]: tap',
        useShell: false,
        opts: { env: { SOME_ENV: 'value' } },
      });
    });

    it('should generate correct axe command with pre-delay', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Tap completed',
      });

      const wrappedExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        opts?: { env?: Record<string, string>; cwd?: string },
      ) => {
        callHistory.push({ command, logPrefix, useShell, opts });
        return mockExecutor(command, logPrefix, useShell, opts);
      };

      const mockAxeHelpers = createMockAxeHelpers();

      await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 150,
          y: 300,
          preDelay: 0.5,
        },
        wrappedExecutor,
        mockAxeHelpers,
      );

      expect(callHistory).toHaveLength(1);
      expect(callHistory[0]).toEqual({
        command: [
          '/mocked/axe/path',
          'tap',
          '-x',
          '150',
          '-y',
          '300',
          '--pre-delay',
          '0.5',
          '--udid',
          '12345678-1234-4234-8234-123456789012',
        ],
        logPrefix: '[AXe]: tap',
        useShell: false,
        opts: { env: { SOME_ENV: 'value' } },
      });
    });

    it('should generate correct axe command with post-delay', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Tap completed',
      });

      const wrappedExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        opts?: { env?: Record<string, string>; cwd?: string },
      ) => {
        callHistory.push({ command, logPrefix, useShell, opts });
        return mockExecutor(command, logPrefix, useShell, opts);
      };

      const mockAxeHelpers = createMockAxeHelpers();

      await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 250,
          y: 400,
          postDelay: 1.0,
        },
        wrappedExecutor,
        mockAxeHelpers,
      );

      expect(callHistory).toHaveLength(1);
      expect(callHistory[0]).toEqual({
        command: [
          '/mocked/axe/path',
          'tap',
          '-x',
          '250',
          '-y',
          '400',
          '--post-delay',
          '1',
          '--udid',
          '12345678-1234-4234-8234-123456789012',
        ],
        logPrefix: '[AXe]: tap',
        useShell: false,
        opts: { env: { SOME_ENV: 'value' } },
      });
    });

    it('should generate correct axe command with both delays', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Tap completed',
      });

      const wrappedExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        opts?: { env?: Record<string, string>; cwd?: string },
      ) => {
        callHistory.push({ command, logPrefix, useShell, opts });
        return mockExecutor(command, logPrefix, useShell, opts);
      };

      const mockAxeHelpers = createMockAxeHelpers();

      await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 350,
          y: 500,
          preDelay: 0.3,
          postDelay: 0.7,
        },
        wrappedExecutor,
        mockAxeHelpers,
      );

      expect(callHistory).toHaveLength(1);
      expect(callHistory[0]).toEqual({
        command: [
          '/mocked/axe/path',
          'tap',
          '-x',
          '350',
          '-y',
          '500',
          '--pre-delay',
          '0.3',
          '--post-delay',
          '0.7',
          '--udid',
          '12345678-1234-4234-8234-123456789012',
        ],
        logPrefix: '[AXe]: tap',
        useShell: false,
        opts: { env: { SOME_ENV: 'value' } },
      });
    });
  });

  describe('Success Response Processing', () => {
    it('should return successful response for basic tap', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Tap completed',
      });

      const mockAxeHelpers = createMockAxeHelpers();

      const result = await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Tap at (100, 200) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
          },
        ],
        isError: false,
      });
    });

    it('should return successful response with coordinate warning when describe_ui not called', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Tap completed',
      });

      const mockAxeHelpers = createMockAxeHelpers();

      const result = await tapLogic(
        {
          simulatorId: '87654321-4321-4321-4321-210987654321',
          x: 150,
          y: 300,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Tap at (150, 300) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
          },
        ],
        isError: false,
      });
    });

    it('should return successful response with delays', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Tap completed',
      });

      const mockAxeHelpers = createMockAxeHelpers();

      const result = await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 250,
          y: 400,
          preDelay: 0.5,
          postDelay: 1.0,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Tap at (250, 400) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
          },
        ],
        isError: false,
      });
    });

    it('should return successful response with integer coordinates', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Tap completed',
      });

      const mockAxeHelpers = createMockAxeHelpers();

      const result = await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 0,
          y: 0,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Tap at (0, 0) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
          },
        ],
        isError: false,
      });
    });

    it('should return successful response with large coordinates', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Tap completed',
      });

      const mockAxeHelpers = createMockAxeHelpers();

      const result = await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 1920,
          y: 1080,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Tap at (1920, 1080) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
          },
        ],
        isError: false,
      });
    });

    it('should return successful response for element id target', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Tap completed',
      });

      const mockAxeHelpers = createMockAxeHelpers();

      const result = await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          id: 'loginButton',
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Tap on element id "loginButton" simulated successfully.',
          },
        ],
        isError: false,
      });
    });

    it('should return successful response for element label target', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Tap completed',
      });

      const mockAxeHelpers = createMockAxeHelpers();

      const result = await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          label: 'Log in',
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Tap on element label "Log in" simulated successfully.',
          },
        ],
        isError: false,
      });
    });
  });

  describe('Plugin Handler Validation', () => {
    it('should require simulatorId session default when not provided', async () => {
      const result = await tapPlugin.handler({
        x: 100,
        y: 200,
      });

      expect(result.isError).toBe(true);
      const message = result.content[0].text;
      expect(message).toContain('Missing required session defaults');
      expect(message).toContain('simulatorId is required');
      expect(message).toContain('session-set-defaults');
    });

    it('should return validation error for missing x coordinate', async () => {
      sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });

      const result = await tapPlugin.handler({
        y: 200,
      });

      expect(result.isError).toBe(true);
      const message = result.content[0].text;
      expect(message).toContain('Parameter validation failed');
      expect(message).toContain('x: X coordinate is required when y is provided.');
    });

    it('should return validation error for missing y coordinate', async () => {
      sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });

      const result = await tapPlugin.handler({
        x: 100,
      });

      expect(result.isError).toBe(true);
      const message = result.content[0].text;
      expect(message).toContain('Parameter validation failed');
      expect(message).toContain('y: Y coordinate is required when x is provided.');
    });

    it('should return validation error when both id and label are provided without coordinates', async () => {
      sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });

      const result = await tapPlugin.handler({
        id: 'loginButton',
        label: 'Log in',
      });

      expect(result.isError).toBe(true);
      const message = result.content[0].text;
      expect(message).toContain('Parameter validation failed');
      expect(message).toContain('id: Provide either id or label, not both.');
    });

    it('should return validation error for non-integer x coordinate', async () => {
      sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });

      const result = await tapPlugin.handler({
        x: 3.14,
        y: 200,
      });

      expect(result.isError).toBe(true);
      const message = result.content[0].text;
      expect(message).toContain('Parameter validation failed');
      expect(message).toContain('x: X coordinate must be an integer');
    });

    it('should return validation error for non-integer y coordinate', async () => {
      sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });

      const result = await tapPlugin.handler({
        x: 100,
        y: 3.14,
      });

      expect(result.isError).toBe(true);
      const message = result.content[0].text;
      expect(message).toContain('Parameter validation failed');
      expect(message).toContain('y: Y coordinate must be an integer');
    });

    it('should return validation error for negative preDelay', async () => {
      sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });

      const result = await tapPlugin.handler({
        x: 100,
        y: 200,
        preDelay: -1,
      });

      expect(result.isError).toBe(true);
      const message = result.content[0].text;
      expect(message).toContain('Parameter validation failed');
      expect(message).toContain('preDelay: Pre-delay must be non-negative');
    });

    it('should return validation error for negative postDelay', async () => {
      sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });

      const result = await tapPlugin.handler({
        x: 100,
        y: 200,
        postDelay: -1,
      });

      expect(result.isError).toBe(true);
      const message = result.content[0].text;
      expect(message).toContain('Parameter validation failed');
      expect(message).toContain('postDelay: Post-delay must be non-negative');
    });
  });

  describe('Handler Behavior (Complete Literal Returns)', () => {
    it('should return DependencyError when axe binary is not found', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Tap completed',
        error: undefined,
      });

      const mockAxeHelpers = createMockAxeHelpersWithNullPath();

      const result = await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          preDelay: 0.5,
          postDelay: 1.0,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
          },
        ],
        isError: true,
      });
    });

    it('should handle DependencyError when axe binary not found (second test)', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'Coordinates out of bounds',
      });

      const mockAxeHelpers = createMockAxeHelpersWithNullPath();

      const result = await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
          },
        ],
        isError: true,
      });
    });

    it('should handle DependencyError when axe binary not found (third test)', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'System error occurred',
      });

      const mockAxeHelpers = createMockAxeHelpersWithNullPath();

      const result = await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
          },
        ],
        isError: true,
      });
    });

    it('should handle DependencyError when axe binary not found (fourth test)', async () => {
      const mockExecutor = async () => {
        throw new Error('ENOENT: no such file or directory');
      };

      const mockAxeHelpers = createMockAxeHelpersWithNullPath();

      const result = await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
          },
        ],
        isError: true,
      });
    });

    it('should handle DependencyError when axe binary not found (fifth test)', async () => {
      const mockExecutor = async () => {
        throw new Error('Unexpected error');
      };

      const mockAxeHelpers = createMockAxeHelpersWithNullPath();

      const result = await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
          },
        ],
        isError: true,
      });
    });

    it('should handle DependencyError when axe binary not found (sixth test)', async () => {
      const mockExecutor = async () => {
        throw 'String error';
      };

      const mockAxeHelpers = createMockAxeHelpersWithNullPath();

      const result = await tapLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
          },
        ],
        isError: true,
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/docs/dev/ZOD_MIGRATION_GUIDE.md:
--------------------------------------------------------------------------------

```markdown
# Migration guide

import { Callout } from "fumadocs-ui/components/callout";
import { Tabs, Tab } from "fumadocs-ui/components/tabs";

This migration guide aims to list the breaking changes in Zod 4 in order of highest to lowest impact. To learn more about the performance enhancements and new features of Zod 4, read the [introductory post](/v4).

{/* To give the ecosystem time to migrate, Zod 4 will initially be published alongside Zod v3.25. To use Zod 4, upgrade to `[email protected]` or later: */}

```
npm install zod@^4.0.0
```

{/* Zod 4 is available at the `"/v4"` subpath:

  ```ts
  import * as z from "zod";
  ``` */}

Many of Zod's behaviors and APIs have been made more intuitive and cohesive. The breaking changes described in this document often represent major quality-of-life improvements for Zod users. I strongly recommend reading this guide thoroughly.

<Callout>
  **Note** — Zod 3 exported a number of undocumented quasi-internal utility types and functions that are not considered part of the public API. Changes to those are not documented here.
</Callout>

<Callout>
  **Unofficial codemod** — A community-maintained codemod [`zod-v3-to-v4`](https://github.com/nicoespeon/zod-v3-to-v4) is available.
</Callout>

## Error customization

Zod 4 standardizes the APIs for error customization under a single, unified `error` param. Previously Zod's error customization APIs were fragmented and inconsistent. This is cleaned up in Zod 4.

### deprecates `message`

Replaces `message` with `error`. The `message` parameter is still supported but deprecated.

<Tabs groupId="error-message" items={["Zod 4", "Zod 3"]} persist>
  <Tab value="Zod 4">
    ```ts
    z.string().min(5, { error: "Too short." });
    ```
  </Tab>

  <Tab value="Zod 3">
    ```ts
    z.string().min(5, { message: "Too short." });
    ```
  </Tab>
</Tabs>

### drops `invalid_type_error` and `required_error`

The `invalid_type_error` / `required_error` params have been dropped. These were hastily added years ago as a way to customize errors that was less verbose than `errorMap`. They came with all sorts of footguns (they can't be used in conjunction with `errorMap`) and do not align with Zod's actual issue codes (there is no `required` issue code).

These can now be cleanly represented with the new `error` parameter.

<Tabs groupId="error-type" items={["Zod 4", "Zod 3"]} persist>
  <Tab value="Zod 4">
    ```ts
    z.string({
      error: (issue) => issue.input === undefined
        ? "This field is required"
        : "Not a string"
    });
    ```
  </Tab>

  <Tab value="Zod 3">
    ```ts
    z.string({
      required_error: "This field is required",
      invalid_type_error: "Not a string",
    });
    ```
  </Tab>
</Tabs>

### drops `errorMap`

This is renamed to `error`.

Error maps can also now return a plain `string` (instead of `{message: string}`). They can also return `undefined`, which tells Zod to yield control to the next error map in the chain.

<Tabs groupId="error-map" items={["Zod 4", "Zod 3"]} persist>
  <Tab value="Zod 4">
    ```ts
    z.string().min(5, {
      error: (issue) => {
        if (issue.code === "too_small") {
          return `Value must be >${issue.minimum}`
        }
      },
    });
    ```
  </Tab>

  <Tab value="Zod 3">
    ```ts
    z.string({
      errorMap: (issue, ctx) => {
        if (issue.code === "too_small") {
          return { message: `Value must be >${issue.minimum}` };
        }
        return { message: ctx.defaultError };
      },
    });
    ```
  </Tab>
</Tabs>

{/* ## `.safeParse()`

  For performance reasons, the errors returned by `.safeParse()` and `.safeParseAsync()` no longer extend `Error`.

  ```ts
  const result = z.string().safeParse(12);
  result.error! instanceof Error; // => false
  ```

  It is very slow to instantiate `Error` instances in JavaScript, as the initialization process snapshots the call stack. In the case of Zod's "safe" parse methods, it's expected that you will handle errors at the point of parsing, so instantiating a true `Error` object adds little value anyway.

  > Pro tip: prefer `.safeParse()` over `try/catch` in performance-sensitive code.

  By contrast the errors thrown by `.parse()` and `.parseAsync()` still extend `Error`. Aside from the prototype difference, the error classes are identical.

  ```ts
  try {
  z.string().parse(12);
  } catch (err) {
  console.log(err instanceof Error); // => true
  }
  ```
  */}

## `ZodError`

{/*
  ### changes to `.message`

  Previously the `.message` property on `ZodError` was a JSON.stringified copy of the `.issues` array. This was redundant, confusing, and a bit of an abuse of the `.message` property. Also due to the [`Error` prototype changes](#safeparse) (and inconsistencies in how Node.js logs `Error` subclasses vs other objects) the logging of a multi-line `.message` property got a lot uglier:

  ```sh
  $ tsx index.ts
  ZodError {
  message: '[\n' +
    '  {\n' +
    '    "expected": "string",\n' +
    '    "code": "invalid_type",\n' +
    '    "path": [],\n' +
    '    "message": "Invalid input: expected string, received number"\n' +
    '  }\n' +
    ']'
  }
  ```


  For these reasons, the `.message` property is left empty and the `.issues` array is marked as enumerable. This keeps error logging consistent and pretty:

  ```sh
  $ tsx index.ts
  z.string().parse(234);

  ZodError {
  issues: [
    {
      expected: 'string',
      code: 'invalid_type',
      path: [],
      message: 'Invalid input: expected string, received number'
    }
  ]
  }
  ```

  Vitest uses special handling for `Error` subclasses that ignores enumerable properties.  */}

### updates issue formats

The issue formats have been dramatically streamlined.

```ts
import * as z from "zod"; // v4

type IssueFormats =
  | z.core.$ZodIssueInvalidType
  | z.core.$ZodIssueTooBig
  | z.core.$ZodIssueTooSmall
  | z.core.$ZodIssueInvalidStringFormat
  | z.core.$ZodIssueNotMultipleOf
  | z.core.$ZodIssueUnrecognizedKeys
  | z.core.$ZodIssueInvalidValue
  | z.core.$ZodIssueInvalidUnion
  | z.core.$ZodIssueInvalidKey // new: used for z.record/z.map
  | z.core.$ZodIssueInvalidElement // new: used for z.map/z.set
  | z.core.$ZodIssueCustom;
```

Below is the list of Zod 3 issues types and their Zod 4 equivalent:

```ts
import * as z from "zod"; // v3

export type IssueFormats =
  | z.ZodInvalidTypeIssue // ♻️ renamed to z.core.$ZodIssueInvalidType
  | z.ZodTooBigIssue  // ♻️ renamed to z.core.$ZodIssueTooBig
  | z.ZodTooSmallIssue // ♻️ renamed to z.core.$ZodIssueTooSmall
  | z.ZodInvalidStringIssue // ♻️ z.core.$ZodIssueInvalidStringFormat
  | z.ZodNotMultipleOfIssue // ♻️ renamed to z.core.$ZodIssueNotMultipleOf
  | z.ZodUnrecognizedKeysIssue // ♻️ renamed to z.core.$ZodIssueUnrecognizedKeys
  | z.ZodInvalidUnionIssue // ♻️ renamed to z.core.$ZodIssueInvalidUnion
  | z.ZodCustomIssue // ♻️ renamed to z.core.$ZodIssueCustom
  | z.ZodInvalidEnumValueIssue // ❌ merged in z.core.$ZodIssueInvalidValue
  | z.ZodInvalidLiteralIssue // ❌ merged into z.core.$ZodIssueInvalidValue
  | z.ZodInvalidUnionDiscriminatorIssue // ❌ throws an Error at schema creation time
  | z.ZodInvalidArgumentsIssue // ❌ z.function throws ZodError directly
  | z.ZodInvalidReturnTypeIssue // ❌ z.function throws ZodError directly
  | z.ZodInvalidDateIssue // ❌ merged into invalid_type
  | z.ZodInvalidIntersectionTypesIssue // ❌ removed (throws regular Error)
  | z.ZodNotFiniteIssue // ❌ infinite values no longer accepted (invalid_type)
```

While certain Zod 4 issue types have been merged, dropped, and modified, each issue remains structurally similar to Zod 3 counterpart (identical, in most cases). All issues still conform to the same base interface as Zod 3, so most common error handling logic will work without modification.

```ts
export interface $ZodIssueBase {
  readonly code?: string;
  readonly input?: unknown;
  readonly path: PropertyKey[];
  readonly message: string;
}
```

### changes error map precedence

The error map precedence has been changed to be more consistent. Specifically, an error map passed into `.parse()` *no longer* takes precedence over a schema-level error map.

```ts
const mySchema = z.string({ error: () => "Schema-level error" });

// in Zod 3
mySchema.parse(12, { error: () => "Contextual error" }); // => "Contextual error"

// in Zod 4
mySchema.parse(12, { error: () => "Contextual error" }); // => "Schema-level error"
```

### deprecates `.format()`

The `.format()` method on `ZodError` has been deprecated. Instead use the top-level `z.treeifyError()` function. Read the [Formatting errors docs](/error-formatting) for more information.

### deprecates `.flatten()`

The `.flatten()` method on `ZodError` has also been deprecated. Instead use the top-level `z.treeifyError()` function. Read the [Formatting errors docs](/error-formatting) for more information.

### drops `.formErrors`

This API was identical to `.flatten()`. It exists for historical reasons and isn't documented.

### deprecates `.addIssue()` and `.addIssues()`

Directly push to `err.issues` array instead, if necessary.

```ts
myError.issues.push({
  // new issue
});
```

{/* ## `.and()` dropped

  The `.and()` method on `ZodType` has been dropped in favor of `z.intersection(A, B)`. Not only is this method rarely used, there are few good reasons to use intersections at all. The `.and()` API prevented bundlers from treeshaking `ZodIntersection`, a fairly large and complex class.

  ```ts
  z.object({ a: z.string() }).and(z.object({ b: z.number() })); // ❌

  // use z.intersection
  z.intersection(z.object({ a: z.string() }), z.object({ b: z.number() })); // ✅
  // or .extend() when possible
  z.object({ a: z.string() }).extend(z.object({ b: z.number() })); // ✅
  ``` */}

## `z.number()`

### no infinite values

`POSITIVE_INFINITY` and `NEGATIVE_INFINITY` are no longer considered valid values for `z.number()`.

### `.safe()` no longer accepts floats

In Zod 3, `z.number().safe()` is deprecated. It now behaves identically to `.int()` (see below). Importantly, that means it no longer accepts floats.

### `.int()` accepts safe integers only

The `z.number().int()` API no longer accepts unsafe integers (outside the range of `Number.MIN_SAFE_INTEGER` and `Number.MAX_SAFE_INTEGER`). Using integers out of this range causes spontaneous rounding errors. (Also: You should switch to `z.int()`.)

## `z.string()` updates

### deprecates `.email()` etc

String formats are now represented as *subclasses* of `ZodString`, instead of simple internal refinements. As such, these APIs have been moved to the top-level `z` namespace. Top-level APIs are also less verbose and more tree-shakable.

```ts
z.email();
z.uuid();
z.url();
z.emoji();         // validates a single emoji character
z.base64();
z.base64url();
z.nanoid();
z.cuid();
z.cuid2();
z.ulid();
z.ipv4();
z.ipv6();
z.cidrv4();          // ip range
z.cidrv6();          // ip range
z.iso.date();
z.iso.time();
z.iso.datetime();
z.iso.duration();
```

The method forms (`z.string().email()`) still exist and work as before, but are now deprecated.

```ts
z.string().email(); // ❌ deprecated
z.email(); // ✅
```

### stricter `.uuid()`

The `z.uuid()` now validates UUIDs more strictly against the RFC 9562/4122 specification; specifically, the variant bits must be `10` per the spec. For a more permissive "UUID-like" validator, use `z.guid()`.

```ts
z.uuid(); // RFC 9562/4122 compliant UUID
z.guid(); // any 8-4-4-4-12 hex pattern
```

### no padding in `.base64url()`

Padding is no longer allowed in `z.base64url()` (formerly `z.string().base64url()`). Generally it's desirable for base64url strings to be unpadded and URL-safe.

### drops `z.string().ip()`

This has been replaced with separate `.ipv4()` and `.ipv6()` methods. Use `z.union()` to combine them if you need to accept both.

```ts
z.string().ip() // ❌
z.ipv4() // ✅
z.ipv6() // ✅
```

### updates `z.string().ipv6()`

Validation now happens using the `new URL()` constructor, which is far more robust than the old regular expression approach. Some invalid values that passed validation previously may now fail.

### drops `z.string().cidr()`

Similarly, this has been replaced with separate `.cidrv4()` and `.cidrv6()` methods. Use `z.union()` to combine them if you need to accept both.

```ts
z.string().cidr() // ❌
z.cidrv4() // ✅
z.cidrv6() // ✅
```

## `z.coerce` updates

The input type of all `z.coerce` schemas is now `unknown`.

```ts
const schema = z.coerce.string();
type schemaInput = z.input<typeof schema>;

// Zod 3: string;
// Zod 4: unknown;
```

## `.default()` updates

The application of `.default()` has changed in a subtle way. If the input is `undefined`, `ZodDefault` short-circuits the parsing process and returns the default value. The default value must be assignable to the *output type*.

```ts
const schema = z.string()
  .transform(val => val.length)
  .default(0); // should be a number
schema.parse(undefined); // => 0
```

In Zod 3, `.default()` expected a value that matched the *input type*. `ZodDefault` would parse the default value, instead of short-circuiting. As such, the default value must be assignable to the *input type* of the schema.

```ts
// Zod 3
const schema = z.string()
  .transform(val => val.length)
  .default("tuna");
schema.parse(undefined); // => 4
```

To replicate the old behavior, Zod implements a new `.prefault()` API. This is short for "pre-parse default".

```ts
// Zod 3
const schema = z.string()
  .transform(val => val.length)
  .prefault("tuna");
schema.parse(undefined); // => 4
```

## `z.object()`

### defaults applied within optional fields

Defaults inside your properties are applied, even within optional fields. This aligns better with expectations and resolves a long-standing usability issue with Zod 3. This is a subtle change that may cause breakage in code paths that rely on key existence, etc.

```ts
const schema = z.object({
  a: z.string().default("tuna").optional(),
});

schema.parse({});
// Zod 4: { a: "tuna" }
// Zod 3: {}
```

### deprecates `.strict()` and `.passthrough()`

These methods are generally no longer necessary. Instead use the top-level `z.strictObject()` and `z.looseObject()` functions.

```ts
// Zod 3
z.object({ name: z.string() }).strict();
z.object({ name: z.string() }).passthrough();

// Zod 4
z.strictObject({ name: z.string() });
z.looseObject({ name: z.string() });
```

> These methods are still available for backwards compatibility, and they will not be removed. They are considered legacy.

### deprecates `.strip()`

This was never particularly useful, as it was the default behavior of `z.object()`. To convert a strict object to a "regular" one, use `z.object(A.shape)`.

### drops `.nonstrict()`

This long-deprecated alias for `.strip()` has been removed.

### drops `.deepPartial()`

This has been long deprecated in Zod 3 and it now removed in Zod 4. There is no direct alternative to this API. There were lots of footguns in its implementation, and its use is generally an anti-pattern.

### changes `z.unknown()` optionality

The `z.unknown()` and `z.any()` types are no longer marked as "key optional" in the inferred types.

```ts
const mySchema = z.object({
  a: z.any(),
  b: z.unknown()
});
// Zod 3: { a?: any; b?: unknown };
// Zod 4: { a: any; b: unknown };
```

### deprecates `.merge()`

The `.merge()` method on `ZodObject` has been deprecated in favor of `.extend()`. The `.extend()` method provides the same functionality, avoids ambiguity around strictness inheritance, and has better TypeScript performance.

```ts
// .merge (deprecated)
const ExtendedSchema = BaseSchema.merge(AdditionalSchema);

// .extend (recommended)
const ExtendedSchema = BaseSchema.extend(AdditionalSchema.shape);

// or use destructuring (best tsc performance)
const ExtendedSchema = z.object({
  ...BaseSchema.shape,
  ...AdditionalSchema.shape,
});
```

> **Note**: For even better TypeScript performance, consider using object destructuring instead of `.extend()`. See the [API documentation](/api?id=extend) for more details.

## `z.nativeEnum()` deprecated

The `z.nativeEnum()` function is now deprecated in favor of just `z.enum()`. The `z.enum()` API has been overloaded to support an enum-like input.

```ts
enum Color {
  Red = "red",
  Green = "green",
  Blue = "blue",
}

const ColorSchema = z.enum(Color); // ✅
```

As part of this refactor of `ZodEnum`, a number of long-deprecated and redundant features have been removed. These were all identical and only existed for historical reasons.

```ts
ColorSchema.enum.Red; // ✅ => "Red" (canonical API)
ColorSchema.Enum.Red; // ❌ removed
ColorSchema.Values.Red; // ❌ removed
```

## `z.array()`

### changes `.nonempty()` type

This now behaves identically to `z.array().min(1)`. The inferred type does not change.

```ts
const NonEmpty = z.array(z.string()).nonempty();

type NonEmpty = z.infer<typeof NonEmpty>;
// Zod 3: [string, ...string[]]
// Zod 4: string[]
```

The old behavior is now better represented with `z.tuple()` and a "rest" argument. This aligns more closely to TypeScript's type system.

```ts
z.tuple([z.string()], z.string());
// => [string, ...string[]]
```

## `z.promise()` deprecated

There's rarely a reason to use `z.promise()`. If you have an input that may be a `Promise`, just `await` it before parsing it with Zod.

> If you are using `z.promise` to define an async function with `z.function()`, that's no longer necessary either; see the [`ZodFunction`](#function) section below.

## `z.function()`

The result of `z.function()` is no longer a Zod schema. Instead, it acts as a standalone "function factory" for defining Zod-validated functions. The API has also changed; you define an `input` and `output` schema upfront, instead of using `args()` and `.returns()` methods.

<Tabs groupId="lib" items={["Zod 4", "Zod 3"]} persist>
  <Tab value="Zod 4">
    ```ts
    const myFunction = z.function({
      input: [z.object({
        name: z.string(),
        age: z.number().int(),
      })],
      output: z.string(),
    });

    myFunction.implement((input) => {
      return `Hello ${input.name}, you are ${input.age} years old.`;
    });
    ```
  </Tab>

  <Tab value="Zod 3">
    ```ts
    const myFunction = z.function()
      .args(z.object({
        name: z.string(),
        age: z.number().int(),
      }))
      .returns(z.string());

    myFunction.implement((input) => {
      return `Hello ${input.name}, you are ${input.age} years old.`;
    });
    ```
  </Tab>
</Tabs>

If you have a desperate need for a Zod schema with a function type, consider [this workaround](https://github.com/colinhacks/zod/issues/4143#issuecomment-2845134912).

### adds `.implementAsync()`

To define an async function, use `implementAsync()` instead of `implement()`.

```ts
myFunction.implementAsync(async (input) => {
  return `Hello ${input.name}, you are ${input.age} years old.`;
});
```

## `.refine()`

### ignores type predicates

In Zod 3, passing a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) as a refinement functions could still narrow the type of a schema. This wasn't documented but was discussed in some issues. This is no longer the case.

```ts
const mySchema = z.unknown().refine((val): val is string => {
  return typeof val === "string"
});

type MySchema = z.infer<typeof mySchema>;
// Zod 3: `string`
// Zod 4: still `unknown`
```

### drops `ctx.path`

Zod's new parsing architecture does not eagerly evaluate the `path` array. This was a necessary change that unlocks Zod 4's dramatic performance improvements.

```ts
z.string().superRefine((val, ctx) => {
  ctx.path; // ❌ no longer available
});
```

### drops function as second argument

The following horrifying overload has been removed.

```ts
const longString = z.string().refine(
  (val) => val.length > 10,
  (val) => ({ message: `${val} is not more than 10 characters` })
);
```

{/* ## `.superRefine()` deprecated

  The `.superRefine()` method has been deprecated in favor of `.check()`. The `.check()` method provides the same functionality with a cleaner API. The `.check()` method is also available on Zod and Zod Mini schemas.

  ```ts
  const UniqueStringArray = z.array(z.string()).check((ctx) => {
  if (ctx.value.length > 3) {
    ctx.issues.push({
      code: "too_big",
      maximum: 3,
      origin: "array",
      inclusive: true,
      message: "Too many items 😡",
      input: ctx.value
    });
  }

  if (ctx.value.length !== new Set(ctx.value).size) {
    ctx.issues.push({
      code: "custom",
      message: `No duplicates allowed.`,
      input: ctx.value
    });
  }
  });
  ``` */}

## `z.ostring()`, etc dropped

The undocumented convenience methods `z.ostring()`, `z.onumber()`, etc. have been removed. These were shorthand methods for defining optional string schemas.

## `z.literal()`

### drops `symbol` support

Symbols aren't considered literal values, nor can they be simply compared with `===`. This was an oversight in Zod 3.

## static `.create()` factories dropped

Previously all Zod classes defined a static `.create()` method. These are now implemented as standalone factory functions.

```ts
z.ZodString.create(); // ❌
```

## `z.record()`

### drops single argument usage

Before, `z.record()` could be used with a single argument. This is no longer supported.

```ts
// Zod 3
z.record(z.string()); // ✅

// Zod 4
z.record(z.string()); // ❌
z.record(z.string(), z.string()); // ✅
```

### improves enum support

Records have gotten a lot smarter. In Zod 3, passing an enum into `z.record()` as a key schema would result in a partial type

```ts
const myRecord = z.record(z.enum(["a", "b", "c"]), z.number());
// { a?: number; b?: number; c?: number; }
```

In Zod 4, this is no longer the case. The inferred type is what you'd expect, and Zod ensures exhaustiveness; that is, it makes sure all enum keys exist in the input during parsing.

```ts
const myRecord = z.record(z.enum(["a", "b", "c"]), z.number());
// { a: number; b: number; c: number; }
```

To replicate the old behavior with optional keys, use `z.partialRecord()`:

```ts
const myRecord = z.partialRecord(z.enum(["a", "b", "c"]), z.number());
// { a?: number; b?: number; c?: number; }
```

## `z.intersection()`

### throws `Error` on merge conflict

Zod intersection parses the input against two schemas, then attempts to merge the results. In Zod 3, when the results were unmergable, Zod threw a `ZodError` with a special `"invalid_intersection_types"` issue.

In Zod 4, this will throw a regular `Error` instead. The existence of unmergable results indicates a structural problem with the schema: an intersection of two incompatible types. Thus, a regular error is more appropriate than a validation error.

## Internal changes

> The typical user of Zod can likely ignore everything below this line. These changes do not impact the user-facing `z` APIs.

There are too many internal changes to list here, but some may be relevant to regular users who are (intentionally or not) relying on certain implementation details. These changes will be of particular interest to library authors building tools on top of Zod.

### updates generics

The generic structure of several classes has changed. Perhaps most significant is the change to the `ZodType` base class:

```ts
// Zod 3
class ZodType<Output, Def extends z.ZodTypeDef, Input = Output> {
  // ...
}

// Zod 4
class ZodType<Output = unknown, Input = unknown> {
  // ...
}
```

The second generic `Def` has been entirely removed. Instead the base class now only tracks `Output` and `Input`. While previously the `Input` value defaulted to `Output`, it now defaults to `unknown`. This allows generic functions involving `z.ZodType` to behave more intuitively in many cases.

```ts
function inferSchema<T extends z.ZodType>(schema: T): T {
  return schema;
};

inferSchema(z.string()); // z.ZodString
```

The need for `z.ZodTypeAny` has been eliminated; just use `z.ZodType` instead.

### adds `z.core`

Many utility functions and types have been moved to the new `zod/v4/core` sub-package, to facilitate code sharing between Zod and Zod Mini.

```ts
import * as z from "zod/v4/core";

function handleError(iss: z.$ZodError) {
  // do stuff
}
```

For convenience, the contents of `zod/v4/core` are also re-exported from `zod` and `zod/mini` under the `z.core` namespace.

```ts
import * as z from "zod";

function handleError(iss: z.core.$ZodError) {
  // do stuff
}
```

Refer to the [Zod Core](/packages/core) docs for more information on the contents of the core sub-library.

### moves `._def`

The `._def` property is now moved to `._zod.def`. The structure of all internal defs is subject to change; this is relevant to library authors but won't be comprehensively documented here.

### drops `ZodEffects`

This doesn't affect the user-facing APIs, but it's an internal change worth highlighting. It's part of a larger restructure of how Zod handles *refinements*.

Previously both refinements and transformations lived inside a wrapper class called `ZodEffects`. That means adding either one to a schema would wrap the original schema in a `ZodEffects` instance. In Zod 4, refinements now live inside the schemas themselves. More accurately, each schema contains an array of "checks"; the concept of a "check" is new in Zod 4 and generalizes the concept of a refinement to include potentially side-effectful transforms like `z.toLowerCase()`.

This is particularly apparent in the Zod Mini API, which heavily relies on the `.check()` method to compose various validations together.

```ts
import * as z from "zod/mini";

z.string().check(
  z.minLength(10),
  z.maxLength(100),
  z.toLowerCase(),
  z.trim(),
);
```

### adds `ZodTransform`

Meanwhile, transforms have been moved into a dedicated `ZodTransform` class. This schema class represents an input transform; in fact, you can actually define standalone transformations now:

```ts
import * as z from "zod";

const schema = z.transform(input => String(input));

schema.parse(12); // => "12"
```

This is primarily used in conjunction with `ZodPipe`. The `.transform()` method now returns an instance of `ZodPipe`.

```ts
z.string().transform(val => val); // ZodPipe<ZodString, ZodTransform>
```

### drops `ZodPreprocess`

As with `.transform()`, the `z.preprocess()` function now returns a `ZodPipe` instance instead of a dedicated `ZodPreprocess` instance.

```ts
z.preprocess(val => val, z.string()); // ZodPipe<ZodTransform, ZodString>
```

### drops `ZodBranded`

Branding is now handled with a direct modification to the inferred type, instead of a dedicated `ZodBranded` class. The user-facing APIs remain the same.

{/* - Dropping support for ES5
  - Zod relies on `Set` internally */}

{/* - `z.keyof` now returns `ZodEnum` instead of `ZodLiteral` */}

{/* ## Changed: `.refine()`

  The `.refine()` method used to accept a function as the second argument.

  ```ts
  // no longer supported
  const longString = z.string().refine(
  (val) => val.length > 10,
  (val) => ({ message: `${val} is not more than 10 characters` })
  );
  ```

  This can be better represented with the new `error` parameter, so this overload has been removed.

  ```ts
  const longString = z.string().refine((val) => val.length > 10, {
  error: (issue) => `${issue.input} is not more than 10 characters`,
  });
  ``
  */}

{/*
  - No support for `null` or `undefined` in `z.literal`
  - `z.literal(null)`
  - `z.literal(undefined)`
  - this was never documented */}

{/* - Array min/max/length checks now run after parsing. This means they won't run if the parse has already aborted. */}

{/* - Drops single-argument `z.record()` */}

{/* - Smarter `z.record`: no longer Partial by default */}

{/* - Intersection merge errors are now thrown as Error not ZodError
  - These usually do not reflect a parse error but a structural problem with the schema */}

{/* - Consolidates `unknownKeys` and `catchall` in ZodObject */}

{/* - Dropping
  - `ZodBranded`: purely a static-domain annotation
  - `ZodFunction` */}

{/* - The `description` is now stored in `z.defaultRegistry`, not the def
  - No support for `description` in factory params
  - Descriptions do not cascade in `.optional()`, etc */}

{/* - Enums:
  - ZodEnum and ZodNativeEnum are merged
  - `.Values` and `.Enum` are removed. Use `.enum` instead.
  - `.options` is removed */}

```
Page 11/12FirstPrevNextLast