#
tokens: 49937/50000 26/393 files (page 4/12)
lines: off (toggle) GitHub
raw markdown copy
This is page 4 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

--------------------------------------------------------------------------------
/src/utils/debugger/dap/transport.ts:
--------------------------------------------------------------------------------

```typescript
import { EventEmitter } from 'node:events';
import type { InteractiveProcess, InteractiveSpawner } from '../../execution/index.ts';
import { log } from '../../logging/index.ts';
import type { DapEvent, DapRequest, DapResponse } from './types.ts';

const DEFAULT_LOG_PREFIX = '[DAP Transport]';

export type DapTransportOptions = {
  spawner: InteractiveSpawner;
  adapterCommand: string[];
  env?: Record<string, string>;
  cwd?: string;
  logPrefix?: string;
};

type PendingRequest = {
  command: string;
  resolve: (body: unknown) => void;
  reject: (error: Error) => void;
  timeout: NodeJS.Timeout;
};

export class DapTransport {
  private readonly process: InteractiveProcess;
  private readonly logPrefix: string;
  private readonly pending = new Map<number, PendingRequest>();
  private readonly events = new EventEmitter();
  private nextSeq = 1;
  private buffer = Buffer.alloc(0);
  private disposed = false;
  private exited = false;

  constructor(options: DapTransportOptions) {
    this.logPrefix = options.logPrefix ?? DEFAULT_LOG_PREFIX;
    this.process = options.spawner(options.adapterCommand, {
      env: options.env,
      cwd: options.cwd,
    });

    this.process.process.stdout?.on('data', (data: Buffer) => this.handleStdout(data));
    this.process.process.stderr?.on('data', (data: Buffer) => this.handleStderr(data));
    this.process.process.on('exit', (code, signal) => this.handleExit(code, signal));
    this.process.process.on('error', (error) => this.handleError(error));
  }

  sendRequest<A, B>(command: string, args?: A, opts?: { timeoutMs?: number }): Promise<B> {
    if (this.disposed || this.exited) {
      return Promise.reject(new Error('DAP transport is not available'));
    }

    const seq = this.nextSeq++;
    const request: DapRequest<A> = {
      seq,
      type: 'request',
      command,
      arguments: args,
    };

    const payload = JSON.stringify(request);
    const message = `Content-Length: ${Buffer.byteLength(payload, 'utf8')}\r\n\r\n${payload}`;

    return new Promise<B>((resolve, reject) => {
      const timeoutMs = opts?.timeoutMs ?? 30_000;
      const timeout = setTimeout(() => {
        this.pending.delete(seq);
        reject(new Error(`DAP request timed out after ${timeoutMs}ms (${command})`));
      }, timeoutMs);

      this.pending.set(seq, {
        command,
        resolve: (body) => resolve(body as B),
        reject,
        timeout,
      });

      try {
        this.process.write(message);
      } catch (error) {
        clearTimeout(timeout);
        this.pending.delete(seq);
        reject(error instanceof Error ? error : new Error(String(error)));
      }
    });
  }

  onEvent(handler: (event: DapEvent) => void): () => void {
    this.events.on('event', handler);
    return () => {
      this.events.off('event', handler);
    };
  }

  dispose(): void {
    if (this.disposed) return;
    this.disposed = true;
    this.failAllPending(new Error('DAP transport disposed'));
    try {
      this.process.dispose();
    } catch (error) {
      log('debug', `${this.logPrefix} dispose error: ${String(error)}`);
    }
  }

  private handleStdout(data: Buffer): void {
    if (this.disposed) return;
    this.buffer = Buffer.concat([this.buffer, data]);
    this.processBuffer();
  }

  private handleStderr(data: Buffer): void {
    if (this.disposed) return;
    const message = data.toString('utf8').trim();
    if (!message) return;
    log('debug', `${this.logPrefix} stderr: ${message}`);
  }

  private handleExit(code: number | null, signal: NodeJS.Signals | null): void {
    this.exited = true;
    const detail = signal ? `signal ${signal}` : `code ${code ?? 'unknown'}`;
    this.failAllPending(new Error(`DAP adapter exited (${detail})`));
  }

  private handleError(error: Error): void {
    this.exited = true;
    this.failAllPending(new Error(`DAP adapter error: ${error.message}`));
  }

  private processBuffer(): void {
    while (true) {
      const headerEnd = this.buffer.indexOf('\r\n\r\n');
      if (headerEnd === -1) {
        return;
      }

      const header = this.buffer.slice(0, headerEnd).toString('utf8');
      const contentLength = this.parseContentLength(header);
      if (contentLength == null) {
        log('error', `${this.logPrefix} invalid DAP header: ${header}`);
        this.buffer = this.buffer.slice(headerEnd + 4);
        continue;
      }

      const messageStart = headerEnd + 4;
      const messageEnd = messageStart + contentLength;
      if (this.buffer.length < messageEnd) {
        return;
      }

      const bodyBuffer = this.buffer.slice(messageStart, messageEnd);
      this.buffer = this.buffer.slice(messageEnd);

      try {
        const message = JSON.parse(bodyBuffer.toString('utf8')) as
          | DapResponse
          | DapEvent
          | DapRequest;
        this.handleMessage(message);
      } catch (error) {
        log('error', `${this.logPrefix} failed to parse DAP message: ${String(error)}`);
      }
    }
  }

  private handleMessage(message: DapResponse | DapEvent | DapRequest): void {
    if (message.type === 'response') {
      const pending = this.pending.get(message.request_seq);
      if (!pending) {
        log('debug', `${this.logPrefix} received response without pending request`);
        return;
      }

      this.pending.delete(message.request_seq);
      clearTimeout(pending.timeout);

      if (!message.success) {
        const detail = message.message ?? 'DAP request failed';
        pending.reject(new Error(`${pending.command} failed: ${detail}`));
        return;
      }

      pending.resolve(message.body ?? {});
      return;
    }

    if (message.type === 'event') {
      this.events.emit('event', message);
      return;
    }

    log('debug', `${this.logPrefix} ignoring DAP request: ${message.command ?? 'unknown'}`);
  }

  private parseContentLength(header: string): number | null {
    const lines = header.split(/\r?\n/);
    for (const line of lines) {
      const match = line.match(/^Content-Length:\s*(\d+)/i);
      if (match) {
        const length = Number(match[1]);
        return Number.isFinite(length) ? length : null;
      }
    }
    return null;
  }

  private failAllPending(error: Error): void {
    for (const [seq, pending] of this.pending.entries()) {
      clearTimeout(pending.timeout);
      pending.reject(error);
      this.pending.delete(seq);
    }
  }
}

```

--------------------------------------------------------------------------------
/example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/CalculatorService.swift:
--------------------------------------------------------------------------------

```swift
import Foundation

// MARK: - Calculator Business Logic Service

/// Handles all calculator operations and state management
/// Separated from UI concerns for better testability and modularity
@Observable
public final class CalculatorService {
    // MARK: - Public Properties

    public private(set) var display: String = "0"
    public private(set) var expressionDisplay: String = ""
    public private(set) var hasError: Bool = false

    // MARK: - Private State

    private var currentNumber: Double = 0
    private var previousNumber: Double = 0
    private var operation: Operation?
    private var shouldResetDisplay = false
    private var isNewCalculation = true
    private var lastOperation: Operation?
    private var lastOperand: Double = 0

    // MARK: - Operations

    public enum Operation: String, CaseIterable, Sendable {
        case add = "+"
        case subtract = "-"
        case multiply = "×"
        case divide = "÷"

        public func calculate(_ a: Double, _ b: Double) -> Double {
            switch self {
            case .add: return a + b
            case .subtract: return a - b
            case .multiply: return a * b
            case .divide: return b != 0 ? a / b : 0
            }
        }
    }

    public init() {}

    // MARK: - Public Interface

    public func inputNumber(_ digit: String) {
        guard !hasError else { clear(); return }

        if shouldResetDisplay || isNewCalculation {
            display = digit
            shouldResetDisplay = false
            isNewCalculation = false
        } else if display.count < 12 {
            display = display == "0" ? digit : display + digit
        }

        currentNumber = Double(display) ?? 0
        updateExpressionDisplay()
    }

    /// Inputs a decimal point into the display
    public func inputDecimal() {
        guard !hasError else {
            clear(); return
        }

        if shouldResetDisplay || isNewCalculation {
            display = "0."
            shouldResetDisplay = false
            isNewCalculation = false
        } else if !display.contains("."), display.count < 11 {
            display += "."
        }
        updateExpressionDisplay()
    }

    public func setOperation(_ op: Operation) {
        guard !hasError else { return }

        if operation != nil, !shouldResetDisplay {
            calculate()
            if hasError { return }
        }

        previousNumber = currentNumber
        operation = op
        shouldResetDisplay = true
        isNewCalculation = false
        updateExpressionDisplay()
    }

    public func calculate() {
        guard let op = operation ?? lastOperation else { return }
        let operand = (operation != nil) ? currentNumber : lastOperand

        #if DEBUG
        if op == .add && previousNumber == 21 && operand == 21 {
            fatalError("Intentional crash for debugger smoke test")
        }
        #endif

        let result = op.calculate(previousNumber, operand)

        // Error handling
        if result.isNaN || result.isInfinite {
            setError("Cannot divide by zero")
            return
        }

        if abs(result) > 1e12 {
            setError("Number too large")
            return
        }

        // Success path
        let prevFormatted = formatNumber(previousNumber)
        let currFormatted = formatNumber(operand)
        display = formatNumber(result)
        expressionDisplay = "\(prevFormatted) \(op.rawValue) \(currFormatted) ="

        previousNumber = result
        if operation != nil {
            lastOperand = currentNumber
        }

        lastOperation = op
        operation = nil
        currentNumber = result
        shouldResetDisplay = true
        isNewCalculation = false
    }

    public func toggleSign() {
        guard !hasError, currentNumber != 0 else { return }
        currentNumber *= -1
        display = formatNumber(currentNumber)
        updateExpressionDisplay()
    }

    public func percentage() {
        guard !hasError else { return }
        currentNumber /= 100
        display = formatNumber(currentNumber)
        updateExpressionDisplay()
    }

    public func clear() {
        display = "0"
        expressionDisplay = ""
        currentNumber = 0
        previousNumber = 0
        operation = nil
        shouldResetDisplay = false
        hasError = false
        isNewCalculation = true
    }

    public func deleteLastDigit() {
        guard !hasError else { clear(); return }

        if shouldResetDisplay || isNewCalculation {
            display = "0"
            shouldResetDisplay = false
            isNewCalculation = false
        } else if display.count > 1 {
            display.removeLast()
            if display == "-" { display = "0" }
        } else {
            display = "0"
        }
        currentNumber = Double(display) ?? 0
        updateExpressionDisplay()
    }

    // MARK: - Private Helpers

    private func setError(_ message: String) {
        hasError = true
        display = "Error"
        expressionDisplay = message
    }

    private func updateExpressionDisplay() {
        guard !hasError else { return }

        if let op = operation {
            let prevFormatted = formatNumber(previousNumber)
            expressionDisplay = "\(prevFormatted) \(op.rawValue)"
        } else if isNewCalculation {
            expressionDisplay = ""
        }
    }

    private func formatNumber(_ number: Double) -> String {
        guard !number.isNaN && !number.isInfinite else { return "Error" }

        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        formatter.maximumFractionDigits = 8
        formatter.minimumFractionDigits = 0

        // For integers, don't show decimal places
        if number == floor(number) && abs(number) < 1e10 {
            formatter.maximumFractionDigits = 0
        }

        // For very small decimals, use scientific notation
        if abs(number) < 0.000001 && number != 0 {
            formatter.numberStyle = .scientific
            formatter.maximumFractionDigits = 2
        }

        return formatter.string(from: NSNumber(value: number)) ?? "0"
    }
}

// MARK: - Testing Support

public extension CalculatorService {
    var currentValue: Double { currentNumber }
    var previousValue: Double { previousNumber }
    var currentOperation: Operation? { operation }
    var willResetDisplay: Bool { shouldResetDisplay }
}

```

--------------------------------------------------------------------------------
/src/mcp/tools/debugging/debug_attach_sim.ts:
--------------------------------------------------------------------------------

```typescript
import * as z from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { createErrorResponse, createTextResponse } from '../../../utils/responses/index.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
import { determineSimulatorUuid } from '../../../utils/simulator-utils.ts';
import {
  createSessionAwareToolWithContext,
  getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
import {
  getDefaultDebuggerToolContext,
  resolveSimulatorAppPid,
  type DebuggerToolContext,
} from '../../../utils/debugger/index.ts';

const baseSchemaObject = z.object({
  simulatorId: z
    .string()
    .optional()
    .describe(
      'UUID of the simulator to use (obtained from list_sims). Provide EITHER this OR simulatorName, not both',
    ),
  simulatorName: z
    .string()
    .optional()
    .describe(
      "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both",
    ),
  bundleId: z
    .string()
    .optional()
    .describe("Bundle identifier of the app to attach (e.g., 'com.example.MyApp')"),
  pid: z.number().int().positive().optional().describe('Process ID to attach directly'),
  waitFor: z.boolean().optional().describe('Wait for the process to appear when attaching'),
  continueOnAttach: z
    .boolean()
    .optional()
    .default(true)
    .describe('Resume execution automatically after attaching (default: true)'),
  makeCurrent: z
    .boolean()
    .optional()
    .default(true)
    .describe('Set this debug session as the current session (default: true)'),
});

const debugAttachSchema = z.preprocess(
  nullifyEmptyStrings,
  baseSchemaObject
    .refine((val) => val.simulatorId !== undefined || val.simulatorName !== undefined, {
      message: 'Either simulatorId or simulatorName is required.',
    })
    .refine((val) => !(val.simulatorId && val.simulatorName), {
      message: 'simulatorId and simulatorName are mutually exclusive. Provide only one.',
    })
    .refine((val) => val.bundleId !== undefined || val.pid !== undefined, {
      message: 'Provide either bundleId or pid to attach.',
    })
    .refine((val) => !(val.bundleId && val.pid), {
      message: 'bundleId and pid are mutually exclusive. Provide only one.',
    }),
);

export type DebugAttachSimParams = z.infer<typeof debugAttachSchema>;

export async function debug_attach_simLogic(
  params: DebugAttachSimParams,
  ctx: DebuggerToolContext,
): Promise<ToolResponse> {
  const { executor, debugger: debuggerManager } = ctx;

  const simResult = await determineSimulatorUuid(
    { simulatorId: params.simulatorId, simulatorName: params.simulatorName },
    executor,
  );

  if (simResult.error) {
    return simResult.error;
  }

  const simulatorId = simResult.uuid;
  if (!simulatorId) {
    return createErrorResponse('Simulator resolution failed', 'Unable to determine simulator UUID');
  }

  let pid = params.pid;
  if (!pid && params.bundleId) {
    try {
      pid = await resolveSimulatorAppPid({
        executor,
        simulatorId,
        bundleId: params.bundleId,
      });
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      return createErrorResponse('Failed to resolve simulator PID', message);
    }
  }

  if (!pid) {
    return createErrorResponse('Missing PID', 'Unable to resolve process ID to attach');
  }

  try {
    const session = await debuggerManager.createSession({
      simulatorId,
      pid,
      waitFor: params.waitFor,
    });

    const isCurrent = params.makeCurrent ?? true;
    if (isCurrent) {
      debuggerManager.setCurrentSession(session.id);
    }

    const shouldContinue = params.continueOnAttach ?? true;
    if (shouldContinue) {
      try {
        await debuggerManager.resumeSession(session.id);
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        try {
          await debuggerManager.detachSession(session.id);
        } catch (detachError) {
          const detachMessage =
            detachError instanceof Error ? detachError.message : String(detachError);
          log('warn', `Failed to detach debugger session after resume failure: ${detachMessage}`);
        }
        return createErrorResponse('Failed to resume debugger after attach', message);
      }
    }

    const warningText = simResult.warning ? `⚠️ ${simResult.warning}\n\n` : '';
    const currentText = isCurrent
      ? 'This session is now the current debug session.'
      : 'This session is not set as the current session.';
    const resumeText = shouldContinue
      ? 'Execution resumed after attach.'
      : 'Execution is paused. Use debug_continue to resume before UI automation.';

    const backendLabel = session.backend === 'dap' ? 'DAP debugger' : 'LLDB';

    return createTextResponse(
      `${warningText}✅ Attached ${backendLabel} to simulator process ${pid} (${simulatorId}).\n\n` +
        `Debug session ID: ${session.id}\n` +
        `${currentText}\n` +
        `${resumeText}\n\n` +
        `Next steps:\n` +
        `1. debug_breakpoint_add({ debugSessionId: "${session.id}", file: "...", line: 123 })\n` +
        `2. debug_continue({ debugSessionId: "${session.id}" })\n` +
        `3. debug_stack({ debugSessionId: "${session.id}" })`,
    );
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    log('error', `Failed to attach LLDB: ${message}`);
    return createErrorResponse('Failed to attach debugger', message);
  }
}

const publicSchemaObject = z.strictObject(
  baseSchemaObject.omit({
    simulatorId: true,
    simulatorName: true,
  }).shape,
);

export default {
  name: 'debug_attach_sim',
  description:
    'Attach LLDB to a running iOS simulator app. Provide bundleId or pid, plus simulator defaults.',
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: baseSchemaObject,
  }),
  handler: createSessionAwareToolWithContext<DebugAttachSimParams, DebuggerToolContext>({
    internalSchema: debugAttachSchema as unknown as z.ZodType<DebugAttachSimParams, unknown>,
    logicFunction: debug_attach_simLogic,
    getContext: getDefaultDebuggerToolContext,
    requirements: [
      { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
    ],
    exclusivePairs: [['simulatorId', 'simulatorName']],
  }),
};

```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/__tests__/get_sim_app_path.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tests for get_sim_app_path plugin (session-aware version)
 * Mirrors patterns from other simulator session-aware migrations.
 */

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

describe('get_sim_app_path tool', () => {
  beforeEach(() => {
    sessionStore.clear();
  });

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

    it('should have concise description', () => {
      expect(getSimAppPath.description).toBe('Retrieves the built app path for an iOS simulator.');
    });

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

    it('should expose only platform in public schema', () => {
      const schema = z.object(getSimAppPath.schema);

      expect(schema.safeParse({ platform: 'iOS Simulator' }).success).toBe(true);
      expect(schema.safeParse({}).success).toBe(false);
      expect(schema.safeParse({ platform: 'iOS' }).success).toBe(false);

      const schemaKeys = Object.keys(getSimAppPath.schema).sort();
      expect(schemaKeys).toEqual(['platform']);
    });
  });

  describe('Handler Requirements', () => {
    it('should require scheme when not provided', async () => {
      const result = await getSimAppPath.handler({
        platform: 'iOS Simulator',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('scheme is required');
    });

    it('should require project or workspace when scheme default exists', async () => {
      sessionStore.setDefaults({ scheme: 'MyScheme' });

      const result = await getSimAppPath.handler({
        platform: 'iOS Simulator',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Provide a project or workspace');
    });

    it('should require simulator identifier when scheme and project defaults exist', async () => {
      sessionStore.setDefaults({
        scheme: 'MyScheme',
        projectPath: '/path/to/project.xcodeproj',
      });

      const result = await getSimAppPath.handler({
        platform: 'iOS Simulator',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Provide simulatorId or simulatorName');
    });

    it('should error when both projectPath and workspacePath provided explicitly', async () => {
      sessionStore.setDefaults({ scheme: 'MyScheme' });

      const result = await getSimAppPath.handler({
        platform: 'iOS Simulator',
        projectPath: '/path/project.xcodeproj',
        workspacePath: '/path/workspace.xcworkspace',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
      expect(result.content[0].text).toContain('projectPath');
      expect(result.content[0].text).toContain('workspacePath');
    });

    it('should error when both simulatorId and simulatorName provided explicitly', async () => {
      sessionStore.setDefaults({
        scheme: 'MyScheme',
        workspacePath: '/path/to/workspace.xcworkspace',
      });

      const result = await getSimAppPath.handler({
        platform: 'iOS Simulator',
        simulatorId: 'SIM-UUID',
        simulatorName: 'iPhone 16',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
      expect(result.content[0].text).toContain('simulatorId');
      expect(result.content[0].text).toContain('simulatorName');
    });
  });

  describe('Logic Behavior', () => {
    it('should return app path with simulator name destination', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        opts?: unknown;
      }> = [];

      const trackingExecutor: CommandExecutor = async (
        command,
        logPrefix,
        useShell,
        opts,
      ): Promise<{
        success: boolean;
        output: string;
        process: ChildProcess;
      }> => {
        callHistory.push({ command, logPrefix, useShell, opts });
        return {
          success: true,
          output:
            '    BUILT_PRODUCTS_DIR = /tmp/DerivedData/Build\n    FULL_PRODUCT_NAME = MyApp.app\n',
          process: { pid: 12345 } as unknown as ChildProcess,
        };
      };

      const result = await get_sim_app_pathLogic(
        {
          workspacePath: '/path/to/workspace.xcworkspace',
          scheme: 'MyScheme',
          platform: 'iOS Simulator',
          simulatorName: 'iPhone 16',
          useLatestOS: true,
        },
        trackingExecutor,
      );

      expect(callHistory).toHaveLength(1);
      expect(callHistory[0].logPrefix).toBe('Get App Path');
      expect(callHistory[0].useShell).toBe(true);
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-showBuildSettings',
        '-workspace',
        '/path/to/workspace.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16,OS=latest',
      ]);

      expect(result.isError).toBe(false);
      expect(result.content[0].text).toContain(
        '✅ App path retrieved successfully: /tmp/DerivedData/Build/MyApp.app',
      );
    });

    it('should surface executor failures when build settings cannot be retrieved', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Failed to run xcodebuild',
      });

      const result = await get_sim_app_pathLogic(
        {
          projectPath: '/path/to/project.xcodeproj',
          scheme: 'MyScheme',
          platform: 'iOS Simulator',
          simulatorId: 'SIM-UUID',
        },
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Failed to get app path');
      expect(result.content[0].text).toContain('Failed to run xcodebuild');
    });
  });
});

```

--------------------------------------------------------------------------------
/src/mcp/tools/device/get_device_app_path.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Device Shared Plugin: Get Device App Path (Unified)
 *
 * Gets the app bundle path for a physical device application (iOS, watchOS, tvOS, visionOS) using either a project or workspace.
 * Accepts mutually exclusive `projectPath` or `workspacePath`.
 */

import * as z from 'zod';
import { ToolResponse, XcodePlatform } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { createTextResponse } from '../../../utils/responses/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
  createSessionAwareTool,
  getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';

// Unified schema: XOR between projectPath and workspacePath, sharing common options
const baseOptions = {
  scheme: z.string().describe('The scheme to use'),
  configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
  platform: z
    .enum(['iOS', 'watchOS', 'tvOS', 'visionOS'])
    .optional()
    .describe('Target platform (defaults to iOS)'),
};

const baseSchemaObject = z.object({
  projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
  workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
  ...baseOptions,
});

const getDeviceAppPathSchema = z.preprocess(
  nullifyEmptyStrings,
  baseSchemaObject
    .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
      message: 'Either projectPath or workspacePath is required.',
    })
    .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
      message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
    }),
);

// Use z.infer for type safety
type GetDeviceAppPathParams = z.infer<typeof getDeviceAppPathSchema>;

const publicSchemaObject = baseSchemaObject.omit({
  projectPath: true,
  workspacePath: true,
  scheme: true,
  configuration: true,
} as const);

export async function get_device_app_pathLogic(
  params: GetDeviceAppPathParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  const platformMap = {
    iOS: XcodePlatform.iOS,
    watchOS: XcodePlatform.watchOS,
    tvOS: XcodePlatform.tvOS,
    visionOS: XcodePlatform.visionOS,
  };

  const platform = platformMap[params.platform ?? 'iOS'];
  const configuration = params.configuration ?? 'Debug';

  log('info', `Getting app path for scheme ${params.scheme} on platform ${platform}`);

  try {
    // Create the command array for xcodebuild with -showBuildSettings option
    const command = ['xcodebuild', '-showBuildSettings'];

    // Add the project or workspace
    if (params.projectPath) {
      command.push('-project', params.projectPath);
    } else if (params.workspacePath) {
      command.push('-workspace', params.workspacePath);
    } else {
      // This should never happen due to schema validation
      throw new Error('Either projectPath or workspacePath is required.');
    }

    // Add the scheme and configuration
    command.push('-scheme', params.scheme);
    command.push('-configuration', configuration);

    // Handle destination based on platform
    let destinationString = '';

    if (platform === XcodePlatform.iOS) {
      destinationString = 'generic/platform=iOS';
    } else if (platform === XcodePlatform.watchOS) {
      destinationString = 'generic/platform=watchOS';
    } else if (platform === XcodePlatform.tvOS) {
      destinationString = 'generic/platform=tvOS';
    } else if (platform === XcodePlatform.visionOS) {
      destinationString = 'generic/platform=visionOS';
    } else {
      return createTextResponse(`Unsupported platform: ${platform}`, true);
    }

    command.push('-destination', destinationString);

    // Execute the command directly
    const result = await executor(command, 'Get App Path', true);

    if (!result.success) {
      return createTextResponse(`Failed to get app path: ${result.error}`, true);
    }

    if (!result.output) {
      return createTextResponse('Failed to extract build settings output from the result.', true);
    }

    const buildSettingsOutput = result.output;
    const builtProductsDirMatch = buildSettingsOutput.match(/^\s*BUILT_PRODUCTS_DIR\s*=\s*(.+)$/m);
    const fullProductNameMatch = buildSettingsOutput.match(/^\s*FULL_PRODUCT_NAME\s*=\s*(.+)$/m);

    if (!builtProductsDirMatch || !fullProductNameMatch) {
      return createTextResponse(
        'Failed to extract app path from build settings. Make sure the app has been built first.',
        true,
      );
    }

    const builtProductsDir = builtProductsDirMatch[1].trim();
    const fullProductName = fullProductNameMatch[1].trim();
    const appPath = `${builtProductsDir}/${fullProductName}`;

    const nextStepsText = `Next Steps:
1. Get bundle ID: get_app_bundle_id({ appPath: "${appPath}" })
2. Install app on device: install_app_device({ deviceId: "DEVICE_UDID", appPath: "${appPath}" })
3. Launch app on device: launch_app_device({ deviceId: "DEVICE_UDID", bundleId: "BUNDLE_ID" })`;

    return {
      content: [
        {
          type: 'text',
          text: `✅ App path retrieved successfully: ${appPath}`,
        },
        {
          type: 'text',
          text: nextStepsText,
        },
      ],
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error retrieving app path: ${errorMessage}`);
    return createTextResponse(`Error retrieving app path: ${errorMessage}`, true);
  }
}

export default {
  name: 'get_device_app_path',
  description: 'Retrieves the built app path for a connected device.',
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: baseSchemaObject,
  }),
  annotations: {
    title: 'Get Device App Path',
    readOnlyHint: true,
  },
  handler: createSessionAwareTool<GetDeviceAppPathParams>({
    internalSchema: getDeviceAppPathSchema as unknown as z.ZodType<GetDeviceAppPathParams, unknown>,
    logicFunction: get_device_app_pathLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { allOf: ['scheme'], message: 'scheme is required' },
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
    ],
    exclusivePairs: [['projectPath', 'workspacePath']],
  }),
};

```

--------------------------------------------------------------------------------
/src/utils/video_capture.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Video capture utility for simulator recordings using AXe.
 *
 * Manages long-running AXe "record-video" processes keyed by simulator UUID.
 * It aggregates stdout/stderr to parse the generated MP4 path on stop.
 */

import type { ChildProcess } from 'child_process';
import { log } from './logging/index.ts';
import { getAxePath, getBundledAxeEnvironment } from './axe-helpers.ts';
import type { CommandExecutor } from './execution/index.ts';

type Session = {
  process: unknown;
  sessionId: string;
  startedAt: number;
  buffer: string;
  ended: boolean;
};

const sessions = new Map<string, Session>();
let signalHandlersAttached = false;

export interface AxeHelpers {
  getAxePath: () => string | null;
  getBundledAxeEnvironment: () => Record<string, string>;
}

function ensureSignalHandlersAttached(): void {
  if (signalHandlersAttached) return;
  signalHandlersAttached = true;

  const stopAll = (): void => {
    for (const [simulatorUuid, sess] of sessions) {
      try {
        const child = sess.process as ChildProcess | undefined;
        child?.kill?.('SIGINT');
      } catch {
        // ignore
      } finally {
        sessions.delete(simulatorUuid);
      }
    }
  };

  try {
    process.on('SIGINT', stopAll);
    process.on('SIGTERM', stopAll);
    process.on('exit', stopAll);
  } catch {
    // Non-Node environments may not support process signals; ignore
  }
}

function parseLastAbsoluteMp4Path(buffer: string | undefined): string | null {
  if (!buffer) return null;
  const matches = [...buffer.matchAll(/(\s|^)(\/[^\s'"]+\.mp4)\b/gi)];
  if (matches.length === 0) return null;
  const last = matches[matches.length - 1];
  return last?.[2] ?? null;
}

function createSessionId(simulatorUuid: string): string {
  return `${simulatorUuid}:${Date.now()}`;
}

/**
 * Start recording video for a simulator using AXe.
 */
export async function startSimulatorVideoCapture(
  params: { simulatorUuid: string; fps?: number },
  executor: CommandExecutor,
  axeHelpers?: AxeHelpers,
): Promise<{ started: boolean; sessionId?: string; warning?: string; error?: string }> {
  const simulatorUuid = params.simulatorUuid;
  if (!simulatorUuid) {
    return { started: false, error: 'simulatorUuid is required' };
  }

  if (sessions.has(simulatorUuid)) {
    return {
      started: false,
      error: 'A video recording session is already active for this simulator. Stop it first.',
    };
  }

  const helpers = axeHelpers ?? {
    getAxePath,
    getBundledAxeEnvironment,
  };

  const axeBinary = helpers.getAxePath();
  if (!axeBinary) {
    return { started: false, error: 'Bundled AXe binary not found' };
  }

  const fps = Number.isFinite(params.fps as number) ? Number(params.fps) : 30;
  const command = [axeBinary, 'record-video', '--udid', simulatorUuid, '--fps', String(fps)];
  const env = helpers.getBundledAxeEnvironment?.() ?? {};

  log('info', `Starting AXe video recording for simulator ${simulatorUuid} at ${fps} fps`);

  const result = await executor(command, 'Start Simulator Video Capture', true, { env }, true);

  if (!result.success || !result.process) {
    return {
      started: false,
      error: result.error ?? 'Failed to start video capture process',
    };
  }

  const child = result.process as ChildProcess;
  const session: Session = {
    process: child,
    sessionId: createSessionId(simulatorUuid),
    startedAt: Date.now(),
    buffer: '',
    ended: false,
  };

  try {
    child.stdout?.on('data', (d: unknown) => {
      try {
        session.buffer += String(d ?? '');
      } catch {
        // ignore
      }
    });
    child.stderr?.on('data', (d: unknown) => {
      try {
        session.buffer += String(d ?? '');
      } catch {
        // ignore
      }
    });
  } catch {
    // ignore stream listener setup failures
  }

  // Track when the child process naturally ends, so stop can short-circuit
  try {
    child.once?.('exit', () => {
      session.ended = true;
    });
    child.once?.('close', () => {
      session.ended = true;
    });
  } catch {
    // ignore
  }

  sessions.set(simulatorUuid, session);
  ensureSignalHandlersAttached();

  return {
    started: true,
    sessionId: session.sessionId,
    warning: fps !== (params.fps ?? 30) ? `FPS coerced to ${fps}` : undefined,
  };
}

/**
 * Stop recording video for a simulator. Returns aggregated output and parsed MP4 path if found.
 */
export async function stopSimulatorVideoCapture(
  params: { simulatorUuid: string },
  executor: CommandExecutor,
): Promise<{
  stopped: boolean;
  sessionId?: string;
  stdout?: string;
  parsedPath?: string;
  error?: string;
}> {
  // Mark executor as used to satisfy lint rule
  void executor;

  const simulatorUuid = params.simulatorUuid;
  if (!simulatorUuid) {
    return { stopped: false, error: 'simulatorUuid is required' };
  }

  const session = sessions.get(simulatorUuid);
  if (!session) {
    return { stopped: false, error: 'No active video recording session for this simulator' };
  }

  const child = session.process as ChildProcess | undefined;

  // Attempt graceful shutdown
  try {
    child?.kill?.('SIGINT');
  } catch {
    try {
      child?.kill?.();
    } catch {
      // ignore
    }
  }

  // Wait for process to close (avoid hanging if it already exited)
  await new Promise<void>((resolve): void => {
    if (!child) return resolve();

    // If process has already ended, resolve immediately
    const alreadyEnded = (session as Session).ended === true;
    const hasExitCode = (child as ChildProcess).exitCode !== null;
    const hasSignal = (child as unknown as { signalCode?: string | null }).signalCode != null;
    if (alreadyEnded || hasExitCode || hasSignal) {
      return resolve();
    }

    let resolved = false;
    const finish = (): void => {
      if (!resolved) {
        resolved = true;
        resolve();
      }
    };
    try {
      child.once('close', finish);
      child.once('exit', finish);
    } catch {
      return finish();
    }
    // Safety timeout to prevent indefinite hangs
    setTimeout(finish, 5000);
  });

  const combinedOutput = session.buffer;
  const parsedPath = parseLastAbsoluteMp4Path(combinedOutput) ?? undefined;

  sessions.delete(simulatorUuid);

  log(
    'info',
    `Stopped AXe video recording for simulator ${simulatorUuid}. ${parsedPath ? `Detected file: ${parsedPath}` : 'No file detected in output.'}`,
  );

  return {
    stopped: true,
    sessionId: session.sessionId,
    stdout: combinedOutput,
    parsedPath,
  };
}

```

--------------------------------------------------------------------------------
/src/mcp/tools/ui-testing/key_sequence.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * UI Testing Plugin: Key Sequence
 *
 * Press key sequence using HID keycodes on iOS simulator with configurable delay.
 */

import * as z from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import {
  createTextResponse,
  createErrorResponse,
  DependencyError,
  AxeError,
  SystemError,
} from '../../../utils/responses/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultDebuggerManager } from '../../../utils/debugger/index.ts';
import type { DebuggerManager } from '../../../utils/debugger/debugger-manager.ts';
import { guardUiAutomationAgainstStoppedDebugger } from '../../../utils/debugger/ui-automation-guard.ts';
import {
  createAxeNotAvailableResponse,
  getAxePath,
  getBundledAxeEnvironment,
} from '../../../utils/axe/index.ts';
import {
  createSessionAwareTool,
  getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';

// Define schema as ZodObject
const keySequenceSchema = z.object({
  simulatorId: z.uuid({ message: 'Invalid Simulator UUID format' }),
  keyCodes: z
    .array(z.number().int().min(0).max(255))
    .min(1, { message: 'At least one key code required' }),
  delay: z.number().min(0, { message: 'Delay must be non-negative' }).optional(),
});

// Use z.infer for type safety
type KeySequenceParams = z.infer<typeof keySequenceSchema>;

export interface AxeHelpers {
  getAxePath: () => string | null;
  getBundledAxeEnvironment: () => Record<string, string>;
  createAxeNotAvailableResponse: () => ToolResponse;
}

const LOG_PREFIX = '[AXe]';

export async function key_sequenceLogic(
  params: KeySequenceParams,
  executor: CommandExecutor,
  axeHelpers: AxeHelpers = {
    getAxePath,
    getBundledAxeEnvironment,
    createAxeNotAvailableResponse,
  },
  debuggerManager: DebuggerManager = getDefaultDebuggerManager(),
): Promise<ToolResponse> {
  const toolName = 'key_sequence';
  const { simulatorId, keyCodes, delay } = params;

  const guard = await guardUiAutomationAgainstStoppedDebugger({
    debugger: debuggerManager,
    simulatorId,
    toolName,
  });
  if (guard.blockedResponse) return guard.blockedResponse;

  const commandArgs = ['key-sequence', '--keycodes', keyCodes.join(',')];
  if (delay !== undefined) {
    commandArgs.push('--delay', String(delay));
  }

  log(
    'info',
    `${LOG_PREFIX}/${toolName}: Starting key sequence [${keyCodes.join(',')}] on ${simulatorId}`,
  );

  try {
    await executeAxeCommand(commandArgs, simulatorId, 'key-sequence', executor, axeHelpers);
    log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`);
    const message = `Key sequence [${keyCodes.join(',')}] executed successfully.`;
    if (guard.warningText) {
      return createTextResponse(`${message}\n\n${guard.warningText}`);
    }
    return createTextResponse(message);
  } catch (error) {
    log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
    if (error instanceof DependencyError) {
      return axeHelpers.createAxeNotAvailableResponse();
    } else if (error instanceof AxeError) {
      return createErrorResponse(
        `Failed to execute key sequence: ${error.message}`,
        error.axeOutput,
      );
    } else if (error instanceof SystemError) {
      return createErrorResponse(
        `System error executing axe: ${error.message}`,
        error.originalError?.stack,
      );
    }
    return createErrorResponse(
      `An unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

const publicSchemaObject = z.strictObject(
  keySequenceSchema.omit({ simulatorId: true } as const).shape,
);

export default {
  name: 'key_sequence',
  description: 'Press key sequence using HID keycodes on iOS simulator with configurable delay',
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: keySequenceSchema,
  }),
  annotations: {
    title: 'Key Sequence',
    destructiveHint: true,
  },
  handler: createSessionAwareTool<KeySequenceParams>({
    internalSchema: keySequenceSchema as unknown as z.ZodType<KeySequenceParams, unknown>,
    logicFunction: (params: KeySequenceParams, executor: CommandExecutor) =>
      key_sequenceLogic(params, executor, {
        getAxePath,
        getBundledAxeEnvironment,
        createAxeNotAvailableResponse,
      }),
    getExecutor: getDefaultCommandExecutor,
    requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
  }),
};

// Helper function for executing axe commands (inlined from src/tools/axe/index.ts)
async function executeAxeCommand(
  commandArgs: string[],
  simulatorId: string,
  commandName: string,
  executor: CommandExecutor = getDefaultCommandExecutor(),
  axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse },
): Promise<void> {
  // Get the appropriate axe binary path
  const axeBinary = axeHelpers.getAxePath();
  if (!axeBinary) {
    throw new DependencyError('AXe binary not found');
  }

  // Add --udid parameter to all commands
  const fullArgs = [...commandArgs, '--udid', simulatorId];

  // Construct the full command array with the axe binary as the first element
  const fullCommand = [axeBinary, ...fullArgs];

  try {
    // Determine environment variables for bundled AXe
    const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined;

    const result = await executor(
      fullCommand,
      `${LOG_PREFIX}: ${commandName}`,
      false,
      axeEnv ? { env: axeEnv } : undefined,
    );

    if (!result.success) {
      throw new AxeError(
        `axe command '${commandName}' failed.`,
        commandName,
        result.error ?? result.output,
        simulatorId,
      );
    }

    // Check for stderr output in successful commands
    if (result.error) {
      log(
        'warn',
        `${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${result.error}`,
      );
    }

    // Function now returns void - the calling code creates its own response
  } catch (error) {
    if (error instanceof Error) {
      if (error instanceof AxeError) {
        throw error;
      }

      // Otherwise wrap it in a SystemError
      throw new SystemError(`Failed to execute axe command: ${error.message}`, error);
    }

    // For any other type of error
    throw new SystemError(`Failed to execute axe command: ${String(error)}`);
  }
}

```

--------------------------------------------------------------------------------
/src/utils/debugger/debugger-manager.ts:
--------------------------------------------------------------------------------

```typescript
import { v4 as uuidv4 } from 'uuid';
import type { DebuggerBackend } from './backends/DebuggerBackend.ts';
import { createDapBackend } from './backends/dap-backend.ts';
import { createLldbCliBackend } from './backends/lldb-cli-backend.ts';
import type {
  BreakpointInfo,
  BreakpointSpec,
  DebugExecutionState,
  DebugSessionInfo,
  DebuggerBackendKind,
} from './types.ts';

export type DebuggerBackendFactory = (kind: DebuggerBackendKind) => Promise<DebuggerBackend>;

export class DebuggerManager {
  private readonly backendFactory: DebuggerBackendFactory;
  private readonly sessions = new Map<
    string,
    { info: DebugSessionInfo; backend: DebuggerBackend }
  >();
  private currentSessionId: string | null = null;

  constructor(options: { backendFactory?: DebuggerBackendFactory } = {}) {
    this.backendFactory = options.backendFactory ?? defaultBackendFactory;
  }

  async createSession(opts: {
    simulatorId: string;
    pid: number;
    backend?: DebuggerBackendKind;
    waitFor?: boolean;
  }): Promise<DebugSessionInfo> {
    const backendKind = resolveBackendKind(opts.backend);
    const backend = await this.backendFactory(backendKind);

    try {
      await backend.attach({ pid: opts.pid, simulatorId: opts.simulatorId, waitFor: opts.waitFor });
    } catch (error) {
      try {
        await backend.dispose();
      } catch {
        // Best-effort cleanup; keep original attach error.
      }
      throw error;
    }

    const now = Date.now();
    const info: DebugSessionInfo = {
      id: uuidv4(),
      backend: backendKind,
      simulatorId: opts.simulatorId,
      pid: opts.pid,
      createdAt: now,
      lastUsedAt: now,
    };

    this.sessions.set(info.id, { info, backend });
    return info;
  }

  getSession(id?: string): { info: DebugSessionInfo; backend: DebuggerBackend } | null {
    const resolvedId = id ?? this.currentSessionId;
    if (!resolvedId) return null;
    return this.sessions.get(resolvedId) ?? null;
  }

  setCurrentSession(id: string): void {
    if (!this.sessions.has(id)) {
      throw new Error(`Debug session not found: ${id}`);
    }
    this.currentSessionId = id;
  }

  getCurrentSessionId(): string | null {
    return this.currentSessionId;
  }

  listSessions(): DebugSessionInfo[] {
    return Array.from(this.sessions.values()).map((session) => ({ ...session.info }));
  }

  findSessionForSimulator(simulatorId: string): DebugSessionInfo | null {
    if (!simulatorId) return null;
    if (this.currentSessionId) {
      const current = this.sessions.get(this.currentSessionId);
      if (current?.info.simulatorId === simulatorId) {
        return current.info;
      }
    }

    for (const session of this.sessions.values()) {
      if (session.info.simulatorId === simulatorId) {
        return session.info;
      }
    }

    return null;
  }

  async detachSession(id?: string): Promise<void> {
    const session = this.requireSession(id);
    try {
      await session.backend.detach();
    } finally {
      await session.backend.dispose();
      this.sessions.delete(session.info.id);
      if (this.currentSessionId === session.info.id) {
        this.currentSessionId = null;
      }
    }
  }

  async disposeAll(): Promise<void> {
    await Promise.allSettled(
      Array.from(this.sessions.values()).map(async (session) => {
        try {
          await session.backend.detach();
        } catch {
          // Best-effort cleanup; detach can fail if the process exited.
        } finally {
          await session.backend.dispose();
        }
      }),
    );
    this.sessions.clear();
    this.currentSessionId = null;
  }

  async addBreakpoint(
    id: string | undefined,
    spec: BreakpointSpec,
    opts?: { condition?: string },
  ): Promise<BreakpointInfo> {
    const session = this.requireSession(id);
    const result = await session.backend.addBreakpoint(spec, opts);
    this.touch(session.info.id);
    return result;
  }

  async removeBreakpoint(id: string | undefined, breakpointId: number): Promise<string> {
    const session = this.requireSession(id);
    const result = await session.backend.removeBreakpoint(breakpointId);
    this.touch(session.info.id);
    return result;
  }

  async getStack(
    id: string | undefined,
    opts?: { threadIndex?: number; maxFrames?: number },
  ): Promise<string> {
    const session = this.requireSession(id);
    const result = await session.backend.getStack(opts);
    this.touch(session.info.id);
    return result;
  }

  async getVariables(id: string | undefined, opts?: { frameIndex?: number }): Promise<string> {
    const session = this.requireSession(id);
    const result = await session.backend.getVariables(opts);
    this.touch(session.info.id);
    return result;
  }

  async getExecutionState(
    id: string | undefined,
    opts?: { timeoutMs?: number },
  ): Promise<DebugExecutionState> {
    const session = this.requireSession(id);
    const result = await session.backend.getExecutionState(opts);
    this.touch(session.info.id);
    return result;
  }

  async runCommand(
    id: string | undefined,
    command: string,
    opts?: { timeoutMs?: number },
  ): Promise<string> {
    const session = this.requireSession(id);
    const result = await session.backend.runCommand(command, opts);
    this.touch(session.info.id);
    return result;
  }

  async resumeSession(id?: string, opts?: { threadId?: number }): Promise<void> {
    const session = this.requireSession(id);
    await session.backend.resume(opts);
    this.touch(session.info.id);
  }

  private requireSession(id?: string): { info: DebugSessionInfo; backend: DebuggerBackend } {
    const session = this.getSession(id);
    if (!session) {
      throw new Error('No active debug session. Provide debugSessionId or attach first.');
    }
    return session;
  }

  private touch(id: string): void {
    const session = this.sessions.get(id);
    if (!session) return;
    session.info.lastUsedAt = Date.now();
  }
}

function resolveBackendKind(explicit?: DebuggerBackendKind): DebuggerBackendKind {
  if (explicit) return explicit;
  const envValue = process.env.XCODEBUILDMCP_DEBUGGER_BACKEND;
  if (!envValue) return 'dap';
  const normalized = envValue.trim().toLowerCase();
  if (normalized === 'lldb-cli' || normalized === 'lldb') return 'lldb-cli';
  if (normalized === 'dap') return 'dap';
  throw new Error(`Unsupported debugger backend: ${envValue}`);
}

const defaultBackendFactory: DebuggerBackendFactory = async (kind) => {
  switch (kind) {
    case 'lldb-cli':
      return createLldbCliBackend();
    case 'dap':
      return createDapBackend();
    default:
      throw new Error(`Unsupported debugger backend: ${kind}`);
  }
};

```

--------------------------------------------------------------------------------
/src/utils/__tests__/environment.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Unit tests for environment utilities
 */

import { describe, it, expect } from 'vitest';
import { normalizeTestRunnerEnv } from '../environment.ts';

describe('normalizeTestRunnerEnv', () => {
  describe('Basic Functionality', () => {
    it('should add TEST_RUNNER_ prefix to unprefixed keys', () => {
      const input = { FOO: 'value1', BAR: 'value2' };
      const result = normalizeTestRunnerEnv(input);

      expect(result).toEqual({
        TEST_RUNNER_FOO: 'value1',
        TEST_RUNNER_BAR: 'value2',
      });
    });

    it('should preserve keys already prefixed with TEST_RUNNER_', () => {
      const input = { TEST_RUNNER_FOO: 'value1', TEST_RUNNER_BAR: 'value2' };
      const result = normalizeTestRunnerEnv(input);

      expect(result).toEqual({
        TEST_RUNNER_FOO: 'value1',
        TEST_RUNNER_BAR: 'value2',
      });
    });

    it('should handle mixed prefixed and unprefixed keys', () => {
      const input = {
        FOO: 'value1',
        TEST_RUNNER_BAR: 'value2',
        BAZ: 'value3',
      };
      const result = normalizeTestRunnerEnv(input);

      expect(result).toEqual({
        TEST_RUNNER_FOO: 'value1',
        TEST_RUNNER_BAR: 'value2',
        TEST_RUNNER_BAZ: 'value3',
      });
    });
  });

  describe('Edge Cases', () => {
    it('should handle empty object', () => {
      const result = normalizeTestRunnerEnv({});
      expect(result).toEqual({});
    });

    it('should handle null/undefined values', () => {
      const input = {
        FOO: 'value1',
        BAR: null as any,
        BAZ: undefined as any,
        QUX: 'value4',
      };
      const result = normalizeTestRunnerEnv(input);

      expect(result).toEqual({
        TEST_RUNNER_FOO: 'value1',
        TEST_RUNNER_QUX: 'value4',
      });
    });

    it('should handle empty string values', () => {
      const input = { FOO: '', BAR: 'value2' };
      const result = normalizeTestRunnerEnv(input);

      expect(result).toEqual({
        TEST_RUNNER_FOO: '',
        TEST_RUNNER_BAR: 'value2',
      });
    });

    it('should handle special characters in keys', () => {
      const input = {
        FOO_BAR: 'value1',
        'FOO-BAR': 'value2',
        'FOO.BAR': 'value3',
      };
      const result = normalizeTestRunnerEnv(input);

      expect(result).toEqual({
        TEST_RUNNER_FOO_BAR: 'value1',
        'TEST_RUNNER_FOO-BAR': 'value2',
        'TEST_RUNNER_FOO.BAR': 'value3',
      });
    });

    it('should handle special characters in values', () => {
      const input = {
        FOO: 'value with spaces',
        BAR: 'value/with/slashes',
        BAZ: 'value=with=equals',
      };
      const result = normalizeTestRunnerEnv(input);

      expect(result).toEqual({
        TEST_RUNNER_FOO: 'value with spaces',
        TEST_RUNNER_BAR: 'value/with/slashes',
        TEST_RUNNER_BAZ: 'value=with=equals',
      });
    });
  });

  describe('Real-world Usage Scenarios', () => {
    it('should handle USE_DEV_MODE scenario from GitHub issue', () => {
      const input = { USE_DEV_MODE: 'YES' };
      const result = normalizeTestRunnerEnv(input);

      expect(result).toEqual({
        TEST_RUNNER_USE_DEV_MODE: 'YES',
      });
    });

    it('should handle multiple test configuration variables', () => {
      const input = {
        USE_DEV_MODE: 'YES',
        SKIP_ANIMATIONS: '1',
        DEBUG_MODE: 'true',
        TEST_TIMEOUT: '30',
      };
      const result = normalizeTestRunnerEnv(input);

      expect(result).toEqual({
        TEST_RUNNER_USE_DEV_MODE: 'YES',
        TEST_RUNNER_SKIP_ANIMATIONS: '1',
        TEST_RUNNER_DEBUG_MODE: 'true',
        TEST_RUNNER_TEST_TIMEOUT: '30',
      });
    });

    it('should handle user providing pre-prefixed variables', () => {
      const input = {
        TEST_RUNNER_USE_DEV_MODE: 'YES',
        SKIP_ANIMATIONS: '1',
      };
      const result = normalizeTestRunnerEnv(input);

      expect(result).toEqual({
        TEST_RUNNER_USE_DEV_MODE: 'YES',
        TEST_RUNNER_SKIP_ANIMATIONS: '1',
      });
    });

    it('should handle boolean-like string values', () => {
      const input = {
        ENABLED: 'true',
        DISABLED: 'false',
        YES_FLAG: 'YES',
        NO_FLAG: 'NO',
      };
      const result = normalizeTestRunnerEnv(input);

      expect(result).toEqual({
        TEST_RUNNER_ENABLED: 'true',
        TEST_RUNNER_DISABLED: 'false',
        TEST_RUNNER_YES_FLAG: 'YES',
        TEST_RUNNER_NO_FLAG: 'NO',
      });
    });
  });

  describe('Prefix Handling Edge Cases', () => {
    it('should not double-prefix already prefixed keys', () => {
      const input = { TEST_RUNNER_FOO: 'value1' };
      const result = normalizeTestRunnerEnv(input);

      expect(result).toEqual({
        TEST_RUNNER_FOO: 'value1',
      });

      // Ensure no double prefixing occurred
      expect(result).not.toHaveProperty('TEST_RUNNER_TEST_RUNNER_FOO');
    });

    it('should handle partial prefix matches correctly', () => {
      const input = {
        TEST_RUN: 'value1', // Should get prefixed (not TEST_RUNNER_)
        TEST_RUNNER: 'value2', // Should get prefixed (no underscore)
        TEST_RUNNER_FOO: 'value3', // Should not get prefixed
      };
      const result = normalizeTestRunnerEnv(input);

      expect(result).toEqual({
        TEST_RUNNER_TEST_RUN: 'value1',
        TEST_RUNNER_TEST_RUNNER: 'value2',
        TEST_RUNNER_FOO: 'value3',
      });
    });

    it('should handle case-sensitive prefix detection', () => {
      const input = {
        test_runner_foo: 'value1', // lowercase - should get prefixed
        Test_Runner_Bar: 'value2', // mixed case - should get prefixed
        TEST_RUNNER_BAZ: 'value3', // correct case - should not get prefixed
      };
      const result = normalizeTestRunnerEnv(input);

      expect(result).toEqual({
        TEST_RUNNER_test_runner_foo: 'value1',
        TEST_RUNNER_Test_Runner_Bar: 'value2',
        TEST_RUNNER_BAZ: 'value3',
      });
    });
  });

  describe('Input Validation', () => {
    it('should handle undefined input gracefully', () => {
      const result = normalizeTestRunnerEnv(undefined as any);
      expect(result).toEqual({});
    });

    it('should handle null input gracefully', () => {
      const result = normalizeTestRunnerEnv(null as any);
      expect(result).toEqual({});
    });

    it('should preserve original object (immutability)', () => {
      const input = { FOO: 'value1', BAR: 'value2' };
      const originalInput = { ...input };
      const result = normalizeTestRunnerEnv(input);

      // Original input should remain unchanged
      expect(input).toEqual(originalInput);

      // Result should be different from input
      expect(result).not.toEqual(input);
    });
  });
});

```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/launch_app_sim.ts:
--------------------------------------------------------------------------------

```typescript
import * as z from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
import {
  createSessionAwareTool,
  getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';

const baseSchemaObject = z.object({
  simulatorId: z
    .string()
    .optional()
    .describe(
      'UUID of the simulator to use (obtained from list_sims). Provide EITHER this OR simulatorName, not both',
    ),
  simulatorName: z
    .string()
    .optional()
    .describe(
      "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both",
    ),
  bundleId: z
    .string()
    .describe("Bundle identifier of the app to launch (e.g., 'com.example.MyApp')"),
  args: z.array(z.string()).optional().describe('Additional arguments to pass to the app'),
});

const launchAppSimSchema = z.preprocess(
  nullifyEmptyStrings,
  baseSchemaObject
    .refine((val) => val.simulatorId !== undefined || val.simulatorName !== undefined, {
      message: 'Either simulatorId or simulatorName is required.',
    })
    .refine((val) => !(val.simulatorId !== undefined && val.simulatorName !== undefined), {
      message: 'simulatorId and simulatorName are mutually exclusive. Provide only one.',
    }),
);

export type LaunchAppSimParams = z.infer<typeof launchAppSimSchema>;

export async function launch_app_simLogic(
  params: LaunchAppSimParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  let simulatorId = params.simulatorId;
  let simulatorDisplayName = simulatorId ?? '';

  if (params.simulatorName && !simulatorId) {
    log('info', `Looking up simulator by name: ${params.simulatorName}`);

    const simulatorListResult = await executor(
      ['xcrun', 'simctl', 'list', 'devices', 'available', '--json'],
      'List Simulators',
      true,
    );
    if (!simulatorListResult.success) {
      return {
        content: [
          {
            type: 'text',
            text: `Failed to list simulators: ${simulatorListResult.error}`,
          },
        ],
        isError: true,
      };
    }

    const simulatorsData = JSON.parse(simulatorListResult.output) as {
      devices: Record<string, Array<{ udid: string; name: string }>>;
    };

    let foundSimulator: { udid: string; name: string } | null = null;
    for (const runtime in simulatorsData.devices) {
      const devices = simulatorsData.devices[runtime];
      const simulator = devices.find((device) => device.name === params.simulatorName);
      if (simulator) {
        foundSimulator = simulator;
        break;
      }
    }

    if (!foundSimulator) {
      return {
        content: [
          {
            type: 'text',
            text: `Simulator named "${params.simulatorName}" not found. Use list_sims to see available simulators.`,
          },
        ],
        isError: true,
      };
    }

    simulatorId = foundSimulator.udid;
    simulatorDisplayName = `"${params.simulatorName}" (${foundSimulator.udid})`;
  }

  if (!simulatorId) {
    return {
      content: [
        {
          type: 'text',
          text: 'No simulator identifier provided',
        },
      ],
      isError: true,
    };
  }

  log('info', `Starting xcrun simctl launch request for simulator ${simulatorId}`);

  try {
    const getAppContainerCmd = [
      'xcrun',
      'simctl',
      'get_app_container',
      simulatorId,
      params.bundleId,
      'app',
    ];
    const getAppContainerResult = await executor(
      getAppContainerCmd,
      'Check App Installed',
      true,
      undefined,
    );
    if (!getAppContainerResult.success) {
      return {
        content: [
          {
            type: 'text',
            text: `App is not installed on the simulator. Please use install_app_sim before launching.\n\nWorkflow: build → install → launch.`,
          },
        ],
        isError: true,
      };
    }
  } catch {
    return {
      content: [
        {
          type: 'text',
          text: `App is not installed on the simulator (check failed). Please use install_app_sim before launching.\n\nWorkflow: build → install → launch.`,
        },
      ],
      isError: true,
    };
  }

  try {
    const command = ['xcrun', 'simctl', 'launch', simulatorId, params.bundleId];
    if (params.args && params.args.length > 0) {
      command.push(...params.args);
    }

    const result = await executor(command, 'Launch App in Simulator', true, undefined);

    if (!result.success) {
      return {
        content: [
          {
            type: 'text',
            text: `Launch app in simulator operation failed: ${result.error}`,
          },
        ],
      };
    }

    const userParamName = params.simulatorId ? 'simulatorId' : 'simulatorName';
    const userParamValue = params.simulatorId ?? params.simulatorName ?? simulatorId;

    return {
      content: [
        {
          type: 'text',
          text: `✅ App launched successfully in simulator ${simulatorDisplayName || simulatorId}.\n\nNext Steps:\n1. To see simulator: open_sim()\n2. Log capture: start_sim_log_cap({ ${userParamName}: "${userParamValue}", bundleId: "${params.bundleId}" })\n   With console: start_sim_log_cap({ ${userParamName}: "${userParamValue}", bundleId: "${params.bundleId}", captureConsole: true })\n3. Stop logs: stop_sim_log_cap({ logSessionId: 'SESSION_ID' })`,
        },
      ],
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error during launch app in simulator operation: ${errorMessage}`);
    return {
      content: [
        {
          type: 'text',
          text: `Launch app in simulator operation failed: ${errorMessage}`,
        },
      ],
    };
  }
}

const publicSchemaObject = z.strictObject(
  baseSchemaObject.omit({
    simulatorId: true,
    simulatorName: true,
  } as const).shape,
);

export default {
  name: 'launch_app_sim',
  description: 'Launches an app in an iOS simulator.',
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: baseSchemaObject,
  }),
  annotations: {
    title: 'Launch App Simulator',
    destructiveHint: true,
  },
  handler: createSessionAwareTool<LaunchAppSimParams>({
    internalSchema: launchAppSimSchema as unknown as z.ZodType<LaunchAppSimParams, unknown>,
    logicFunction: launch_app_simLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
    ],
    exclusivePairs: [['simulatorId', 'simulatorName']],
  }),
};

```

--------------------------------------------------------------------------------
/src/mcp/tools/macos/get_mac_app_path.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * macOS Shared Plugin: Get macOS App Path (Unified)
 *
 * Gets the app bundle path for a macOS application using either a project or workspace.
 * Accepts mutually exclusive `projectPath` or `workspacePath`.
 */

import * as z from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
  createSessionAwareTool,
  getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';

// Unified schema: XOR between projectPath and workspacePath, sharing common options
const baseOptions = {
  scheme: z.string().describe('The scheme to use'),
  configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
  derivedDataPath: z.string().optional().describe('Path to derived data directory'),
  extraArgs: z.array(z.string()).optional().describe('Additional arguments to pass to xcodebuild'),
  arch: z
    .enum(['arm64', 'x86_64'])
    .optional()
    .describe('Architecture to build for (arm64 or x86_64). For macOS only.'),
};

const baseSchemaObject = z.object({
  projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
  workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
  ...baseOptions,
});

const publicSchemaObject = baseSchemaObject.omit({
  projectPath: true,
  workspacePath: true,
  scheme: true,
  configuration: true,
  arch: true,
} as const);

const getMacosAppPathSchema = z.preprocess(
  nullifyEmptyStrings,
  baseSchemaObject
    .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
      message: 'Either projectPath or workspacePath is required.',
    })
    .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
      message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
    }),
);

// Use z.infer for type safety
type GetMacosAppPathParams = z.infer<typeof getMacosAppPathSchema>;

const XcodePlatform = {
  iOS: 'iOS',
  watchOS: 'watchOS',
  tvOS: 'tvOS',
  visionOS: 'visionOS',
  iOSSimulator: 'iOS Simulator',
  watchOSSimulator: 'watchOS Simulator',
  tvOSSimulator: 'tvOS Simulator',
  visionOSSimulator: 'visionOS Simulator',
  macOS: 'macOS',
};

export async function get_mac_app_pathLogic(
  params: GetMacosAppPathParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  const configuration = params.configuration ?? 'Debug';

  log('info', `Getting app path for scheme ${params.scheme} on platform ${XcodePlatform.macOS}`);

  try {
    // Create the command array for xcodebuild with -showBuildSettings option
    const command = ['xcodebuild', '-showBuildSettings'];

    // Add the project or workspace
    if (params.projectPath) {
      command.push('-project', params.projectPath);
    } else if (params.workspacePath) {
      command.push('-workspace', params.workspacePath);
    } else {
      // This should never happen due to schema validation
      throw new Error('Either projectPath or workspacePath is required.');
    }

    // Add the scheme and configuration
    command.push('-scheme', params.scheme);
    command.push('-configuration', configuration);

    // Add optional derived data path
    if (params.derivedDataPath) {
      command.push('-derivedDataPath', params.derivedDataPath);
    }

    // Handle destination for macOS when arch is specified
    if (params.arch) {
      const destinationString = `platform=macOS,arch=${params.arch}`;
      command.push('-destination', destinationString);
    }

    // Add extra arguments if provided
    if (params.extraArgs && Array.isArray(params.extraArgs)) {
      command.push(...params.extraArgs);
    }

    // Execute the command directly with executor
    const result = await executor(command, 'Get App Path', true, undefined);

    if (!result.success) {
      return {
        content: [
          {
            type: 'text',
            text: `Error: Failed to get macOS app path\nDetails: ${result.error}`,
          },
        ],
        isError: true,
      };
    }

    if (!result.output) {
      return {
        content: [
          {
            type: 'text',
            text: 'Error: Failed to get macOS app path\nDetails: Failed to extract build settings output from the result',
          },
        ],
        isError: true,
      };
    }

    const buildSettingsOutput = result.output;
    const builtProductsDirMatch = buildSettingsOutput.match(/^\s*BUILT_PRODUCTS_DIR\s*=\s*(.+)$/m);
    const fullProductNameMatch = buildSettingsOutput.match(/^\s*FULL_PRODUCT_NAME\s*=\s*(.+)$/m);

    if (!builtProductsDirMatch || !fullProductNameMatch) {
      return {
        content: [
          {
            type: 'text',
            text: 'Error: Failed to get macOS app path\nDetails: Could not extract app path from build settings',
          },
        ],
        isError: true,
      };
    }

    const builtProductsDir = builtProductsDirMatch[1].trim();
    const fullProductName = fullProductNameMatch[1].trim();
    const appPath = `${builtProductsDir}/${fullProductName}`;

    // Include next steps guidance (following workspace pattern)
    const nextStepsText = `Next Steps:
1. Get bundle ID: get_app_bundle_id({ appPath: "${appPath}" })
2. Launch app: launch_mac_app({ appPath: "${appPath}" })`;

    return {
      content: [
        {
          type: 'text',
          text: `✅ App path retrieved successfully: ${appPath}`,
        },
        {
          type: 'text',
          text: nextStepsText,
        },
      ],
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error retrieving app path: ${errorMessage}`);
    return {
      content: [
        {
          type: 'text',
          text: `Error: Failed to get macOS app path\nDetails: ${errorMessage}`,
        },
      ],
      isError: true,
    };
  }
}

export default {
  name: 'get_mac_app_path',
  description: 'Retrieves the built macOS app bundle path.',
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: baseSchemaObject,
  }),
  annotations: {
    title: 'Get macOS App Path',
    readOnlyHint: true,
  },
  handler: createSessionAwareTool<GetMacosAppPathParams>({
    internalSchema: getMacosAppPathSchema as unknown as z.ZodType<GetMacosAppPathParams, unknown>,
    logicFunction: get_mac_app_pathLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { allOf: ['scheme'], message: 'scheme is required' },
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
    ],
    exclusivePairs: [['projectPath', 'workspacePath']],
  }),
};

```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/list_sims.ts:
--------------------------------------------------------------------------------

```typescript
import * as z from 'zod';
import type { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';

// Define schema as ZodObject
const listSimsSchema = z.object({
  enabled: z.boolean().optional().describe('Optional flag to enable the listing operation.'),
});

// Use z.infer for type safety
type ListSimsParams = z.infer<typeof listSimsSchema>;

interface SimulatorDevice {
  name: string;
  udid: string;
  state: string;
  isAvailable: boolean;
  runtime?: string;
}

interface SimulatorData {
  devices: Record<string, SimulatorDevice[]>;
}

// Parse text output as fallback for Apple simctl JSON bugs (e.g., duplicate runtime IDs)
function parseTextOutput(textOutput: string): SimulatorDevice[] {
  const devices: SimulatorDevice[] = [];
  const lines = textOutput.split('\n');
  let currentRuntime = '';

  for (const line of lines) {
    // Match runtime headers like "-- iOS 26.0 --" or "-- iOS 18.6 --"
    const runtimeMatch = line.match(/^-- ([\w\s.]+) --$/);
    if (runtimeMatch) {
      currentRuntime = runtimeMatch[1];
      continue;
    }

    // Match device lines like "    iPhone 17 Pro (UUID) (Booted)"
    // UUID pattern is flexible to handle test UUIDs like "test-uuid-123"
    const deviceMatch = line.match(
      /^\s+(.+?)\s+\(([^)]+)\)\s+\((Booted|Shutdown|Booting|Shutting Down)\)(\s+\(unavailable.*\))?$/i,
    );
    if (deviceMatch && currentRuntime) {
      const [, name, udid, state, unavailableSuffix] = deviceMatch;
      const isUnavailable = Boolean(unavailableSuffix);
      if (!isUnavailable) {
        devices.push({
          name: name.trim(),
          udid,
          state,
          isAvailable: true,
          runtime: currentRuntime,
        });
      }
    }
  }

  return devices;
}

function isSimulatorData(value: unknown): value is SimulatorData {
  if (!value || typeof value !== 'object') {
    return false;
  }

  const obj = value as Record<string, unknown>;
  if (!obj.devices || typeof obj.devices !== 'object') {
    return false;
  }

  const devices = obj.devices as Record<string, unknown>;
  for (const runtime in devices) {
    const deviceList = devices[runtime];
    if (!Array.isArray(deviceList)) {
      return false;
    }

    for (const device of deviceList) {
      if (!device || typeof device !== 'object') {
        return false;
      }

      const deviceObj = device as Record<string, unknown>;
      if (
        typeof deviceObj.name !== 'string' ||
        typeof deviceObj.udid !== 'string' ||
        typeof deviceObj.state !== 'string' ||
        typeof deviceObj.isAvailable !== 'boolean'
      ) {
        return false;
      }
    }
  }

  return true;
}

export async function list_simsLogic(
  params: ListSimsParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  log('info', 'Starting xcrun simctl list devices request');

  try {
    // Try JSON first for structured data
    const jsonCommand = ['xcrun', 'simctl', 'list', 'devices', '--json'];
    const jsonResult = await executor(jsonCommand, 'List Simulators (JSON)', true);

    if (!jsonResult.success) {
      return {
        content: [
          {
            type: 'text',
            text: `Failed to list simulators: ${jsonResult.error}`,
          },
        ],
      };
    }

    // Parse JSON output
    let jsonDevices: Record<string, SimulatorDevice[]> = {};
    try {
      const parsedData: unknown = JSON.parse(jsonResult.output);
      if (isSimulatorData(parsedData)) {
        jsonDevices = parsedData.devices;
      }
    } catch {
      log('warn', 'Failed to parse JSON output, falling back to text parsing');
    }

    // Fallback to text parsing for Apple simctl bugs (duplicate runtime IDs in iOS 26.0 beta)
    const textCommand = ['xcrun', 'simctl', 'list', 'devices'];
    const textResult = await executor(textCommand, 'List Simulators (Text)', true);

    const textDevices = textResult.success ? parseTextOutput(textResult.output) : [];

    // Merge JSON and text devices, preferring JSON but adding any missing from text
    const allDevices: Record<string, SimulatorDevice[]> = { ...jsonDevices };
    const jsonUUIDs = new Set<string>();

    // Collect all UUIDs from JSON
    for (const runtime in jsonDevices) {
      for (const device of jsonDevices[runtime]) {
        if (device.isAvailable) {
          jsonUUIDs.add(device.udid);
        }
      }
    }

    // Add devices from text that aren't in JSON (handles Apple's duplicate runtime ID bug)
    for (const textDevice of textDevices) {
      if (!jsonUUIDs.has(textDevice.udid)) {
        const runtime = textDevice.runtime ?? 'Unknown Runtime';
        if (!allDevices[runtime]) {
          allDevices[runtime] = [];
        }
        allDevices[runtime].push(textDevice);
        log(
          'info',
          `Added missing device from text parsing: ${textDevice.name} (${textDevice.udid})`,
        );
      }
    }

    // Format output
    let responseText = 'Available iOS Simulators:\n\n';

    for (const runtime in allDevices) {
      const devices = allDevices[runtime].filter((d) => d.isAvailable);

      if (devices.length === 0) continue;

      responseText += `${runtime}:\n`;

      for (const device of devices) {
        responseText += `- ${device.name} (${device.udid})${device.state === 'Booted' ? ' [Booted]' : ''}\n`;
      }

      responseText += '\n';
    }

    responseText += 'Next Steps:\n';
    responseText += "1. Boot a simulator: boot_sim({ simulatorId: 'UUID_FROM_ABOVE' })\n";
    responseText += '2. Open the simulator UI: open_sim({})\n';
    responseText +=
      "3. Build for simulator: build_sim({ scheme: 'YOUR_SCHEME', simulatorId: 'UUID_FROM_ABOVE' })\n";
    responseText +=
      "4. Get app path: get_sim_app_path({ scheme: 'YOUR_SCHEME', platform: 'iOS Simulator', simulatorId: 'UUID_FROM_ABOVE' })\n";
    responseText +=
      "Hint: Save a default simulator with session-set-defaults { simulatorId: 'UUID_FROM_ABOVE' } (or simulatorName).";

    return {
      content: [
        {
          type: 'text',
          text: responseText,
        },
      ],
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error listing simulators: ${errorMessage}`);
    return {
      content: [
        {
          type: 'text',
          text: `Failed to list simulators: ${errorMessage}`,
        },
      ],
    };
  }
}

export default {
  name: 'list_sims',
  description: 'Lists available iOS simulators with their UUIDs. ',
  schema: listSimsSchema.shape, // MCP SDK compatibility
  annotations: {
    title: 'List Simulators',
    readOnlyHint: true,
  },
  handler: createTypedTool(listSimsSchema, list_simsLogic, getDefaultCommandExecutor),
};

```

--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------

```yaml
name: Release

on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:
    inputs:
      version:
        description: 'Test version (e.g., 1.9.1-test)'
        required: true
        type: string

permissions:
  contents: write
  id-token: write

jobs:
  release:
    runs-on: macos-latest
    environment: production

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '24'
          registry-url: 'https://registry.npmjs.org'

      # Ensure npm 11.5.1 or later is installed
      - name: Update npm
        run: npm install -g npm@latest

      - name: Clear npm cache and install dependencies
        run: |
          npm cache clean --force
          rm -rf node_modules package-lock.json
          npm install --ignore-scripts

      - name: Check formatting
        run: npm run format:check

      - name: Bundle AXe artifacts
        run: npm run bundle:axe

      - name: Build Smithery bundle
        run: npm run build

      - name: Run tests
        run: npm test

      - name: Get version from tag or input
        id: get_version
        run: |
          if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
            VERSION="${{ github.event.inputs.version }}"
            echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
            echo "IS_TEST=true" >> $GITHUB_OUTPUT
            echo "📝 Test version: $VERSION"
            # Update package.json version for test releases only
            npm version $VERSION --no-git-tag-version
          else
            VERSION=${GITHUB_REF#refs/tags/v}
            echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
            echo "IS_TEST=false" >> $GITHUB_OUTPUT
            echo "🚀 Release version: $VERSION"
            # For tag-based releases, package.json was already updated by release script
          fi

      - name: Create package
        run: npm pack

      - name: Test publish (dry run for manual triggers)
        if: github.event_name == 'workflow_dispatch'
        run: |
          echo "🧪 Testing package creation (dry run)"
          npm publish --dry-run --access public

      - name: Publish to NPM (production releases only)
        if: github.event_name == 'push'
        run: |
          VERSION="${{ steps.get_version.outputs.VERSION }}"
          # Skip if this exact version is already published (idempotent reruns)
          if npm view xcodebuildmcp@"$VERSION" version >/dev/null 2>&1; then
            echo "✅ xcodebuildmcp@$VERSION already on NPM. Skipping publish."
            exit 0
          fi
          # Determine the appropriate npm tag based on version
          if [[ "$VERSION" == *"-beta"* ]]; then
            NPM_TAG="beta"
          elif [[ "$VERSION" == *"-alpha"* ]]; then
            NPM_TAG="alpha"
          elif [[ "$VERSION" == *"-rc"* ]]; then
            NPM_TAG="rc"
          else
            # For stable releases, explicitly use latest tag
            NPM_TAG="latest"
          fi
          echo "📦 Publishing to NPM with tag: $NPM_TAG"
          npm publish --access public --tag "$NPM_TAG"

      - name: Create GitHub Release (production releases only)
        if: github.event_name == 'push'
        uses: softprops/action-gh-release@v1
        with:
          tag_name: v${{ steps.get_version.outputs.VERSION }}
          name: Release v${{ steps.get_version.outputs.VERSION }}
          body: |
            ## Release v${{ steps.get_version.outputs.VERSION }}

            ### Installation
            ```bash
            npm install -g xcodebuildmcp@${{ steps.get_version.outputs.VERSION }}
            ```

            Or use with npx:
            ```bash
            npx xcodebuildmcp@${{ steps.get_version.outputs.VERSION }}
            ```

            📦 **NPM Package**: https://www.npmjs.com/package/xcodebuildmcp/v/${{ steps.get_version.outputs.VERSION }}
          files: |
            xcodebuildmcp-${{ steps.get_version.outputs.VERSION }}.tgz
          draft: false
          prerelease: false

      - name: Summary
        run: |
          if [ "${{ steps.get_version.outputs.IS_TEST }}" = "true" ]; then
            echo "🧪 Test completed for version: ${{ steps.get_version.outputs.VERSION }}"
            echo "Ready for production release!"
          else
            echo "🎉 Production release completed!"
            echo "Version: ${{ steps.get_version.outputs.VERSION }}"
            echo "📦 NPM: https://www.npmjs.com/package/xcodebuildmcp/v/${{ steps.get_version.outputs.VERSION }}"
            echo "📚 MCP Registry: publish attempted in separate job (mcp_registry)"
          fi

  mcp_registry:
    if: github.event_name == 'push'
    needs: release
    runs-on: ubuntu-latest
    env:
      MCP_DNS_PRIVATE_KEY: ${{ secrets.MCP_DNS_PRIVATE_KEY }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get version from tag
        id: get_version_mcp
        run: |
          VERSION=${GITHUB_REF#refs/tags/v}
          echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
          echo "🚢 MCP publish for version: $VERSION"

      - name: Missing secret — skip MCP publish
        if: env.MCP_DNS_PRIVATE_KEY == ''
        run: |
          echo "⚠️ Skipping MCP Registry publish: secrets.MCP_DNS_PRIVATE_KEY is not set."
          echo "This is optional and does not affect the release."

      - name: Setup Go (for MCP Publisher)
        if: env.MCP_DNS_PRIVATE_KEY != ''
        uses: actions/setup-go@v5
        with:
          go-version: '1.22'

      - name: Install MCP Publisher
        if: env.MCP_DNS_PRIVATE_KEY != ''
        run: |
          echo "📥 Fetching MCP Publisher"
          git clone https://github.com/modelcontextprotocol/registry publisher-repo
          cd publisher-repo
          make publisher
          cp bin/mcp-publisher ../mcp-publisher
          cd ..
          chmod +x mcp-publisher

      - name: Login to MCP Registry (DNS)
        if: env.MCP_DNS_PRIVATE_KEY != ''
        run: |
          echo "🔐 Using DNS authentication for com.xcodebuildmcp/* namespace"
          ./mcp-publisher login dns --domain xcodebuildmcp.com --private-key "${MCP_DNS_PRIVATE_KEY}"

      - name: Publish to MCP Registry (best-effort)
        if: env.MCP_DNS_PRIVATE_KEY != ''
        run: |
          echo "🚢 Publishing to MCP Registry with retries..."
          attempts=0
          max_attempts=5
          delay=5
          until ./mcp-publisher publish; do
            rc=$?
            attempts=$((attempts+1))
            if [ $attempts -ge $max_attempts ]; then
              echo "⚠️ MCP Registry publish failed after $attempts attempts (exit $rc). Skipping without failing workflow."
              exit 0
            fi
            echo "⚠️ Publish failed (exit $rc). Retrying in ${delay}s... (attempt ${attempts}/${max_attempts})"
            sleep $delay
            delay=$((delay*2))
          done
          echo "✅ MCP Registry publish succeeded."

```

--------------------------------------------------------------------------------
/src/utils/log_capture.ts:
--------------------------------------------------------------------------------

```typescript
import * as path from 'path';
import type { ChildProcess } from 'child_process';
import { v4 as uuidv4 } from 'uuid';
import { log } from '../utils/logger.ts';
import {
  CommandExecutor,
  getDefaultCommandExecutor,
  getDefaultFileSystemExecutor,
} from './command.ts';
import { FileSystemExecutor } from './FileSystemExecutor.ts';

/**
 * Log file retention policy:
 * - Old log files (older than LOG_RETENTION_DAYS) are automatically deleted from the temp directory
 * - Cleanup runs on every new log capture start
 */
const LOG_RETENTION_DAYS = 3;
const LOG_FILE_PREFIX = 'xcodemcp_sim_log_';

export interface LogSession {
  processes: ChildProcess[];
  logFilePath: string;
  simulatorUuid: string;
  bundleId: string;
}

export const activeLogSessions: Map<string, LogSession> = new Map();

/**
 * Start a log capture session for an iOS simulator.
 * Returns { sessionId, logFilePath, processes, error? }
 */
export async function startLogCapture(
  params: {
    simulatorUuid: string;
    bundleId: string;
    captureConsole?: boolean;
    args?: string[];
  },
  executor: CommandExecutor = getDefaultCommandExecutor(),
  fileSystem: FileSystemExecutor = getDefaultFileSystemExecutor(),
): Promise<{ sessionId: string; logFilePath: string; processes: ChildProcess[]; error?: string }> {
  // Clean up old logs before starting a new session
  await cleanOldLogs(fileSystem);

  const { simulatorUuid, bundleId, captureConsole = false, args = [] } = params;
  const logSessionId = uuidv4();
  const logFileName = `${LOG_FILE_PREFIX}${logSessionId}.log`;
  const logFilePath = path.join(fileSystem.tmpdir(), logFileName);

  try {
    await fileSystem.mkdir(fileSystem.tmpdir(), { recursive: true });
    await fileSystem.writeFile(logFilePath, '');
    const logStream = fileSystem.createWriteStream(logFilePath, { flags: 'a' });
    const processes: ChildProcess[] = [];
    logStream.write('\n--- Log capture for bundle ID: ' + bundleId + ' ---\n');

    if (captureConsole) {
      const launchCommand = [
        'xcrun',
        'simctl',
        'launch',
        '--console-pty',
        '--terminate-running-process',
        simulatorUuid,
        bundleId,
      ];
      if (args.length > 0) {
        launchCommand.push(...args);
      }

      const stdoutLogResult = await executor(
        launchCommand,
        'Console Log Capture',
        true, // useShell
        undefined, // env
        true, // detached - don't wait for this streaming process to complete
      );

      if (!stdoutLogResult.success) {
        return {
          sessionId: '',
          logFilePath: '',
          processes: [],
          error: stdoutLogResult.error ?? 'Failed to start console log capture',
        };
      }

      stdoutLogResult.process.stdout?.pipe(logStream);
      stdoutLogResult.process.stderr?.pipe(logStream);
      processes.push(stdoutLogResult.process);
    }

    const osLogResult = await executor(
      [
        'xcrun',
        'simctl',
        'spawn',
        simulatorUuid,
        'log',
        'stream',
        '--level=debug',
        '--predicate',
        `subsystem == "${bundleId}"`,
      ],
      'OS Log Capture',
      true, // useShell
      undefined, // env
      true, // detached - don't wait for this streaming process to complete
    );

    if (!osLogResult.success) {
      return {
        sessionId: '',
        logFilePath: '',
        processes: [],
        error: osLogResult.error ?? 'Failed to start OS log capture',
      };
    }

    osLogResult.process.stdout?.pipe(logStream);
    osLogResult.process.stderr?.pipe(logStream);
    processes.push(osLogResult.process);

    for (const process of processes) {
      process.on('close', (code) => {
        log('info', `A log capture process for session ${logSessionId} exited with code ${code}.`);
      });
    }

    activeLogSessions.set(logSessionId, {
      processes,
      logFilePath,
      simulatorUuid,
      bundleId,
    });

    log('info', `Log capture started with session ID: ${logSessionId}`);
    return { sessionId: logSessionId, logFilePath, processes };
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    log('error', `Failed to start log capture: ${message}`);
    return { sessionId: '', logFilePath: '', processes: [], error: message };
  }
}

/**
 * Stop a log capture session and retrieve the log content.
 */
export async function stopLogCapture(
  logSessionId: string,
  fileSystem: FileSystemExecutor = getDefaultFileSystemExecutor(),
): Promise<{ logContent: string; error?: string }> {
  const session = activeLogSessions.get(logSessionId);
  if (!session) {
    log('warning', `Log session not found: ${logSessionId}`);
    return { logContent: '', error: `Log capture session not found: ${logSessionId}` };
  }

  try {
    log('info', `Attempting to stop log capture session: ${logSessionId}`);
    const logFilePath = session.logFilePath;
    for (const process of session.processes) {
      if (!process.killed && process.exitCode === null) {
        process.kill('SIGTERM');
      }
    }
    activeLogSessions.delete(logSessionId);
    log(
      'info',
      `Log capture session ${logSessionId} stopped. Log file retained at: ${logFilePath}`,
    );
    if (!fileSystem.existsSync(logFilePath)) {
      throw new Error(`Log file not found: ${logFilePath}`);
    }
    const fileContent = await fileSystem.readFile(logFilePath, 'utf-8');
    log('info', `Successfully read log content from ${logFilePath}`);
    return { logContent: fileContent };
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    log('error', `Failed to stop log capture session ${logSessionId}: ${message}`);
    return { logContent: '', error: message };
  }
}

/**
 * Deletes log files older than LOG_RETENTION_DAYS from the temp directory.
 * Runs quietly; errors are logged but do not throw.
 */
async function cleanOldLogs(fileSystem: FileSystemExecutor): Promise<void> {
  const tempDir = fileSystem.tmpdir();
  let files: unknown[];
  try {
    files = await fileSystem.readdir(tempDir);
  } catch (err) {
    log(
      'warn',
      `Could not read temp dir for log cleanup: ${err instanceof Error ? err.message : String(err)}`,
    );
    return;
  }
  const now = Date.now();
  const retentionMs = LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
  const fileNames = files.filter((file): file is string => typeof file === 'string');

  await Promise.all(
    fileNames
      .filter((f) => f.startsWith(LOG_FILE_PREFIX) && f.endsWith('.log'))
      .map(async (f) => {
        const filePath = path.join(tempDir, f);
        try {
          const stat = await fileSystem.stat(filePath);
          if (now - stat.mtimeMs > retentionMs) {
            await fileSystem.rm(filePath, { force: true });
            log('info', `Deleted old log file: ${filePath}`);
          }
        } catch (err) {
          log(
            'warn',
            `Error during log cleanup for ${filePath}: ${err instanceof Error ? err.message : String(err)}`,
          );
        }
      }),
  );
}

```

--------------------------------------------------------------------------------
/src/mcp/tools/ui-testing/describe_ui.ts:
--------------------------------------------------------------------------------

```typescript
import * as z from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { createErrorResponse } from '../../../utils/responses/index.ts';
import { DependencyError, AxeError, SystemError } from '../../../utils/errors.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultDebuggerManager } from '../../../utils/debugger/index.ts';
import type { DebuggerManager } from '../../../utils/debugger/debugger-manager.ts';
import { guardUiAutomationAgainstStoppedDebugger } from '../../../utils/debugger/ui-automation-guard.ts';
import {
  createAxeNotAvailableResponse,
  getAxePath,
  getBundledAxeEnvironment,
} from '../../../utils/axe-helpers.ts';
import {
  createSessionAwareTool,
  getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';

// Define schema as ZodObject
const describeUiSchema = z.object({
  simulatorId: z.uuid({ message: 'Invalid Simulator UUID format' }),
});

// Use z.infer for type safety
type DescribeUiParams = z.infer<typeof describeUiSchema>;

export interface AxeHelpers {
  getAxePath: () => string | null;
  getBundledAxeEnvironment: () => Record<string, string>;
  createAxeNotAvailableResponse: () => ToolResponse;
}

const LOG_PREFIX = '[AXe]';

// Session tracking for describe_ui warnings (shared across UI tools)
const describeUITimestamps = new Map<string, { timestamp: number; simulatorId: string }>();

function recordDescribeUICall(simulatorId: string): void {
  describeUITimestamps.set(simulatorId, {
    timestamp: Date.now(),
    simulatorId,
  });
}

/**
 * Core business logic for describe_ui functionality
 */
export async function describe_uiLogic(
  params: DescribeUiParams,
  executor: CommandExecutor,
  axeHelpers: AxeHelpers = {
    getAxePath,
    getBundledAxeEnvironment,
    createAxeNotAvailableResponse,
  },
  debuggerManager: DebuggerManager = getDefaultDebuggerManager(),
): Promise<ToolResponse> {
  const toolName = 'describe_ui';
  const { simulatorId } = params;
  const commandArgs = ['describe-ui'];

  const guard = await guardUiAutomationAgainstStoppedDebugger({
    debugger: debuggerManager,
    simulatorId,
    toolName,
  });
  if (guard.blockedResponse) return guard.blockedResponse;

  log('info', `${LOG_PREFIX}/${toolName}: Starting for ${simulatorId}`);

  try {
    const responseText = await executeAxeCommand(
      commandArgs,
      simulatorId,
      'describe-ui',
      executor,
      axeHelpers,
    );

    // Record the describe_ui call for warning system
    recordDescribeUICall(simulatorId);

    log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`);
    const response: ToolResponse = {
      content: [
        {
          type: 'text',
          text:
            'Accessibility hierarchy retrieved successfully:\n```json\n' + responseText + '\n```',
        },
        {
          type: 'text',
          text: `Next Steps:
- Use frame coordinates for tap/swipe (center: x+width/2, y+height/2)
- Re-run describe_ui after layout changes
- If a debugger is attached, ensure the app is running (not stopped on breakpoints)
- Screenshots are for visual verification only`,
        },
      ],
    };
    if (guard.warningText) {
      response.content.push({ type: 'text', text: guard.warningText });
    }
    return response;
  } catch (error) {
    log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
    if (error instanceof DependencyError) {
      return axeHelpers.createAxeNotAvailableResponse();
    } else if (error instanceof AxeError) {
      return createErrorResponse(
        `Failed to get accessibility hierarchy: ${error.message}`,
        error.axeOutput,
      );
    } else if (error instanceof SystemError) {
      return createErrorResponse(
        `System error executing axe: ${error.message}`,
        error.originalError?.stack,
      );
    }
    return createErrorResponse(
      `An unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

const publicSchemaObject = z.strictObject(
  describeUiSchema.omit({ simulatorId: true } as const).shape,
);

export default {
  name: 'describe_ui',
  description:
    'Gets entire view hierarchy with precise frame coordinates (x, y, width, height) for all visible elements. Use this before UI interactions or after layout changes - do NOT guess coordinates from screenshots. Returns JSON tree with frame data for accurate automation. Requires the target process to be running; paused debugger/breakpoints can yield an empty tree.',
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: describeUiSchema,
  }),
  annotations: {
    title: 'Describe UI',
    readOnlyHint: true,
  },
  handler: createSessionAwareTool<DescribeUiParams>({
    internalSchema: describeUiSchema as unknown as z.ZodType<DescribeUiParams, unknown>,
    logicFunction: (params: DescribeUiParams, executor: CommandExecutor) =>
      describe_uiLogic(params, executor, {
        getAxePath,
        getBundledAxeEnvironment,
        createAxeNotAvailableResponse,
      }),
    getExecutor: getDefaultCommandExecutor,
    requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
  }),
};

// Helper function for executing axe commands (inlined from src/tools/axe/index.ts)
async function executeAxeCommand(
  commandArgs: string[],
  simulatorId: string,
  commandName: string,
  executor: CommandExecutor = getDefaultCommandExecutor(),
  axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse },
): Promise<string> {
  // Get the appropriate axe binary path
  const axeBinary = axeHelpers.getAxePath();
  if (!axeBinary) {
    throw new DependencyError('AXe binary not found');
  }

  // Add --udid parameter to all commands
  const fullArgs = [...commandArgs, '--udid', simulatorId];

  // Construct the full command array with the axe binary as the first element
  const fullCommand = [axeBinary, ...fullArgs];

  try {
    // Determine environment variables for bundled AXe
    const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined;

    const result = await executor(
      fullCommand,
      `${LOG_PREFIX}: ${commandName}`,
      false,
      axeEnv ? { env: axeEnv } : undefined,
    );

    if (!result.success) {
      throw new AxeError(
        `axe command '${commandName}' failed.`,
        commandName,
        result.error ?? result.output,
        simulatorId,
      );
    }

    // Check for stderr output in successful commands
    if (result.error) {
      log(
        'warn',
        `${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${result.error}`,
      );
    }

    return result.output.trim();
  } catch (error) {
    if (error instanceof Error) {
      if (error instanceof AxeError) {
        throw error;
      }

      // Otherwise wrap it in a SystemError
      throw new SystemError(`Failed to execute axe command: ${error.message}`, error);
    }

    // For any other type of error
    throw new SystemError(`Failed to execute axe command: ${String(error)}`);
  }
}

```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/record_sim_video.ts:
--------------------------------------------------------------------------------

```typescript
import * as z from 'zod';
import type { ToolResponse } from '../../../types/common.ts';
import { createTextResponse } from '../../../utils/responses/index.ts';
import {
  getDefaultCommandExecutor,
  getDefaultFileSystemExecutor,
} from '../../../utils/execution/index.ts';
import type { CommandExecutor, FileSystemExecutor } from '../../../utils/execution/index.ts';
import {
  areAxeToolsAvailable,
  isAxeAtLeastVersion,
  createAxeNotAvailableResponse,
} from '../../../utils/axe/index.ts';
import {
  startSimulatorVideoCapture,
  stopSimulatorVideoCapture,
} from '../../../utils/video-capture/index.ts';
import {
  createSessionAwareTool,
  getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
import { dirname } from 'path';

// Base schema object (used for MCP schema exposure)
const recordSimVideoSchemaObject = z.object({
  simulatorId: z
    .uuid({ message: 'Invalid Simulator UUID format' })
    .describe('UUID of the simulator to record'),
  start: z.boolean().optional().describe('Start recording if true'),
  stop: z.boolean().optional().describe('Stop recording if true'),
  fps: z.number().int().min(1).max(120).optional().describe('Frames per second (default 30)'),
  outputFile: z
    .string()
    .optional()
    .describe('Destination MP4 path to move the recorded video to on stop'),
});

// Schema enforcing mutually exclusive start/stop and requiring outputFile on stop
const recordSimVideoSchema = recordSimVideoSchemaObject
  .refine(
    (v) => {
      const s = v.start === true ? 1 : 0;
      const t = v.stop === true ? 1 : 0;
      return s + t === 1;
    },
    {
      message:
        'Provide exactly one of start=true or stop=true; these options are mutually exclusive',
      path: ['start'],
    },
  )
  .refine((v) => (v.stop ? typeof v.outputFile === 'string' && v.outputFile.length > 0 : true), {
    message: 'outputFile is required when stop=true',
    path: ['outputFile'],
  });

type RecordSimVideoParams = z.infer<typeof recordSimVideoSchema>;

export async function record_sim_videoLogic(
  params: RecordSimVideoParams,
  executor: CommandExecutor,
  axe: {
    areAxeToolsAvailable(): boolean;
    isAxeAtLeastVersion(v: string, e: CommandExecutor): Promise<boolean>;
    createAxeNotAvailableResponse(): ToolResponse;
  } = {
    areAxeToolsAvailable,
    isAxeAtLeastVersion,
    createAxeNotAvailableResponse,
  },
  video: {
    startSimulatorVideoCapture: typeof startSimulatorVideoCapture;
    stopSimulatorVideoCapture: typeof stopSimulatorVideoCapture;
  } = {
    startSimulatorVideoCapture,
    stopSimulatorVideoCapture,
  },
  fs: FileSystemExecutor = getDefaultFileSystemExecutor(),
): Promise<ToolResponse> {
  // Preflight checks for AXe availability and version
  if (!axe.areAxeToolsAvailable()) {
    return axe.createAxeNotAvailableResponse();
  }
  const hasVersion = await axe.isAxeAtLeastVersion('1.1.0', executor);
  if (!hasVersion) {
    return createTextResponse(
      'AXe v1.1.0 or newer is required for simulator video capture. Please update bundled AXe artifacts.',
      true,
    );
  }

  // using injected fs executor

  if (params.start) {
    const fpsUsed = Number.isFinite(params.fps as number) ? Number(params.fps) : 30;
    const startRes = await video.startSimulatorVideoCapture(
      { simulatorUuid: params.simulatorId, fps: fpsUsed },
      executor,
    );

    if (!startRes.started) {
      return createTextResponse(
        `Failed to start video recording: ${startRes.error ?? 'Unknown error'}`,
        true,
      );
    }

    const notes: string[] = [];
    if (typeof params.outputFile === 'string' && params.outputFile.length > 0) {
      notes.push(
        'Note: outputFile is ignored when start=true; provide it when stopping to move/rename the recorded file.',
      );
    }
    if (startRes.warning) {
      notes.push(startRes.warning);
    }

    const nextSteps = `Next Steps:
Stop and save the recording:
record_sim_video({ simulatorId: "${params.simulatorId}", stop: true, outputFile: "/path/to/output.mp4" })`;

    return {
      content: [
        {
          type: 'text',
          text: `🎥 Video recording started for simulator ${params.simulatorId} at ${fpsUsed} fps.\nSession: ${startRes.sessionId}`,
        },
        ...(notes.length > 0
          ? [
              {
                type: 'text' as const,
                text: notes.join('\n'),
              },
            ]
          : []),
        {
          type: 'text',
          text: nextSteps,
        },
      ],
      isError: false,
    };
  }

  // params.stop must be true here per schema
  const stopRes = await video.stopSimulatorVideoCapture(
    { simulatorUuid: params.simulatorId },
    executor,
  );

  if (!stopRes.stopped) {
    return createTextResponse(
      `Failed to stop video recording: ${stopRes.error ?? 'Unknown error'}`,
      true,
    );
  }

  // Attempt to move/rename the recording if we parsed a source path and an outputFile was given
  const outputs: string[] = [];
  let finalSavedPath = params.outputFile ?? stopRes.parsedPath ?? '';
  try {
    if (params.outputFile) {
      if (!stopRes.parsedPath) {
        return createTextResponse(
          `Recording stopped but could not determine the recorded file path from AXe output.\nRaw output:\n${stopRes.stdout ?? '(no output captured)'}`,
          true,
        );
      }

      const src = stopRes.parsedPath;
      const dest = params.outputFile;
      await fs.mkdir(dirname(dest), { recursive: true });
      await fs.cp(src, dest);
      try {
        await fs.rm(src, { recursive: false });
      } catch {
        // Ignore cleanup failure
      }
      finalSavedPath = dest;

      outputs.push(`Original file: ${src}`);
      outputs.push(`Saved to: ${dest}`);
    } else if (stopRes.parsedPath) {
      outputs.push(`Saved to: ${stopRes.parsedPath}`);
      finalSavedPath = stopRes.parsedPath;
    }
  } catch (e) {
    const msg = e instanceof Error ? e.message : String(e);
    return createTextResponse(
      `Recording stopped but failed to save/move the video file: ${msg}`,
      true,
    );
  }

  return {
    content: [
      {
        type: 'text',
        text: `✅ Video recording stopped for simulator ${params.simulatorId}.`,
      },
      ...(outputs.length > 0
        ? [
            {
              type: 'text' as const,
              text: outputs.join('\n'),
            },
          ]
        : []),
      ...(!outputs.length && stopRes.stdout
        ? [
            {
              type: 'text' as const,
              text: `AXe output:\n${stopRes.stdout}`,
            },
          ]
        : []),
    ],
    isError: false,
    _meta: finalSavedPath ? { outputFile: finalSavedPath } : undefined,
  };
}

const publicSchemaObject = z.strictObject(
  recordSimVideoSchemaObject.omit({ simulatorId: true } as const).shape,
);

export default {
  name: 'record_sim_video',
  description: 'Starts or stops video capture for an iOS simulator.',
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: recordSimVideoSchemaObject,
  }),
  annotations: {
    title: 'Record Simulator Video',
    destructiveHint: true,
  },
  handler: createSessionAwareTool<RecordSimVideoParams>({
    internalSchema: recordSimVideoSchema as unknown as z.ZodType<RecordSimVideoParams, unknown>,
    logicFunction: record_sim_videoLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
  }),
};

```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator-management/__tests__/sim_statusbar.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tests for sim_statusbar plugin
 * Following CLAUDE.md testing standards with literal validation
 * Using dependency injection for deterministic testing
 */

import { describe, it, expect } from 'vitest';
import * as z from 'zod';
import {
  createMockCommandResponse,
  createMockExecutor,
  type CommandExecutor,
} from '../../../../test-utils/mock-executors.ts';
import simStatusbar, { sim_statusbarLogic } from '../sim_statusbar.ts';

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

    it('should have correct description', () => {
      expect(simStatusbar.description).toBe(
        'Sets the data network indicator in the iOS simulator status bar. Use "clear" to reset all overrides, or specify a network type (hide, wifi, 3g, 4g, lte, lte-a, lte+, 5g, 5g+, 5g-uwb, 5g-uc).',
      );
    });

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

    it('should expose public schema without simulatorId field', () => {
      const schema = z.object(simStatusbar.schema);

      expect(schema.safeParse({ dataNetwork: 'wifi' }).success).toBe(true);
      expect(schema.safeParse({ dataNetwork: 'clear' }).success).toBe(true);
      expect(schema.safeParse({ dataNetwork: 'invalid' }).success).toBe(false);

      const withSimId = schema.safeParse({ simulatorId: 'test-uuid', dataNetwork: 'wifi' });
      expect(withSimId.success).toBe(true);
      expect('simulatorId' in (withSimId.data as any)).toBe(false);
    });
  });

  describe('Handler Behavior (Complete Literal Returns)', () => {
    it('should handle successful status bar data network setting', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Status bar set successfully',
      });

      const result = await sim_statusbarLogic(
        {
          simulatorId: 'test-uuid-123',
          dataNetwork: 'wifi',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Successfully set simulator test-uuid-123 status bar data network to wifi',
          },
        ],
      });
    });

    it('should handle minimal valid parameters (Zod handles validation)', async () => {
      // Note: With createTypedTool, Zod validation happens before the logic function is called
      // So we test with a valid minimal parameter set since validation is handled upstream
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Status bar set successfully',
      });

      const result = await sim_statusbarLogic(
        {
          simulatorId: 'test-uuid-123',
          dataNetwork: 'wifi',
        },
        mockExecutor,
      );

      // The logic function should execute normally with valid parameters
      // Zod validation errors are handled by createTypedTool wrapper
      expect(result.isError).toBe(undefined);
      expect(result.content[0].text).toContain('Successfully set simulator');
    });

    it('should handle command failure', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Simulator not found',
      });

      const result = await sim_statusbarLogic(
        {
          simulatorId: 'invalid-uuid',
          dataNetwork: '3g',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Failed to set status bar: Simulator not found',
          },
        ],
        isError: true,
      });
    });

    it('should handle exception with Error object', async () => {
      const mockExecutor: CommandExecutor = async () => {
        throw new Error('Connection failed');
      };

      const result = await sim_statusbarLogic(
        {
          simulatorId: 'test-uuid-123',
          dataNetwork: '4g',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Failed to set status bar: Connection failed',
          },
        ],
        isError: true,
      });
    });

    it('should handle exception with string error', async () => {
      const mockExecutor: CommandExecutor = async () => {
        throw 'String error';
      };

      const result = await sim_statusbarLogic(
        {
          simulatorId: 'test-uuid-123',
          dataNetwork: 'lte',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Failed to set status bar: String error',
          },
        ],
        isError: true,
      });
    });

    it('should verify command generation with mock executor for override', async () => {
      const calls: Array<{
        command: string[];
        operationDescription?: string;
        keepAlive?: boolean;
        opts?: { cwd?: string };
      }> = [];

      const mockExecutor: CommandExecutor = async (
        command,
        operationDescription,
        keepAlive,
        opts,
        detached,
      ) => {
        calls.push({ command, operationDescription, keepAlive, opts });
        void detached;
        return createMockCommandResponse({
          success: true,
          output: 'Status bar set successfully',
          error: undefined,
        });
      };

      await sim_statusbarLogic(
        {
          simulatorId: 'test-uuid-123',
          dataNetwork: 'wifi',
        },
        mockExecutor,
      );

      expect(calls).toHaveLength(1);
      expect(calls[0]).toEqual({
        command: [
          'xcrun',
          'simctl',
          'status_bar',
          'test-uuid-123',
          'override',
          '--dataNetwork',
          'wifi',
        ],
        operationDescription: 'Set Status Bar',
        keepAlive: true,
        opts: undefined,
      });
    });

    it('should verify command generation for clear operation', async () => {
      const calls: Array<{
        command: string[];
        operationDescription?: string;
        keepAlive?: boolean;
        opts?: { cwd?: string };
      }> = [];

      const mockExecutor: CommandExecutor = async (
        command,
        operationDescription,
        keepAlive,
        opts,
        detached,
      ) => {
        calls.push({ command, operationDescription, keepAlive, opts });
        void detached;
        return createMockCommandResponse({
          success: true,
          output: 'Status bar cleared successfully',
          error: undefined,
        });
      };

      await sim_statusbarLogic(
        {
          simulatorId: 'test-uuid-123',
          dataNetwork: 'clear',
        },
        mockExecutor,
      );

      expect(calls).toHaveLength(1);
      expect(calls[0]).toEqual({
        command: ['xcrun', 'simctl', 'status_bar', 'test-uuid-123', 'clear'],
        operationDescription: 'Set Status Bar',
        keepAlive: true,
        opts: undefined,
      });
    });

    it('should handle successful clear operation', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Status bar cleared successfully',
      });

      const result = await sim_statusbarLogic(
        {
          simulatorId: 'test-uuid-123',
          dataNetwork: 'clear',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Successfully cleared status bar overrides for simulator test-uuid-123',
          },
        ],
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/src/utils/xcodemake.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * xcodemake Utilities - Support for using xcodemake as an alternative build strategy
 *
 * This utility module provides functions for using xcodemake (https://github.com/johnno1962/xcodemake)
 * as an alternative build strategy for Xcode projects. xcodemake logs xcodebuild output to generate
 * a Makefile for an Xcode project, allowing for faster incremental builds using the "make" command.
 *
 * Responsibilities:
 * - Checking if xcodemake is enabled via environment variable
 * - Executing xcodemake commands with proper argument handling
 * - Converting xcodebuild arguments to xcodemake arguments
 * - Handling xcodemake-specific output and error reporting
 * - Auto-downloading xcodemake if enabled but not found
 */

import { log } from './logger.ts';
import { CommandResponse, getDefaultCommandExecutor } from './command.ts';
import { existsSync, readdirSync } from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs/promises';

// Environment variable to control xcodemake usage
export const XCODEMAKE_ENV_VAR = 'INCREMENTAL_BUILDS_ENABLED';

// Store the overridden path for xcodemake if needed
let overriddenXcodemakePath: string | null = null;

/**
 * Check if xcodemake is enabled via environment variable
 * @returns boolean indicating if xcodemake should be used
 */
export function isXcodemakeEnabled(): boolean {
  const envValue = process.env[XCODEMAKE_ENV_VAR];
  return envValue === '1' || envValue === 'true' || envValue === 'yes';
}

/**
 * Get the xcodemake command to use
 * @returns The command string for xcodemake
 */
function getXcodemakeCommand(): string {
  return overriddenXcodemakePath ?? 'xcodemake';
}

/**
 * Override the xcodemake command path
 * @param path Path to the xcodemake executable
 */
function overrideXcodemakeCommand(path: string): void {
  overriddenXcodemakePath = path;
  log('info', `Using overridden xcodemake path: ${path}`);
}

/**
 * Install xcodemake by downloading it from GitHub
 * @returns Promise resolving to boolean indicating if installation was successful
 */
async function installXcodemake(): Promise<boolean> {
  const tempDir = os.tmpdir();
  const xcodemakeDir = path.join(tempDir, 'xcodebuildmcp');
  const xcodemakePath = path.join(xcodemakeDir, 'xcodemake');

  log('info', `Attempting to install xcodemake to ${xcodemakePath}`);

  try {
    // Create directory if it doesn't exist
    await fs.mkdir(xcodemakeDir, { recursive: true });

    // Download the script
    log('info', 'Downloading xcodemake from GitHub...');
    const response = await fetch(
      'https://raw.githubusercontent.com/cameroncooke/xcodemake/main/xcodemake',
    );

    if (!response.ok) {
      throw new Error(`Failed to download xcodemake: ${response.status} ${response.statusText}`);
    }

    const scriptContent = await response.text();
    await fs.writeFile(xcodemakePath, scriptContent, 'utf8');

    // Make executable
    await fs.chmod(xcodemakePath, 0o755);
    log('info', 'Made xcodemake executable');

    // Override the command to use the direct path
    overrideXcodemakeCommand(xcodemakePath);

    return true;
  } catch (error) {
    log(
      'error',
      `Error installing xcodemake: ${error instanceof Error ? error.message : String(error)}`,
    );
    return false;
  }
}

/**
 * Check if xcodemake is installed and available. If enabled but not available, attempts to download it.
 * @returns Promise resolving to boolean indicating if xcodemake is available
 */
export async function isXcodemakeAvailable(): Promise<boolean> {
  // First check if xcodemake is enabled, if not, no need to check or install
  if (!isXcodemakeEnabled()) {
    log('debug', 'xcodemake is not enabled, skipping availability check');
    return false;
  }

  try {
    // Check if we already have an overridden path
    if (overriddenXcodemakePath && existsSync(overriddenXcodemakePath)) {
      log('debug', `xcodemake found at overridden path: ${overriddenXcodemakePath}`);
      return true;
    }

    // Check if xcodemake is available in PATH
    const result = await getDefaultCommandExecutor()(['which', 'xcodemake']);
    if (result.success) {
      log('debug', 'xcodemake found in PATH');
      return true;
    }

    // If not found, download and install it
    log('info', 'xcodemake not found in PATH, attempting to download...');
    const installed = await installXcodemake();
    if (installed) {
      log('info', 'xcodemake installed successfully');
      return true;
    } else {
      log('warn', 'xcodemake installation failed');
      return false;
    }
  } catch (error) {
    log(
      'error',
      `Error checking for xcodemake: ${error instanceof Error ? error.message : String(error)}`,
    );
    return false;
  }
}

/**
 * Check if a Makefile exists in the current directory
 * @returns boolean indicating if a Makefile exists
 */
export function doesMakefileExist(projectDir: string): boolean {
  return existsSync(`${projectDir}/Makefile`);
}

/**
 * Check if a Makefile log exists in the current directory
 * @param projectDir Directory containing the Makefile
 * @param command Command array to check for log file
 * @returns boolean indicating if a Makefile log exists
 */
export function doesMakeLogFileExist(projectDir: string, command: string[]): boolean {
  // Change to the project directory as xcodemake requires being in the project dir
  const originalDir = process.cwd();

  try {
    process.chdir(projectDir);

    // Construct the expected log filename
    const xcodemakeCommand = ['xcodemake', ...command.slice(1)];
    const escapedCommand = xcodemakeCommand.map((arg) => {
      // Remove projectDir from arguments if present at the start
      const prefix = projectDir + '/';
      if (arg.startsWith(prefix)) {
        return arg.substring(prefix.length);
      }
      return arg;
    });
    const commandString = escapedCommand.join(' ');
    const logFileName = `${commandString}.log`;
    log('debug', `Checking for Makefile log: ${logFileName} in directory: ${process.cwd()}`);

    // Read directory contents and check if the file exists
    const files = readdirSync('.');
    const exists = files.includes(logFileName);
    log('debug', `Makefile log ${exists ? 'exists' : 'does not exist'}: ${logFileName}`);
    return exists;
  } catch (error) {
    // Log potential errors like directory not found, permissions issues, etc.
    log(
      'error',
      `Error checking for Makefile log: ${error instanceof Error ? error.message : String(error)}`,
    );
    return false;
  } finally {
    // Always restore the original directory
    process.chdir(originalDir);
  }
}

/**
 * Execute an xcodemake command to generate a Makefile
 * @param buildArgs Build arguments to pass to xcodemake (without the 'xcodebuild' command)
 * @param logPrefix Prefix for logging
 * @returns Promise resolving to command response
 */
export async function executeXcodemakeCommand(
  projectDir: string,
  buildArgs: string[],
  logPrefix: string,
): Promise<CommandResponse> {
  // Change directory to project directory, this is needed for xcodemake to work
  process.chdir(projectDir);

  const xcodemakeCommand = [getXcodemakeCommand(), ...buildArgs];

  // Remove projectDir from arguments
  const command = xcodemakeCommand.map((arg) => arg.replace(projectDir + '/', ''));

  return getDefaultCommandExecutor()(command, logPrefix);
}

/**
 * Execute a make command for incremental builds
 * @param projectDir Directory containing the Makefile
 * @param logPrefix Prefix for logging
 * @returns Promise resolving to command response
 */
export async function executeMakeCommand(
  projectDir: string,
  logPrefix: string,
): Promise<CommandResponse> {
  const command = ['cd', projectDir, '&&', 'make'];
  return getDefaultCommandExecutor()(command, logPrefix);
}

```

--------------------------------------------------------------------------------
/src/utils/__tests__/session-aware-tool-factory.test.ts:
--------------------------------------------------------------------------------

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

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

  const internalSchema = z
    .object({
      scheme: z.string(),
      projectPath: z.string().optional(),
      workspacePath: z.string().optional(),
      simulatorId: z.string().optional(),
      simulatorName: z.string().optional(),
    })
    .refine((v) => !!v.projectPath !== !!v.workspacePath, {
      message: 'projectPath and workspacePath are mutually exclusive',
      path: ['projectPath'],
    })
    .refine((v) => !!v.simulatorId !== !!v.simulatorName, {
      message: 'simulatorId and simulatorName are mutually exclusive',
      path: ['simulatorId'],
    });

  type Params = z.infer<typeof internalSchema>;

  async function logic(_params: Params): Promise<import('../../types/common.ts').ToolResponse> {
    return { content: [{ type: 'text', text: 'OK' }], isError: false };
  }

  const handler = createSessionAwareTool<Params>({
    internalSchema,
    logicFunction: logic,
    getExecutor: () => createMockExecutor({ success: true }),
    requirements: [
      { allOf: ['scheme'], message: 'scheme is required' },
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
      { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
    ],
  });

  it('should merge session defaults and satisfy requirements', async () => {
    sessionStore.setDefaults({
      scheme: 'App',
      projectPath: '/path/proj.xcodeproj',
      simulatorId: 'SIM-1',
    });

    const result = await handler({});
    expect(result.isError).toBe(false);
    expect(result.content[0].text).toBe('OK');
  });

  it('should prefer explicit args over session defaults (same key wins)', async () => {
    // Create a handler that echoes the chosen scheme
    const echoHandler = createSessionAwareTool<Params>({
      internalSchema,
      logicFunction: async (params) => ({
        content: [{ type: 'text', text: params.scheme }],
        isError: false,
      }),
      getExecutor: () => createMockExecutor({ success: true }),
      requirements: [
        { allOf: ['scheme'], message: 'scheme is required' },
        { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
        {
          oneOf: ['simulatorId', 'simulatorName'],
          message: 'Provide simulatorId or simulatorName',
        },
      ],
    });

    sessionStore.setDefaults({
      scheme: 'Default',
      projectPath: '/a.xcodeproj',
      simulatorId: 'SIM-A',
    });
    const result = await echoHandler({ scheme: 'FromArgs' });
    expect(result.isError).toBe(false);
    expect(result.content[0].text).toBe('FromArgs');
  });

  it('should return friendly error when allOf requirement missing', async () => {
    const result = await handler({ projectPath: '/p.xcodeproj', simulatorId: 'SIM-1' });
    expect(result.isError).toBe(true);
    expect(result.content[0].text).toContain('Missing required session defaults');
    expect(result.content[0].text).toContain('scheme is required');
  });

  it('should return friendly error when oneOf requirement missing', async () => {
    const result = await handler({ scheme: 'App', simulatorId: 'SIM-1' });
    expect(result.isError).toBe(true);
    expect(result.content[0].text).toContain('Missing required session defaults');
    expect(result.content[0].text).toContain('Provide a project or workspace');
  });

  it('uses opt-out messaging when session defaults schema is disabled', async () => {
    const original = process.env.XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS;
    process.env.XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS = 'true';

    try {
      const result = await handler({ projectPath: '/p.xcodeproj', simulatorId: 'SIM-1' });
      expect(result.isError).toBe(true);
      const text = result.content[0].text;
      expect(text).toContain('Missing required parameters');
      expect(text).toContain('scheme is required');
      expect(text).not.toContain('session defaults');
    } finally {
      if (original === undefined) {
        delete process.env.XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS;
      } else {
        process.env.XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS = original;
      }
    }
  });

  it('should surface Zod validation errors when invalid', async () => {
    const badHandler = createSessionAwareTool<any>({
      internalSchema,
      logicFunction: logic,
      getExecutor: () => createMockExecutor({ success: true }),
    });
    const result = await badHandler({ scheme: 123 });
    expect(result.isError).toBe(true);
    expect(result.content[0].text).toContain('Parameter validation failed');
  });

  it('exclusivePairs should NOT prune session defaults when user provides null (treat as not provided)', async () => {
    const handlerWithExclusive = createSessionAwareTool<Params>({
      internalSchema,
      logicFunction: logic,
      getExecutor: () => createMockExecutor({ success: true }),
      requirements: [
        { allOf: ['scheme'], message: 'scheme is required' },
        { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
      ],
      exclusivePairs: [['projectPath', 'workspacePath']],
    });

    sessionStore.setDefaults({
      scheme: 'App',
      projectPath: '/path/proj.xcodeproj',
      simulatorId: 'SIM-1',
    });

    const res = await handlerWithExclusive({ workspacePath: null as unknown as string });
    expect(res.isError).toBe(false);
    expect(res.content[0].text).toBe('OK');
  });

  it('exclusivePairs should NOT prune when user provides undefined (key present)', async () => {
    const handlerWithExclusive = createSessionAwareTool<Params>({
      internalSchema,
      logicFunction: logic,
      getExecutor: () => createMockExecutor({ success: true }),
      requirements: [
        { allOf: ['scheme'], message: 'scheme is required' },
        { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
      ],
      exclusivePairs: [['projectPath', 'workspacePath']],
    });

    sessionStore.setDefaults({
      scheme: 'App',
      projectPath: '/path/proj.xcodeproj',
      simulatorId: 'SIM-1',
    });

    const res = await handlerWithExclusive({ workspacePath: undefined as unknown as string });
    expect(res.isError).toBe(false);
    expect(res.content[0].text).toBe('OK');
  });

  it('rejects when multiple explicit args in an exclusive pair are provided (factory-level)', async () => {
    const internalSchemaNoXor = z.object({
      scheme: z.string(),
      projectPath: z.string().optional(),
      workspacePath: z.string().optional(),
    });

    const handlerNoXor = createSessionAwareTool<z.infer<typeof internalSchemaNoXor>>({
      internalSchema: internalSchemaNoXor,
      logicFunction: (async () => ({
        content: [{ type: 'text', text: 'OK' }],
        isError: false,
      })) as any,
      getExecutor: () => createMockExecutor({ success: true }),
      requirements: [{ allOf: ['scheme'], message: 'scheme is required' }],
      exclusivePairs: [['projectPath', 'workspacePath']],
    });

    const res = await handlerNoXor({
      scheme: 'App',
      projectPath: '/path/a.xcodeproj',
      workspacePath: '/path/b.xcworkspace',
    });

    expect(res.isError).toBe(true);
    const msg = res.content[0].text;
    expect(msg).toContain('Parameter validation failed');
    expect(msg).toContain('Mutually exclusive parameters provided');
    expect(msg).toContain('projectPath');
    expect(msg).toContain('workspacePath');
  });
});

```

--------------------------------------------------------------------------------
/src/utils/validation.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Validation Utilities - Input validation and error response generation
 *
 * This utility module provides a comprehensive set of validation functions to ensure
 * that tool inputs meet expected requirements. It centralizes validation logic,
 * error message formatting, and response generation for consistent error handling
 * across the application.
 *
 * Responsibilities:
 * - Validating required parameters (validateRequiredParam)
 * - Checking parameters against allowed values (validateAllowedValues, validateEnumParam)
 * - Verifying file existence (validateFileExists)
 * - Validating logical conditions (validateCondition)
 * - Ensuring at least one of multiple parameters is provided (validateAtLeastOneParam)
 * - Creating standardized response objects for tools (createTextResponse)
 *
 * Using these validation utilities ensures consistent error messaging and helps
 * provide clear feedback to users when their inputs don't meet requirements.
 * The functions return ValidationResult objects that make it easy to chain
 * validations and generate appropriate responses.
 */

import * as fs from 'fs';
import { log } from './logger.ts';
import { ToolResponse, ValidationResult } from '../types/common.ts';
import { FileSystemExecutor } from './FileSystemExecutor.ts';
import { getDefaultEnvironmentDetector } from './environment.ts';

/**
 * Creates a text response with the given message
 * @param message The message to include in the response
 * @param isError Whether this is an error response
 * @returns A ToolResponse object with the message
 */
export function createTextResponse(message: string, isError = false): ToolResponse {
  return {
    content: [
      {
        type: 'text',
        text: message,
      },
    ],
    isError,
  };
}

/**
 * Validates that a required parameter is present
 * @param paramName Name of the parameter
 * @param paramValue Value of the parameter
 * @param helpfulMessage Optional helpful message to include in the error response
 * @returns Validation result
 */
export function validateRequiredParam(
  paramName: string,
  paramValue: unknown,
  helpfulMessage = `Required parameter '${paramName}' is missing. Please provide a value for this parameter.`,
): ValidationResult {
  if (paramValue === undefined || paramValue === null) {
    log('warning', `Required parameter '${paramName}' is missing`);
    return {
      isValid: false,
      errorResponse: createTextResponse(helpfulMessage, true),
    };
  }

  return { isValid: true };
}

/**
 * Validates that a parameter value is one of the allowed values
 * @param paramName Name of the parameter
 * @param paramValue Value of the parameter
 * @param allowedValues Array of allowed values
 * @returns Validation result
 */
export function validateAllowedValues<T>(
  paramName: string,
  paramValue: T,
  allowedValues: T[],
): ValidationResult {
  if (!allowedValues.includes(paramValue)) {
    log(
      'warning',
      `Parameter '${paramName}' has invalid value '${paramValue}'. Allowed values: ${allowedValues.join(
        ', ',
      )}`,
    );
    return {
      isValid: false,
      errorResponse: createTextResponse(
        `Parameter '${paramName}' must be one of: ${allowedValues.join(', ')}. You provided: '${paramValue}'.`,
        true,
      ),
    };
  }

  return { isValid: true };
}

/**
 * Validates that a condition is true
 * @param condition Condition to validate
 * @param message Message to include in the warning response
 * @param logWarning Whether to log a warning message
 * @returns Validation result
 */
export function validateCondition(
  condition: boolean,
  message: string,
  logWarning: boolean = true,
): ValidationResult {
  if (!condition) {
    if (logWarning) {
      log('warning', message);
    }
    return {
      isValid: false,
      warningResponse: createTextResponse(message),
    };
  }

  return { isValid: true };
}

/**
 * Validates that a file exists
 * @param filePath Path to check
 * @returns Validation result
 */
export function validateFileExists(
  filePath: string,
  fileSystem?: FileSystemExecutor,
): ValidationResult {
  const exists = fileSystem ? fileSystem.existsSync(filePath) : fs.existsSync(filePath);
  if (!exists) {
    return {
      isValid: false,
      errorResponse: createTextResponse(
        `File not found: '${filePath}'. Please check the path and try again.`,
        true,
      ),
    };
  }

  return { isValid: true };
}

/**
 * Validates that at least one of two parameters is provided
 * @param param1Name Name of the first parameter
 * @param param1Value Value of the first parameter
 * @param param2Name Name of the second parameter
 * @param param2Value Value of the second parameter
 * @returns Validation result
 */
export function validateAtLeastOneParam(
  param1Name: string,
  param1Value: unknown,
  param2Name: string,
  param2Value: unknown,
): ValidationResult {
  if (
    (param1Value === undefined || param1Value === null) &&
    (param2Value === undefined || param2Value === null)
  ) {
    log('warning', `At least one of '${param1Name}' or '${param2Name}' must be provided`);
    return {
      isValid: false,
      errorResponse: createTextResponse(
        `At least one of '${param1Name}' or '${param2Name}' must be provided.`,
        true,
      ),
    };
  }

  return { isValid: true };
}

/**
 * Validates that a parameter value is one of the allowed enum values
 * @param paramName Name of the parameter
 * @param paramValue Value of the parameter
 * @param allowedValues Array of allowed enum values
 * @returns Validation result
 */
export function validateEnumParam<T>(
  paramName: string,
  paramValue: T,
  allowedValues: T[],
): ValidationResult {
  if (!allowedValues.includes(paramValue)) {
    log(
      'warning',
      `Parameter '${paramName}' has invalid value '${paramValue}'. Allowed values: ${allowedValues.join(
        ', ',
      )}`,
    );
    return {
      isValid: false,
      errorResponse: createTextResponse(
        `Parameter '${paramName}' must be one of: ${allowedValues.join(', ')}. You provided: '${paramValue}'.`,
        true,
      ),
    };
  }

  return { isValid: true };
}

/**
 * Consolidates multiple content blocks into a single text response for Claude Code compatibility
 *
 * Claude Code violates the MCP specification by only showing the first content block.
 * This function provides a workaround by concatenating all text content into a single block.
 * Detection is automatic - no environment variable configuration required.
 *
 * @param response The original ToolResponse with multiple content blocks
 * @returns A new ToolResponse with consolidated content
 */
export function consolidateContentForClaudeCode(response: ToolResponse): ToolResponse {
  // Automatically detect if running under Claude Code
  const shouldConsolidate = getDefaultEnvironmentDetector().isRunningUnderClaudeCode();

  if (!shouldConsolidate || !response.content || response.content.length <= 1) {
    return response;
  }

  // Extract all text content and concatenate with separators
  const textParts: string[] = [];

  response.content.forEach((item, index) => {
    if (item.type === 'text') {
      // Add a separator between content blocks (except for the first one)
      if (index > 0 && textParts.length > 0) {
        textParts.push('\n---\n');
      }
      textParts.push(item.text);
    }
    // Note: Image content is not handled in this workaround as it requires special formatting
  });

  // If no text content was found, return the original response to preserve non-text content
  if (textParts.length === 0) {
    return response;
  }

  const consolidatedText = textParts.join('');

  return {
    ...response,
    content: [
      {
        type: 'text',
        text: consolidatedText,
      },
    ],
  };
}

// Export the ToolResponse type for use in other files
export { ToolResponse, ValidationResult };

```

--------------------------------------------------------------------------------
/src/mcp/tools/ui-testing/long_press.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * UI Testing Plugin: Long Press
 *
 * Long press at specific coordinates for given duration (ms).
 * Use describe_ui for precise coordinates (don't guess from screenshots).
 */

import * as z from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import {
  createTextResponse,
  createErrorResponse,
  DependencyError,
  AxeError,
  SystemError,
} from '../../../utils/responses/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultDebuggerManager } from '../../../utils/debugger/index.ts';
import type { DebuggerManager } from '../../../utils/debugger/debugger-manager.ts';
import { guardUiAutomationAgainstStoppedDebugger } from '../../../utils/debugger/ui-automation-guard.ts';
import {
  createAxeNotAvailableResponse,
  getAxePath,
  getBundledAxeEnvironment,
} from '../../../utils/axe/index.ts';
import {
  createSessionAwareTool,
  getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';

// Define schema as ZodObject
const longPressSchema = z.object({
  simulatorId: z.uuid({ message: 'Invalid Simulator UUID format' }),
  x: z.number().int({ message: 'X coordinate for the long press' }),
  y: z.number().int({ message: 'Y coordinate for the long press' }),
  duration: z.number().positive({ message: 'Duration of the long press in milliseconds' }),
});

// Use z.infer for type safety
type LongPressParams = z.infer<typeof longPressSchema>;

const publicSchemaObject = z.strictObject(
  longPressSchema.omit({ simulatorId: true } as const).shape,
);

export interface AxeHelpers {
  getAxePath: () => string | null;
  getBundledAxeEnvironment: () => Record<string, string>;
  createAxeNotAvailableResponse: () => ToolResponse;
}

const LOG_PREFIX = '[AXe]';

export async function long_pressLogic(
  params: LongPressParams,
  executor: CommandExecutor,
  axeHelpers: AxeHelpers = {
    getAxePath,
    getBundledAxeEnvironment,
    createAxeNotAvailableResponse,
  },
  debuggerManager: DebuggerManager = getDefaultDebuggerManager(),
): Promise<ToolResponse> {
  const toolName = 'long_press';
  const { simulatorId, x, y, duration } = params;

  const guard = await guardUiAutomationAgainstStoppedDebugger({
    debugger: debuggerManager,
    simulatorId,
    toolName,
  });
  if (guard.blockedResponse) return guard.blockedResponse;

  // AXe uses touch command with --down, --up, and --delay for long press
  const delayInSeconds = Number(duration) / 1000; // Convert ms to seconds
  const commandArgs = [
    'touch',
    '-x',
    String(x),
    '-y',
    String(y),
    '--down',
    '--up',
    '--delay',
    String(delayInSeconds),
  ];

  log(
    'info',
    `${LOG_PREFIX}/${toolName}: Starting for (${x}, ${y}), ${duration}ms on ${simulatorId}`,
  );

  try {
    await executeAxeCommand(commandArgs, simulatorId, 'touch', executor, axeHelpers);
    log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`);

    const coordinateWarning = getCoordinateWarning(simulatorId);
    const message = `Long press at (${x}, ${y}) for ${duration}ms simulated successfully.`;
    const warnings = [guard.warningText, coordinateWarning].filter(Boolean).join('\n\n');

    if (warnings) {
      return createTextResponse(`${message}\n\n${warnings}`);
    }

    return createTextResponse(message);
  } catch (error) {
    log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
    if (error instanceof DependencyError) {
      return axeHelpers.createAxeNotAvailableResponse();
    } else if (error instanceof AxeError) {
      return createErrorResponse(
        `Failed to simulate long press at (${x}, ${y}): ${error.message}`,
        error.axeOutput,
      );
    } else if (error instanceof SystemError) {
      return createErrorResponse(
        `System error executing axe: ${error.message}`,
        error.originalError?.stack,
      );
    }
    return createErrorResponse(
      `An unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

export default {
  name: 'long_press',
  description:
    "Long press at specific coordinates for given duration (ms). Use describe_ui for precise coordinates (don't guess from screenshots).",
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: longPressSchema,
  }),
  annotations: {
    title: 'Long Press',
    destructiveHint: true,
  },
  handler: createSessionAwareTool<LongPressParams>({
    internalSchema: longPressSchema as unknown as z.ZodType<LongPressParams, unknown>,
    logicFunction: (params: LongPressParams, executor: CommandExecutor) =>
      long_pressLogic(params, executor, {
        getAxePath,
        getBundledAxeEnvironment,
        createAxeNotAvailableResponse,
      }),
    getExecutor: getDefaultCommandExecutor,
    requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
  }),
};

// Session tracking for describe_ui warnings
interface DescribeUISession {
  timestamp: number;
  simulatorId: string;
}

const describeUITimestamps = new Map<string, DescribeUISession>();
const DESCRIBE_UI_WARNING_TIMEOUT = 60000; // 60 seconds

function getCoordinateWarning(simulatorId: string): string | null {
  const session = describeUITimestamps.get(simulatorId);
  if (!session) {
    return 'Warning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.';
  }

  const timeSinceDescribe = Date.now() - session.timestamp;
  if (timeSinceDescribe > DESCRIBE_UI_WARNING_TIMEOUT) {
    const secondsAgo = Math.round(timeSinceDescribe / 1000);
    return `Warning: describe_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with describe_ui instead of using potentially stale coordinates.`;
  }

  return null;
}

// Helper function for executing axe commands (inlined from src/tools/axe/index.ts)
async function executeAxeCommand(
  commandArgs: string[],
  simulatorId: string,
  commandName: string,
  executor: CommandExecutor = getDefaultCommandExecutor(),
  axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse },
): Promise<void> {
  // Get the appropriate axe binary path
  const axeBinary = axeHelpers.getAxePath();
  if (!axeBinary) {
    throw new DependencyError('AXe binary not found');
  }

  // Add --udid parameter to all commands
  const fullArgs = [...commandArgs, '--udid', simulatorId];

  // Construct the full command array with the axe binary as the first element
  const fullCommand = [axeBinary, ...fullArgs];

  try {
    // Determine environment variables for bundled AXe
    const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined;

    const result = await executor(
      fullCommand,
      `${LOG_PREFIX}: ${commandName}`,
      false,
      axeEnv ? { env: axeEnv } : undefined,
    );

    if (!result.success) {
      throw new AxeError(
        `axe command '${commandName}' failed.`,
        commandName,
        result.error ?? result.output,
        simulatorId,
      );
    }

    // Check for stderr output in successful commands
    if (result.error) {
      log(
        'warn',
        `${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${result.error}`,
      );
    }

    // Function now returns void - the calling code creates its own response
  } catch (error) {
    if (error instanceof Error) {
      if (error instanceof AxeError) {
        throw error;
      }

      // Otherwise wrap it in a SystemError
      throw new SystemError(`Failed to execute axe command: ${error.message}`, error);
    }

    // For any other type of error
    throw new SystemError(`Failed to execute axe command: ${String(error)}`);
  }
}

```

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

```typescript
/**
 * UI Testing Plugin: Touch
 *
 * Perform touch down/up events at specific coordinates.
 * Use describe_ui for precise coordinates (don't guess from screenshots).
 */

import * as z from 'zod';
import { log } from '../../../utils/logging/index.ts';
import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.ts';
import { DependencyError, AxeError, SystemError } from '../../../utils/errors.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultDebuggerManager } from '../../../utils/debugger/index.ts';
import type { DebuggerManager } from '../../../utils/debugger/debugger-manager.ts';
import { guardUiAutomationAgainstStoppedDebugger } from '../../../utils/debugger/ui-automation-guard.ts';
import {
  createAxeNotAvailableResponse,
  getAxePath,
  getBundledAxeEnvironment,
} from '../../../utils/axe-helpers.ts';
import { ToolResponse } from '../../../types/common.ts';
import {
  createSessionAwareTool,
  getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';

// Define schema as ZodObject
const touchSchema = z.object({
  simulatorId: z.uuid({ message: 'Invalid Simulator UUID format' }),
  x: z.number().int({ message: 'X coordinate must be an integer' }),
  y: z.number().int({ message: 'Y coordinate must be an integer' }),
  down: z.boolean().optional(),
  up: z.boolean().optional(),
  delay: z.number().min(0, { message: 'Delay must be non-negative' }).optional(),
});

// Use z.infer for type safety
type TouchParams = z.infer<typeof touchSchema>;

const publicSchemaObject = z.strictObject(touchSchema.omit({ simulatorId: true } as const).shape);

interface AxeHelpers {
  getAxePath: () => string | null;
  getBundledAxeEnvironment: () => Record<string, string>;
}

const LOG_PREFIX = '[AXe]';

export async function touchLogic(
  params: TouchParams,
  executor: CommandExecutor,
  axeHelpers?: AxeHelpers,
  debuggerManager: DebuggerManager = getDefaultDebuggerManager(),
): Promise<ToolResponse> {
  const toolName = 'touch';

  // Params are already validated by createTypedTool - use directly
  const { simulatorId, x, y, down, up, delay } = params;

  // Validate that at least one of down or up is specified
  if (!down && !up) {
    return createErrorResponse('At least one of "down" or "up" must be true');
  }

  const guard = await guardUiAutomationAgainstStoppedDebugger({
    debugger: debuggerManager,
    simulatorId,
    toolName,
  });
  if (guard.blockedResponse) return guard.blockedResponse;

  const commandArgs = ['touch', '-x', String(x), '-y', String(y)];
  if (down) {
    commandArgs.push('--down');
  }
  if (up) {
    commandArgs.push('--up');
  }
  if (delay !== undefined) {
    commandArgs.push('--delay', String(delay));
  }

  const actionText = down && up ? 'touch down+up' : down ? 'touch down' : 'touch up';
  log(
    'info',
    `${LOG_PREFIX}/${toolName}: Starting ${actionText} at (${x}, ${y}) on ${simulatorId}`,
  );

  try {
    await executeAxeCommand(commandArgs, simulatorId, 'touch', executor, axeHelpers);
    log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`);

    const coordinateWarning = getCoordinateWarning(simulatorId);
    const message = `Touch event (${actionText}) at (${x}, ${y}) executed successfully.`;
    const warnings = [guard.warningText, coordinateWarning].filter(Boolean).join('\n\n');

    if (warnings) {
      return createTextResponse(`${message}\n\n${warnings}`);
    }

    return createTextResponse(message);
  } catch (error) {
    log(
      'error',
      `${LOG_PREFIX}/${toolName}: Failed - ${error instanceof Error ? error.message : String(error)}`,
    );
    if (error instanceof DependencyError) {
      return createAxeNotAvailableResponse();
    } else if (error instanceof AxeError) {
      return createErrorResponse(
        `Failed to execute touch event: ${error.message}`,
        error.axeOutput,
      );
    } else if (error instanceof SystemError) {
      return createErrorResponse(
        `System error executing axe: ${error.message}`,
        error.originalError?.stack,
      );
    }
    return createErrorResponse(
      `An unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

export default {
  name: 'touch',
  description:
    "Perform touch down/up events at specific coordinates. Use describe_ui for precise coordinates (don't guess from screenshots).",
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: touchSchema,
  }),
  annotations: {
    title: 'Touch',
    destructiveHint: true,
  },
  handler: createSessionAwareTool<TouchParams>({
    internalSchema: touchSchema as unknown as z.ZodType<TouchParams, unknown>,
    logicFunction: (params: TouchParams, executor: CommandExecutor) => touchLogic(params, executor),
    getExecutor: getDefaultCommandExecutor,
    requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
  }),
};

// Session tracking for describe_ui warnings
interface DescribeUISession {
  timestamp: number;
  simulatorId: string;
}

const describeUITimestamps = new Map<string, DescribeUISession>();
const DESCRIBE_UI_WARNING_TIMEOUT = 60000; // 60 seconds

function getCoordinateWarning(simulatorId: string): string | null {
  const session = describeUITimestamps.get(simulatorId);
  if (!session) {
    return 'Warning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.';
  }

  const timeSinceDescribe = Date.now() - session.timestamp;
  if (timeSinceDescribe > DESCRIBE_UI_WARNING_TIMEOUT) {
    const secondsAgo = Math.round(timeSinceDescribe / 1000);
    return `Warning: describe_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with describe_ui instead of using potentially stale coordinates.`;
  }

  return null;
}

// Helper function for executing axe commands (inlined from src/tools/axe/index.ts)
async function executeAxeCommand(
  commandArgs: string[],
  simulatorId: string,
  commandName: string,
  executor: CommandExecutor = getDefaultCommandExecutor(),
  axeHelpers?: AxeHelpers,
): Promise<void> {
  // Use injected helpers or default to imported functions
  const helpers = axeHelpers ?? { getAxePath, getBundledAxeEnvironment };

  // Get the appropriate axe binary path
  const axeBinary = helpers.getAxePath();
  if (!axeBinary) {
    throw new DependencyError('AXe binary not found');
  }

  // Add --udid parameter to all commands
  const fullArgs = [...commandArgs, '--udid', simulatorId];

  // Construct the full command array with the axe binary as the first element
  const fullCommand = [axeBinary, ...fullArgs];

  try {
    // Determine environment variables for bundled AXe
    const axeEnv = axeBinary !== 'axe' ? helpers.getBundledAxeEnvironment() : undefined;

    const result = await executor(
      fullCommand,
      `${LOG_PREFIX}: ${commandName}`,
      false,
      axeEnv ? { env: axeEnv } : undefined,
    );

    if (!result.success) {
      throw new AxeError(
        `axe command '${commandName}' failed.`,
        commandName,
        result.error ?? result.output,
        simulatorId,
      );
    }

    // Check for stderr output in successful commands
    if (result.error) {
      log(
        'warn',
        `${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${result.error}`,
      );
    }

    // Function now returns void - the calling code creates its own response
  } catch (error) {
    if (error instanceof Error) {
      if (error instanceof AxeError) {
        throw error;
      }

      // Otherwise wrap it in a SystemError
      throw new SystemError(`Failed to execute axe command: ${error.message}`, error);
    }

    // For any other type of error
    throw new SystemError(`Failed to execute axe command: ${String(error)}`);
  }
}

```

--------------------------------------------------------------------------------
/src/utils/command.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Command Utilities - Generic command execution utilities
 *
 * This utility module provides functions for executing shell commands.
 * It serves as a foundation for other utility modules that need to execute commands.
 *
 * Responsibilities:
 * - Executing shell commands with proper argument handling
 * - Managing process spawning, output capture, and error handling
 */

import { spawn } from 'child_process';
import { createWriteStream, existsSync } from 'fs';
import { tmpdir as osTmpdir } from 'os';
import { log } from './logger.ts';
import { FileSystemExecutor } from './FileSystemExecutor.ts';
import { CommandExecutor, CommandResponse, CommandExecOptions } from './CommandExecutor.ts';

// Re-export types for backward compatibility
export { CommandExecutor, CommandResponse, CommandExecOptions } from './CommandExecutor.ts';
export { FileSystemExecutor } from './FileSystemExecutor.ts';

/**
 * Default executor implementation using spawn (current production behavior)
 * Private instance - use getDefaultCommandExecutor() for access
 * @param command An array of command and arguments
 * @param logPrefix Prefix for logging
 * @param useShell Whether to use shell execution (true) or direct execution (false)
 * @param opts Optional execution options (env: environment variables to merge with process.env, cwd: working directory)
 * @param detached Whether to spawn process without waiting for completion (for streaming/background processes)
 * @returns Promise resolving to command response with the process
 */
async function defaultExecutor(
  command: string[],
  logPrefix?: string,
  useShell: boolean = true,
  opts?: CommandExecOptions,
  detached: boolean = false,
): Promise<CommandResponse> {
  // Properly escape arguments for shell
  let escapedCommand = command;
  if (useShell) {
    // For shell execution, we need to format as ['sh', '-c', 'full command string']
    const commandString = command
      .map((arg) => {
        // Shell metacharacters that require quoting: space, quotes, equals, dollar, backticks, semicolons, pipes, etc.
        if (/[\s,"'=$`;&|<>(){}[\]\\*?~]/.test(arg) && !/^".*"$/.test(arg)) {
          // Escape all quotes and backslashes, then wrap in double quotes
          return `"${arg.replace(/(["\\])/g, '\\$1')}"`;
        }
        return arg;
      })
      .join(' ');

    escapedCommand = ['sh', '-c', commandString];
  }

  // Log the actual command that will be executed
  const displayCommand =
    useShell && escapedCommand.length === 3 ? escapedCommand[2] : escapedCommand.join(' ');
  log('info', `Executing ${logPrefix ?? ''} command: ${displayCommand}`);

  return new Promise((resolve, reject) => {
    const executable = escapedCommand[0];
    const args = escapedCommand.slice(1);

    const spawnOpts: Parameters<typeof spawn>[2] = {
      stdio: ['ignore', 'pipe', 'pipe'], // ignore stdin, pipe stdout/stderr
      env: { ...process.env, ...(opts?.env ?? {}) },
      cwd: opts?.cwd,
    };

    const childProcess = spawn(executable, args, spawnOpts);

    let stdout = '';
    let stderr = '';

    childProcess.stdout?.on('data', (data: Buffer) => {
      stdout += data.toString();
    });

    childProcess.stderr?.on('data', (data: Buffer) => {
      stderr += data.toString();
    });

    // For detached processes, handle differently to avoid race conditions
    if (detached) {
      // For detached processes, only wait for spawn success/failure
      let resolved = false;

      childProcess.on('error', (err) => {
        if (!resolved) {
          resolved = true;
          reject(err);
        }
      });

      // Give a small delay to ensure the process starts successfully
      setTimeout(() => {
        if (!resolved) {
          resolved = true;
          if (childProcess.pid) {
            resolve({
              success: true,
              output: '', // No output for detached processes
              process: childProcess,
            });
          } else {
            resolve({
              success: false,
              output: '',
              error: 'Failed to start detached process',
              process: childProcess,
            });
          }
        }
      }, 100);
    } else {
      // For non-detached processes, handle normally
      childProcess.on('close', (code) => {
        const success = code === 0;
        const response: CommandResponse = {
          success,
          output: stdout,
          error: success ? undefined : stderr,
          process: childProcess,
          exitCode: code ?? undefined,
        };

        resolve(response);
      });

      childProcess.on('error', (err) => {
        reject(err);
      });
    }
  });
}

/**
 * Default file system executor implementation using Node.js fs/promises
 * Private instance - use getDefaultFileSystemExecutor() for access
 */
const defaultFileSystemExecutor: FileSystemExecutor = {
  async mkdir(path: string, options?: { recursive?: boolean }): Promise<void> {
    const fs = await import('fs/promises');
    await fs.mkdir(path, options);
  },

  async readFile(path: string, encoding: BufferEncoding = 'utf8'): Promise<string> {
    const fs = await import('fs/promises');
    const content = await fs.readFile(path, encoding);
    return content;
  },

  async writeFile(path: string, content: string, encoding: BufferEncoding = 'utf8'): Promise<void> {
    const fs = await import('fs/promises');
    await fs.writeFile(path, content, encoding);
  },

  createWriteStream(path: string, options?: { flags?: string }) {
    return createWriteStream(path, options);
  },

  async cp(source: string, destination: string, options?: { recursive?: boolean }): Promise<void> {
    const fs = await import('fs/promises');
    await fs.cp(source, destination, options);
  },

  async readdir(path: string, options?: { withFileTypes?: boolean }): Promise<unknown[]> {
    const fs = await import('fs/promises');
    return await fs.readdir(path, options as Record<string, unknown>);
  },

  async rm(path: string, options?: { recursive?: boolean; force?: boolean }): Promise<void> {
    const fs = await import('fs/promises');
    await fs.rm(path, options);
  },

  existsSync(path: string): boolean {
    return existsSync(path);
  },

  async stat(path: string): Promise<{ isDirectory(): boolean; mtimeMs: number }> {
    const fs = await import('fs/promises');
    return await fs.stat(path);
  },

  async mkdtemp(prefix: string): Promise<string> {
    const fs = await import('fs/promises');
    return await fs.mkdtemp(prefix);
  },

  tmpdir(): string {
    return osTmpdir();
  },
};

/**
 * Get default command executor with test safety
 * Throws error if used in test environment to ensure proper mocking
 */
export function getDefaultCommandExecutor(): CommandExecutor {
  if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') {
    throw new Error(
      `🚨 REAL SYSTEM EXECUTOR DETECTED IN TEST! 🚨\n` +
        `This test is trying to use the default command executor instead of a mock.\n` +
        `Fix: Pass createMockExecutor() as the commandExecutor parameter in your test.\n` +
        `Example: await plugin.handler(args, createMockExecutor({success: true}), mockFileSystem)\n` +
        `See docs/dev/TESTING.md for proper testing patterns.`,
    );
  }
  return defaultExecutor;
}

/**
 * Get default file system executor with test safety
 * Throws error if used in test environment to ensure proper mocking
 */
export function getDefaultFileSystemExecutor(): FileSystemExecutor {
  if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') {
    throw new Error(
      `🚨 REAL FILESYSTEM EXECUTOR DETECTED IN TEST! 🚨\n` +
        `This test is trying to use the default filesystem executor instead of a mock.\n` +
        `Fix: Pass createMockFileSystemExecutor() as the fileSystemExecutor parameter in your test.\n` +
        `Example: await plugin.handler(args, mockCmd, createMockFileSystemExecutor())\n` +
        `See docs/dev/TESTING.md for proper testing patterns.`,
    );
  }
  return defaultFileSystemExecutor;
}

```

--------------------------------------------------------------------------------
/scripts/update-tools-docs.ts:
--------------------------------------------------------------------------------

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

/**
 * XcodeBuildMCP Tools Documentation Updater
 *
 * Automatically updates docs/TOOLS.md with current tool and workflow information
 * using static AST analysis. Ensures documentation always reflects the actual codebase.
 *
 * Usage:
 *   npx tsx scripts/update-tools-docs.ts [--dry-run] [--verbose]
 *
 * Options:
 *   --dry-run, -d       Show what would be updated without making changes
 *   --verbose, -v       Show detailed information about the update process
 *   --help, -h         Show this help message
 */

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

// Get project paths
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '..');
const docsPath = path.join(projectRoot, 'docs', 'TOOLS.md');

// CLI options
const args = process.argv.slice(2);
const options = {
  dryRun: args.includes('--dry-run') || args.includes('-d'),
  verbose: args.includes('--verbose') || args.includes('-v'),
  help: args.includes('--help') || args.includes('-h'),
};

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;

if (options.help) {
  console.log(`
${colors.bright}${colors.blue}XcodeBuildMCP Tools Documentation Updater${colors.reset}

Automatically updates docs/TOOLS.md with current tool and workflow information.

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

${colors.bright}Options:${colors.reset}
  --dry-run, -d       Show what would be updated without making changes
  --verbose, -v       Show detailed information about the update process
  --help, -h         Show this help message

${colors.bright}Examples:${colors.reset}
  ${colors.cyan}npx tsx scripts/update-tools-docs.ts${colors.reset}                    # Update docs/TOOLS.md
  ${colors.cyan}npx tsx scripts/update-tools-docs.ts --dry-run${colors.reset}          # Preview changes
  ${colors.cyan}npx tsx scripts/update-tools-docs.ts --verbose${colors.reset}          # Show detailed progress
`);
  process.exit(0);
}

/**
 * Generate the workflow section content
 */
function generateWorkflowSection(workflow: WorkflowInfo): string {
  const canonicalTools = workflow.tools.filter((tool) => tool.isCanonical);
  const toolCount = canonicalTools.length;

  let content = `### ${workflow.displayName} (\`${workflow.name}\`)\n`;
  content += `**Purpose**: ${workflow.description} (${toolCount} tools)\n\n`;

  // List each tool with its description
  for (const tool of canonicalTools.sort((a, b) => a.name.localeCompare(b.name))) {
    // Clean up the description for documentation
    const cleanDescription = tool.description
      .replace(/IMPORTANT:.*?Example:.*?\)/g, '') // Remove IMPORTANT sections
      .replace(/\s+/g, ' ') // Normalize whitespace
      .trim();

    content += `- \`${tool.name}\` - ${cleanDescription}\n`;
  }

  return content;
}

/**
 * Generate the complete TOOLS.md content
 */
function generateToolsDocumentation(analysis: StaticAnalysisResult): string {
  const { workflows, stats } = analysis;

  // Sort workflows by display name for consistent ordering
  const sortedWorkflows = workflows.sort((a, b) => a.displayName.localeCompare(b.displayName));

  const content = `# XcodeBuildMCP Tools Reference

XcodeBuildMCP provides ${stats.canonicalTools} tools organized into ${stats.workflowCount} workflow groups for comprehensive Apple development workflows.

## Workflow Groups

${sortedWorkflows.map((workflow) => generateWorkflowSection(workflow)).join('')}
## Summary Statistics

- **Total Tools**: ${stats.canonicalTools} canonical tools + ${stats.reExportTools} re-exports = ${stats.totalTools} total
- **Workflow Groups**: ${stats.workflowCount}

---

*This documentation is automatically generated by \`scripts/update-tools-docs.ts\` using static analysis. Last updated: ${new Date().toISOString().split('T')[0]}*
`;

  return content;
}

/**
 * Compare old and new content to show what changed
 */
function showDiff(oldContent: string, newContent: string): void {
  if (!options.verbose) return;

  console.log(`${colors.bright}${colors.cyan}📄 Content Comparison:${colors.reset}`);
  console.log('─'.repeat(50));

  const oldLines = oldContent.split('\n');
  const newLines = newContent.split('\n');

  const maxLength = Math.max(oldLines.length, newLines.length);
  let changes = 0;

  for (let i = 0; i < maxLength; i++) {
    const oldLine = oldLines[i] || '';
    const newLine = newLines[i] || '';

    if (oldLine !== newLine) {
      changes++;
      if (changes <= 10) {
        // Show first 10 changes
        console.log(`${colors.red}- Line ${i + 1}: ${oldLine}${colors.reset}`);
        console.log(`${colors.green}+ Line ${i + 1}: ${newLine}${colors.reset}`);
      }
    }
  }

  if (changes > 10) {
    console.log(`${colors.yellow}... and ${changes - 10} more changes${colors.reset}`);
  }

  console.log(`${colors.blue}Total changes: ${changes} lines${colors.reset}\n`);
}

/**
 * Main execution function
 */
async function main(): Promise<void> {
  try {
    console.log(
      `${colors.bright}${colors.blue}🔧 XcodeBuildMCP Tools Documentation Updater${colors.reset}`,
    );

    if (options.dryRun) {
      console.log(
        `${colors.yellow}🔍 Running in dry-run mode - no files will be modified${colors.reset}`,
      );
    }

    console.log(`${colors.cyan}📊 Analyzing tools...${colors.reset}`);

    // Get current tool analysis
    const analysis = await getStaticToolAnalysis();

    if (options.verbose) {
      console.log(
        `${colors.green}✓ Found ${analysis.stats.canonicalTools} canonical tools in ${analysis.stats.workflowCount} workflows${colors.reset}`,
      );
      console.log(
        `${colors.green}✓ Found ${analysis.stats.reExportTools} re-export files${colors.reset}`,
      );
    }

    // Generate new documentation content
    console.log(`${colors.cyan}📝 Generating documentation...${colors.reset}`);
    const newContent = generateToolsDocumentation(analysis);

    // Read current content for comparison
    let oldContent = '';
    if (fs.existsSync(docsPath)) {
      oldContent = fs.readFileSync(docsPath, 'utf-8');
    }

    // Check if content has changed
    if (oldContent === newContent) {
      console.log(`${colors.green}✅ Documentation is already up to date!${colors.reset}`);
      return;
    }

    // Show differences if verbose
    if (oldContent && options.verbose) {
      showDiff(oldContent, newContent);
    }

    if (options.dryRun) {
      console.log(
        `${colors.yellow}📋 Dry run completed. Documentation would be updated with:${colors.reset}`,
      );
      console.log(`   - ${analysis.stats.canonicalTools} canonical tools`);
      console.log(`   - ${analysis.stats.workflowCount} workflow groups`);
      console.log(`   - ${newContent.split('\n').length} lines total`);

      if (!options.verbose) {
        console.log(`\n${colors.cyan}💡 Use --verbose to see detailed changes${colors.reset}`);
      }

      return;
    }

    // Write new content
    console.log(`${colors.cyan}✏️  Writing updated documentation...${colors.reset}`);
    fs.writeFileSync(docsPath, newContent, 'utf-8');

    console.log(
      `${colors.green}✅ Successfully updated ${path.relative(projectRoot, docsPath)}!${colors.reset}`,
    );

    if (options.verbose) {
      console.log(`\n${colors.bright}📈 Update Summary:${colors.reset}`);
      console.log(
        `   Tools: ${analysis.stats.canonicalTools} canonical + ${analysis.stats.reExportTools} re-exports = ${analysis.stats.totalTools} total`,
      );
      console.log(`   Workflows: ${analysis.stats.workflowCount}`);
      console.log(`   File size: ${(newContent.length / 1024).toFixed(1)}KB`);
      console.log(`   Lines: ${newContent.split('\n').length}`);
    }
  } catch (error) {
    console.error(`${colors.red}❌ Error: ${(error as Error).message}${colors.reset}`);
    process.exit(1);
  }
}

// Run the updater
main();

```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import * as z from 'zod';
import {
  createMockExecutor,
  createMockCommandResponse,
} from '../../../../test-utils/mock-executors.ts';
import type { CommandExecutor } from '../../../../utils/execution/index.ts';
import { sessionStore } from '../../../../utils/session-store.ts';
import plugin, { stop_app_simLogic } from '../stop_app_sim.ts';

describe('stop_app_sim tool', () => {
  beforeEach(() => {
    sessionStore.clear();
  });

  describe('Export Field Validation (Literal)', () => {
    it('should expose correct metadata', () => {
      expect(plugin.name).toBe('stop_app_sim');
      expect(plugin.description).toBe('Stops an app running in an iOS simulator.');
    });

    it('should expose public schema with only bundleId', () => {
      const schema = z.object(plugin.schema);

      expect(schema.safeParse({ bundleId: 'com.example.app' }).success).toBe(true);
      expect(schema.safeParse({}).success).toBe(false);
      expect(schema.safeParse({ bundleId: 42 }).success).toBe(false);
      expect(Object.keys(plugin.schema)).toEqual(['bundleId']);

      const withSessionDefaults = schema.safeParse({
        simulatorId: 'SIM-UUID',
        simulatorName: 'iPhone 16',
        bundleId: 'com.example.app',
      });
      expect(withSessionDefaults.success).toBe(true);
      const parsed = withSessionDefaults.data as Record<string, unknown>;
      expect(parsed.simulatorId).toBeUndefined();
      expect(parsed.simulatorName).toBeUndefined();
    });
  });

  describe('Handler Requirements', () => {
    it('should require simulator identifier when not provided', async () => {
      const result = await plugin.handler({ bundleId: 'com.example.app' });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Missing required session defaults');
      expect(result.content[0].text).toContain('Provide simulatorId or simulatorName');
      expect(result.content[0].text).toContain('session-set-defaults');
    });

    it('should validate bundleId when simulatorId default exists', async () => {
      sessionStore.setDefaults({ simulatorId: 'SIM-UUID' });

      const result = await plugin.handler({});

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Parameter validation failed');
      expect(result.content[0].text).toContain(
        'bundleId: Invalid input: expected string, received undefined',
      );
    });

    it('should reject mutually exclusive simulator parameters', async () => {
      const result = await plugin.handler({
        simulatorId: 'SIM-UUID',
        simulatorName: 'iPhone 16',
        bundleId: 'com.example.app',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
      expect(result.content[0].text).toContain('simulatorId');
      expect(result.content[0].text).toContain('simulatorName');
    });
  });

  describe('Logic Behavior (Literal Returns)', () => {
    it('should stop app successfully with simulatorId', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: '' });

      const result = await stop_app_simLogic(
        {
          simulatorId: 'test-uuid',
          bundleId: 'com.example.App',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '✅ App com.example.App stopped successfully in simulator test-uuid',
          },
        ],
      });
    });

    it('should stop app successfully when resolving simulatorName', async () => {
      let callCount = 0;
      const sequencedExecutor = async (command: string[]) => {
        callCount++;
        if (callCount === 1) {
          return {
            success: true,
            output: JSON.stringify({
              devices: {
                'iOS 17.0': [
                  { name: 'iPhone 16', udid: 'resolved-uuid', isAvailable: true, state: 'Booted' },
                ],
              },
            }),
            error: '',
            process: {} as any,
          };
        }
        return {
          success: true,
          output: '',
          error: '',
          process: {} as any,
        };
      };

      const result = await stop_app_simLogic(
        {
          simulatorName: 'iPhone 16',
          bundleId: 'com.example.App',
        },
        sequencedExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '✅ App com.example.App stopped successfully in simulator "iPhone 16" (resolved-uuid)',
          },
        ],
      });
    });

    it('should surface error when simulator name is missing', async () => {
      const result = await stop_app_simLogic(
        {
          simulatorName: 'Missing Simulator',
          bundleId: 'com.example.App',
        },
        async () => ({
          success: true,
          output: JSON.stringify({ devices: {} }),
          error: '',
          process: {} as any,
        }),
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Simulator named "Missing Simulator" not found. Use list_sims to see available simulators.',
          },
        ],
        isError: true,
      });
    });

    it('should handle simulator list command failure', async () => {
      const listExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'simctl list failed',
      });

      const result = await stop_app_simLogic(
        {
          simulatorName: 'iPhone 16',
          bundleId: 'com.example.App',
        },
        listExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Failed to list simulators: simctl list failed',
          },
        ],
        isError: true,
      });
    });

    it('should surface terminate failures', async () => {
      const terminateExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'Simulator not found',
      });

      const result = await stop_app_simLogic(
        {
          simulatorId: 'invalid-uuid',
          bundleId: 'com.example.App',
        },
        terminateExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Stop app in simulator operation failed: Simulator not found',
          },
        ],
        isError: true,
      });
    });

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

      const result = await stop_app_simLogic(
        {
          simulatorId: 'test-uuid',
          bundleId: 'com.example.App',
        },
        throwingExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Stop app in simulator operation failed: Unexpected error',
          },
        ],
        isError: true,
      });
    });

    it('should call correct terminate command', async () => {
      const calls: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        opts?: { env?: Record<string, string>; cwd?: string };
        detached?: boolean;
      }> = [];

      const trackingExecutor: CommandExecutor = async (
        command,
        logPrefix,
        useShell,
        opts,
        detached,
      ) => {
        calls.push({ command, logPrefix, useShell, opts, detached });
        return createMockCommandResponse({
          success: true,
          output: '',
          error: undefined,
        });
      };

      await stop_app_simLogic(
        {
          simulatorId: 'test-uuid',
          bundleId: 'com.example.App',
        },
        trackingExecutor,
      );

      expect(calls).toEqual([
        {
          command: ['xcrun', 'simctl', 'terminate', 'test-uuid', 'com.example.App'],
          logPrefix: 'Stop App in Simulator',
          useShell: true,
          opts: undefined,
          detached: undefined,
        },
      ]);
    });
  });
});

```

--------------------------------------------------------------------------------
/docs/dev/CODE_QUALITY.md:
--------------------------------------------------------------------------------

```markdown
# XcodeBuildMCP Code Quality Guide

This guide consolidates all code quality, linting, and architectural compliance information for the XcodeBuildMCP project.

## Table of Contents

1. [Overview](#overview)
2. [ESLint Configuration](#eslint-configuration)
3. [Architectural Rules](#architectural-rules)
4. [Development Scripts](#development-scripts)
5. [Code Pattern Violations](#code-pattern-violations)
6. [Type Safety Migration](#type-safety-migration)
7. [Best Practices](#best-practices)

## Overview

XcodeBuildMCP enforces code quality through multiple layers:

1. **ESLint**: Handles general code quality, TypeScript rules, and stylistic consistency
2. **TypeScript**: Enforces type safety with strict mode
3. **Pattern Checker**: Enforces XcodeBuildMCP-specific architectural rules
4. **Migration Scripts**: Track progress on type safety improvements

## ESLint Configuration

### Current Configuration

The project uses a comprehensive ESLint setup that covers:

- TypeScript type safety rules
- Code style consistency
- Import ordering
- Unused variable detection
- Testing best practices

### ESLint Rules

For detailed ESLint rules and rationale, see [ESLINT_TYPE_SAFETY.md](ESLINT_TYPE_SAFETY.md).

### Running ESLint

```bash
# Check for linting issues
npm run lint

# Auto-fix linting issues
npm run lint:fix
```

## Architectural Rules

XcodeBuildMCP enforces several architectural patterns that cannot be expressed through ESLint:

### 1. Dependency Injection Pattern

**Rule**: All tools must use dependency injection for external interactions.

✅ **Allowed**:
- `createMockExecutor()` for command execution mocking
- `createMockFileSystemExecutor()` for file system mocking
- Logic functions accepting `executor?: CommandExecutor` parameter

❌ **Forbidden**:
- Direct use of `vi.mock()`, `vi.fn()`, or any Vitest mocking
- Direct calls to `execSync`, `spawn`, or `exec` in production code
- Testing handler functions directly

### 2. Handler Signature Compliance

**Rule**: MCP handlers must have exact signatures as required by the SDK.

✅ **Tool Handler Signature**:
```typescript
async handler(args: Record<string, unknown>): Promise<ToolResponse>
```

✅ **Resource Handler Signature**:
```typescript
async handler(uri: URL): Promise<{ contents: Array<{ text: string }> }>
```

❌ **Forbidden**:
- Multiple parameters in handlers
- Optional parameters
- Dependency injection parameters in handlers

### 3. Testing Architecture

**Rule**: Tests must only call logic functions, never handlers directly.

✅ **Correct Pattern**:
```typescript
const result = await myToolLogic(params, mockExecutor);
```

❌ **Forbidden Pattern**:
```typescript
const result = await myTool.handler(params);
```

### 4. Server Type Safety

**Rule**: MCP server instances must use proper SDK types, not generic casts.

✅ **Correct Pattern**:
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
const server = (globalThis as { mcpServer?: McpServer }).mcpServer;
server.server.createMessage({...});
```

❌ **Forbidden Pattern**:
```typescript
const server = (globalThis as { mcpServer?: Record<string, unknown> }).mcpServer;
const serverInstance = (server.server ?? server) as Record<string, unknown> & {...};
```

## Development Scripts

### Core Scripts

```bash
# Build the project
npm run build

# Run type checking
npm run typecheck

# Run tests
npm run test

# Check code patterns (architectural compliance)
node scripts/check-code-patterns.js

# Check type safety migration progress
npm run check-migration
```

### Pattern Checker Usage

The pattern checker enforces XcodeBuildMCP-specific architectural rules:

```bash
# Check all patterns
node scripts/check-code-patterns.js

# Check specific pattern type
node scripts/check-code-patterns.js --pattern=vitest
node scripts/check-code-patterns.js --pattern=execsync
node scripts/check-code-patterns.js --pattern=handler
node scripts/check-code-patterns.js --pattern=handler-testing
node scripts/check-code-patterns.js --pattern=server-typing

# Get help
node scripts/check-code-patterns.js --help
```

### Tool Summary Scripts

```bash
# Show tool and resource summary
npm run tools

# List all tools
npm run tools:list

# List both tools and resources
npm run tools:all
```

## Code Pattern Violations

The pattern checker identifies the following violations:

### 1. Vitest Mocking Violations

**What**: Any use of Vitest mocking functions
**Why**: Breaks dependency injection architecture
**Fix**: Use `createMockExecutor()` instead

### 2. ExecSync Violations

**What**: Direct use of Node.js child_process functions in production code
**Why**: Bypasses CommandExecutor dependency injection
**Fix**: Accept `CommandExecutor` parameter and use it

### 3. Handler Signature Violations

**What**: Handlers with incorrect parameter signatures
**Why**: MCP SDK requires exact signatures
**Fix**: Move dependencies inside handler body

### 4. Handler Testing Violations

**What**: Tests calling `.handler()` directly
**Why**: Violates dependency injection principle
**Fix**: Test logic functions instead

### 5. Improper Server Typing Violations

**What**: Casting MCP server instances to `Record<string, unknown>` or using custom interfaces instead of SDK types
**Why**: Breaks type safety and prevents proper API usage
**Fix**: Import `McpServer` from SDK and use proper typing instead of generic casts

## Type Safety Migration

The project is migrating to improved type safety using the `createTypedTool` factory:

### Check Migration Status

```bash
# Show summary
npm run check-migration

# Show detailed analysis
npm run check-migration:verbose

# Show only unmigrated tools
npm run check-migration:unfixed
```

### Migration Benefits

1. **Compile-time type safety** for tool parameters
2. **Automatic Zod schema validation**
3. **Better IDE support** and autocomplete
4. **Consistent error handling**

## Best Practices

### 1. Before Committing

Always run these checks before committing:

```bash
npm run build          # Ensure code compiles
npm run typecheck      # Check TypeScript types
npm run lint           # Check linting rules
npm run test           # Run tests
node scripts/check-code-patterns.js  # Check architectural compliance
```

### 2. Adding New Tools

1. Use dependency injection pattern
2. Follow handler signature requirements
3. Create comprehensive tests (test logic, not handlers)
4. Use `createTypedTool` factory for type safety
5. Document parameter schemas clearly

### 3. Writing Tests

1. Import the logic function, not the default export
2. Use `createMockExecutor()` for mocking
3. Test three dimensions: validation, command generation, output processing
4. Never test handlers directly

### 4. Code Organization

1. Keep tools in appropriate workflow directories
2. Share common tools via `-shared` directories
3. Re-export shared tools, don't duplicate
4. Follow naming conventions for tools

## Automated Enforcement

The project uses multiple layers of automated enforcement:

1. **Pre-commit**: ESLint and TypeScript checks (if configured)
2. **CI Pipeline**: All checks run on every PR
3. **PR Blocking**: Checks must pass before merge
4. **Code Review**: Automated and manual review processes

## Troubleshooting

### ESLint False Positives

If ESLint reports false positives in test files, check that:
1. Test files are properly configured in `.eslintrc.json`
2. Test-specific rules are applied correctly
3. File patterns match your test file locations

### Pattern Checker Issues

If the pattern checker reports unexpected violations:
1. Check if it's a legitimate architectural violation
2. Verify the file is in the correct directory
3. Ensure you're using the latest pattern definitions

### Type Safety Migration

If migration tooling reports incorrect status:
1. Ensure the tool exports follow standard patterns
2. Check that schema definitions are properly typed
3. Verify the handler uses the schema correctly

## Future Improvements

1. **Automated Fixes**: Add auto-fix capability to pattern checker
2. **IDE Integration**: Create VS Code extension for real-time checking
3. **Performance Metrics**: Add build and test performance tracking
4. **Complexity Analysis**: Add code complexity metrics
5. **Documentation Linting**: Add documentation quality checks

```
Page 4/12FirstPrevNextLast