#
tokens: 48567/50000 32/393 files (page 3/12)
lines: off (toggle) GitHub
raw markdown copy
This is page 3 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/mcp/tools/device/launch_app_device.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Device Workspace Plugin: Launch App Device
 *
 * Launches an app on a physical Apple device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro).
 * Requires deviceId and bundleId.
 */

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

// Type for the launch JSON response
type LaunchDataResponse = {
  result?: {
    process?: {
      processIdentifier?: number;
    };
  };
};

// Define schema as ZodObject
const launchAppDeviceSchema = z.object({
  deviceId: z.string().describe('UDID of the device (obtained from list_devices)'),
  bundleId: z
    .string()
    .describe('Bundle identifier of the app to launch (e.g., "com.example.MyApp")'),
});

const publicSchemaObject = launchAppDeviceSchema.omit({ deviceId: true } as const);

// Use z.infer for type safety
type LaunchAppDeviceParams = z.infer<typeof launchAppDeviceSchema>;

export async function launch_app_deviceLogic(
  params: LaunchAppDeviceParams,
  executor: CommandExecutor,
  fileSystem: FileSystemExecutor,
): Promise<ToolResponse> {
  const { deviceId, bundleId } = params;

  log('info', `Launching app ${bundleId} on device ${deviceId}`);

  try {
    // Use JSON output to capture process ID
    const tempJsonPath = join(fileSystem.tmpdir(), `launch-${Date.now()}.json`);

    const result = await executor(
      [
        'xcrun',
        'devicectl',
        'device',
        'process',
        'launch',
        '--device',
        deviceId,
        '--json-output',
        tempJsonPath,
        '--terminate-existing',
        bundleId,
      ],
      'Launch app on device',
      true, // useShell
      undefined, // env
    );

    if (!result.success) {
      return {
        content: [
          {
            type: 'text',
            text: `Failed to launch app: ${result.error}`,
          },
        ],
        isError: true,
      };
    }

    // Parse JSON to extract process ID
    let processId: number | undefined;
    try {
      const jsonContent = await fileSystem.readFile(tempJsonPath, 'utf8');
      const parsedData: unknown = JSON.parse(jsonContent);

      // Type guard to validate the parsed data structure
      if (
        parsedData &&
        typeof parsedData === 'object' &&
        'result' in parsedData &&
        parsedData.result &&
        typeof parsedData.result === 'object' &&
        'process' in parsedData.result &&
        parsedData.result.process &&
        typeof parsedData.result.process === 'object' &&
        'processIdentifier' in parsedData.result.process &&
        typeof parsedData.result.process.processIdentifier === 'number'
      ) {
        const launchData = parsedData as LaunchDataResponse;
        processId = launchData.result?.process?.processIdentifier;
      }

      // Clean up temp file
      await fileSystem.rm(tempJsonPath, { force: true }).catch(() => {});
    } catch (error) {
      log('warn', `Failed to parse launch JSON output: ${error}`);
    }

    let responseText = `✅ App launched successfully\n\n${result.output}`;

    if (processId) {
      responseText += `\n\nProcess ID: ${processId}`;
      responseText += `\n\nNext Steps:`;
      responseText += `\n1. Interact with your app on the device`;
      responseText += `\n2. Stop the app: stop_app_device({ deviceId: "${deviceId}", processId: ${processId} })`;
    }

    return {
      content: [
        {
          type: 'text',
          text: responseText,
        },
      ],
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error launching app on device: ${errorMessage}`);
    return {
      content: [
        {
          type: 'text',
          text: `Failed to launch app on device: ${errorMessage}`,
        },
      ],
      isError: true,
    };
  }
}

export default {
  name: 'launch_app_device',
  description: 'Launches an app on a connected device.',
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: launchAppDeviceSchema,
  }),
  annotations: {
    title: 'Launch App Device',
    destructiveHint: true,
  },
  handler: createSessionAwareTool<LaunchAppDeviceParams>({
    internalSchema: launchAppDeviceSchema as unknown as z.ZodType<LaunchAppDeviceParams>,
    logicFunction: (params, executor) =>
      launch_app_deviceLogic(params, executor, getDefaultFileSystemExecutor()),
    getExecutor: getDefaultCommandExecutor,
    requirements: [{ allOf: ['deviceId'], message: 'deviceId is required' }],
  }),
};

```

--------------------------------------------------------------------------------
/src/utils/debugger/dap/__tests__/transport-framing.test.ts:
--------------------------------------------------------------------------------

```typescript
import type { ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events';
import { PassThrough } from 'node:stream';
import type { InteractiveProcess, InteractiveSpawner } from '../../../execution/index.ts';
import { describe, expect, it } from 'vitest';

import { DapTransport } from '../transport.ts';
import type { DapEvent, DapResponse } from '../types.ts';
type TestSession = {
  stdout: PassThrough;
  stderr: PassThrough;
  stdin: PassThrough;
  emitExit: (code?: number | null, signal?: NodeJS.Signals | null) => void;
  emitError: (error: Error) => void;
};

function encodeMessage(message: Record<string, unknown>): string {
  const payload = JSON.stringify(message);
  return `Content-Length: ${Buffer.byteLength(payload, 'utf8')}\r\n\r\n${payload}`;
}

function buildResponse(
  requestSeq: number,
  command: string,
  body?: Record<string, unknown>,
): DapResponse {
  return {
    seq: requestSeq + 100,
    type: 'response',
    request_seq: requestSeq,
    success: true,
    command,
    body,
  };
}

function createTestSpawner(): { spawner: InteractiveSpawner; session: TestSession } {
  const stdout = new PassThrough();
  const stderr = new PassThrough();
  const stdin = new PassThrough();
  const emitter = new EventEmitter();
  const mockProcess = emitter as unknown as ChildProcess;
  const mutableProcess = mockProcess as unknown as {
    stdout: PassThrough | null;
    stderr: PassThrough | null;
    stdin: PassThrough | null;
    killed: boolean;
    exitCode: number | null;
    signalCode: NodeJS.Signals | null;
    spawnargs: string[];
    spawnfile: string;
    pid: number;
  };

  mutableProcess.stdout = stdout;
  mutableProcess.stderr = stderr;
  mutableProcess.stdin = stdin;
  mutableProcess.killed = false;
  mutableProcess.exitCode = null;
  mutableProcess.signalCode = null;
  mutableProcess.spawnargs = [];
  mutableProcess.spawnfile = 'mock';
  mutableProcess.pid = 12345;
  mockProcess.kill = ((signal?: NodeJS.Signals): boolean => {
    mutableProcess.killed = true;
    emitter.emit('exit', 0, signal ?? null);
    return true;
  }) as ChildProcess['kill'];

  const session: TestSession = {
    stdout,
    stderr,
    stdin,
    emitExit: (code = 0, signal = null) => {
      emitter.emit('exit', code, signal);
    },
    emitError: (error) => {
      emitter.emit('error', error);
    },
  };

  const spawner: InteractiveSpawner = (): InteractiveProcess => ({
    process: mockProcess,
    write(data: string): void {
      stdin.write(data);
    },
    kill(signal?: NodeJS.Signals): void {
      mockProcess.kill?.(signal);
    },
    dispose(): void {
      stdout.end();
      stderr.end();
      stdin.end();
      emitter.removeAllListeners();
    },
  });

  return { spawner, session };
}

describe('DapTransport framing', () => {
  it('parses responses across chunk boundaries', async () => {
    const { spawner, session } = createTestSpawner();

    const transport = new DapTransport({ spawner, adapterCommand: ['lldb-dap'] });

    const responsePromise = transport.sendRequest<undefined, { ok: boolean }>(
      'initialize',
      undefined,
      { timeoutMs: 1_000 },
    );

    const response = encodeMessage(buildResponse(1, 'initialize', { ok: true }));
    session.stdout.write(response.slice(0, 12));
    session.stdout.write(response.slice(12));

    await expect(responsePromise).resolves.toEqual({ ok: true });
    transport.dispose();
  });

  it('handles multiple messages in a single chunk', async () => {
    const { spawner, session } = createTestSpawner();

    const transport = new DapTransport({ spawner, adapterCommand: ['lldb-dap'] });
    const events: DapEvent[] = [];
    transport.onEvent((event) => events.push(event));

    const responsePromise = transport.sendRequest<undefined, { ok: boolean }>(
      'threads',
      undefined,
      { timeoutMs: 1_000 },
    );

    const eventMessage = encodeMessage({
      seq: 55,
      type: 'event',
      event: 'output',
      body: { output: 'hello' },
    });
    const responseMessage = encodeMessage(buildResponse(1, 'threads', { ok: true }));

    session.stdout.write(`${eventMessage}${responseMessage}`);

    await expect(responsePromise).resolves.toEqual({ ok: true });
    expect(events).toHaveLength(1);
    expect(events[0]?.event).toBe('output');
    transport.dispose();
  });

  it('continues after invalid headers', async () => {
    const { spawner, session } = createTestSpawner();

    const transport = new DapTransport({ spawner, adapterCommand: ['lldb-dap'] });

    const responsePromise = transport.sendRequest<undefined, { ok: boolean }>(
      'stackTrace',
      undefined,
      { timeoutMs: 1_000 },
    );

    session.stdout.write('Content-Length: nope\r\n\r\n');
    const responseMessage = encodeMessage(buildResponse(1, 'stackTrace', { ok: true }));
    session.stdout.write(responseMessage);

    await expect(responsePromise).resolves.toEqual({ ok: true });
    transport.dispose();
  });
});

```

--------------------------------------------------------------------------------
/src/utils/__tests__/typed-tool-factory.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tests for the createTypedTool factory
 */

import { describe, it, expect } from 'vitest';
import * as z from 'zod';
import { createTypedTool } from '../typed-tool-factory.ts';
import { createMockExecutor } from '../../test-utils/mock-executors.ts';
import { ToolResponse } from '../../types/common.ts';

// Test schema and types
const testSchema = z.object({
  requiredParam: z.string().describe('A required string parameter'),
  optionalParam: z.number().optional().describe('An optional number parameter'),
});

type TestParams = z.infer<typeof testSchema>;

// Mock logic function for testing
async function testLogic(params: TestParams): Promise<ToolResponse> {
  return {
    content: [{ type: 'text', text: `Logic executed with: ${params.requiredParam}` }],
    isError: false,
  };
}

describe('createTypedTool', () => {
  describe('Type Safety and Validation', () => {
    it('should accept valid parameters and call logic function', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'test' });
      const handler = createTypedTool(testSchema, testLogic, () => mockExecutor);

      const result = await handler({
        requiredParam: 'valid-value',
        optionalParam: 42,
      });

      expect(result.isError).toBe(false);
      expect(result.content[0].text).toContain('Logic executed with: valid-value');
    });

    it('should reject parameters with missing required fields', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'test' });
      const handler = createTypedTool(testSchema, testLogic, () => mockExecutor);

      const result = await handler({
        // Missing requiredParam
        optionalParam: 42,
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Parameter validation failed');
      expect(result.content[0].text).toContain('requiredParam');
    });

    it('should reject parameters with wrong types', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'test' });
      const handler = createTypedTool(testSchema, testLogic, () => mockExecutor);

      const result = await handler({
        requiredParam: 123, // Should be string, not number
        optionalParam: 42,
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Parameter validation failed');
      expect(result.content[0].text).toContain('requiredParam');
    });

    it('should accept parameters with only required fields', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'test' });
      const handler = createTypedTool(testSchema, testLogic, () => mockExecutor);

      const result = await handler({
        requiredParam: 'valid-value',
        // optionalParam omitted
      });

      expect(result.isError).toBe(false);
      expect(result.content[0].text).toContain('Logic executed with: valid-value');
    });

    it('should provide detailed validation error messages', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'test' });
      const handler = createTypedTool(testSchema, testLogic, () => mockExecutor);

      const result = await handler({
        requiredParam: 123, // Wrong type
        optionalParam: 'should-be-number', // Wrong type
      });

      expect(result.isError).toBe(true);
      const errorText = result.content[0].text;
      expect(errorText).toContain('Parameter validation failed');
      expect(errorText).toContain('requiredParam');
      expect(errorText).toContain('optionalParam');
    });
  });

  describe('Error Handling', () => {
    it('should re-throw non-Zod errors from logic function', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'test' });

      // Logic function that throws a non-Zod error
      async function errorLogic(): Promise<ToolResponse> {
        throw new Error('Unexpected error');
      }

      const handler = createTypedTool(testSchema, errorLogic, () => mockExecutor);

      await expect(handler({ requiredParam: 'valid' })).rejects.toThrow('Unexpected error');
    });
  });

  describe('Executor Integration', () => {
    it('should pass the provided executor to logic function', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'test' });

      async function executorTestLogic(params: TestParams, executor: any): Promise<ToolResponse> {
        // Verify executor is passed correctly
        expect(executor).toBe(mockExecutor);
        return {
          content: [{ type: 'text', text: 'Executor passed correctly' }],
          isError: false,
        };
      }

      const handler = createTypedTool(testSchema, executorTestLogic, () => mockExecutor);

      const result = await handler({ requiredParam: 'valid' });

      expect(result.isError).toBe(false);
      expect(result.content[0].text).toBe('Executor passed correctly');
    });
  });
});

```

--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------

```javascript
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettierPlugin from 'eslint-plugin-prettier';

export default [
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  {
    ignores: ['node_modules/**', 'build/**', 'dist/**', 'coverage/**', 'src/core/generated-plugins.ts', 'src/core/generated-resources.ts'],
  },
  {
    // TypeScript files in src/ directory (covered by tsconfig.json)
    files: ['src/**/*.ts'],
    languageOptions: {
      ecmaVersion: 2020,
      sourceType: 'module',
      parser: tseslint.parser,
      parserOptions: {
        project: ['./tsconfig.json'],
      },
    },
    plugins: {
      '@typescript-eslint': tseslint.plugin,
      'prettier': prettierPlugin,
    },
    rules: {
      'prettier/prettier': 'error',
      '@typescript-eslint/explicit-function-return-type': 'warn',
      '@typescript-eslint/no-explicit-any': 'error',
      '@typescript-eslint/no-unused-vars': ['error', { 
        argsIgnorePattern: 'never',
        varsIgnorePattern: 'never' 
      }],
      'no-console': ['warn', { allow: ['error'] }],
      
      // Prevent dangerous type casting anti-patterns (errors)
      '@typescript-eslint/consistent-type-assertions': ['error', {
        assertionStyle: 'as',
        objectLiteralTypeAssertions: 'never'
      }],
      '@typescript-eslint/no-unsafe-argument': 'error',
      '@typescript-eslint/no-unsafe-assignment': 'error',
      '@typescript-eslint/no-unsafe-call': 'error',
      '@typescript-eslint/no-unsafe-member-access': 'error',
      '@typescript-eslint/no-unsafe-return': 'error',
      
      // Prevent specific anti-patterns we found
      '@typescript-eslint/ban-ts-comment': ['error', {
        'ts-expect-error': 'allow-with-description',
        'ts-ignore': true,
        'ts-nocheck': true,
        'ts-check': false,
      }],
      
      // Encourage best practices (warnings - can be gradually fixed)
      '@typescript-eslint/prefer-as-const': 'warn',
      '@typescript-eslint/prefer-nullish-coalescing': 'warn',
      '@typescript-eslint/prefer-optional-chain': 'warn',
      
      // Prevent barrel imports to maintain architectural improvements
      'no-restricted-imports': ['error', {
        patterns: [
          {
            group: ['**/utils/index.js', '../utils/index.js', '../../utils/index.js', '../../../utils/index.js', '**/utils/index.ts', '../utils/index.ts', '../../utils/index.ts', '../../../utils/index.ts'],
            message: 'Barrel imports from utils/index are prohibited. Use focused facade imports instead (e.g., utils/logging/index.ts, utils/execution/index.ts).'
          },
          {
            group: ['./**/*.js', '../**/*.js'],
            message: 'Import TypeScript files with .ts extension, not .js. This ensures compatibility with native TypeScript runtimes like Bun and Deno. Change .js to .ts in your import path.'
          }
        ]
      }],
    },
  },
  {
    // JavaScript and TypeScript files outside the main project (scripts/, etc.)
    files: ['**/*.{js,ts}'],
    ignores: ['src/**/*', '**/*.test.ts'],
    languageOptions: {
      ecmaVersion: 2020,
      sourceType: 'module',
      parser: tseslint.parser,
      // No project reference for scripts - use standalone parsing
    },
    plugins: {
      '@typescript-eslint': tseslint.plugin,
      'prettier': prettierPlugin,
    },
    rules: {
      'prettier/prettier': 'error',
      // Relaxed TypeScript rules for scripts since they're not in the main project
      '@typescript-eslint/explicit-function-return-type': 'off',
      '@typescript-eslint/no-explicit-any': 'warn',
      '@typescript-eslint/no-unused-vars': ['warn', { 
        argsIgnorePattern: 'never',
        varsIgnorePattern: 'never' 
      }],
      'no-console': 'off', // Scripts are allowed to use console
      
      // Disable project-dependent rules for scripts
      '@typescript-eslint/no-unsafe-argument': 'off',
      '@typescript-eslint/no-unsafe-assignment': 'off',
      '@typescript-eslint/no-unsafe-call': 'off',
      '@typescript-eslint/no-unsafe-member-access': 'off',
      '@typescript-eslint/no-unsafe-return': 'off',
      '@typescript-eslint/prefer-nullish-coalescing': 'off',
      '@typescript-eslint/prefer-optional-chain': 'off',
    },
  },
  {
    files: ['**/*.test.ts'],
    languageOptions: {
      parser: tseslint.parser,
      parserOptions: {
        project: './tsconfig.test.json',
      },
    },
    rules: {
      '@typescript-eslint/no-explicit-any': 'off',
      '@typescript-eslint/no-unused-vars': 'off',
      '@typescript-eslint/explicit-function-return-type': 'off',
      'prefer-const': 'off',
      
      // Relax unsafe rules for tests - tests often need more flexibility
      '@typescript-eslint/no-unsafe-argument': 'off',
      '@typescript-eslint/no-unsafe-assignment': 'off',
      '@typescript-eslint/no-unsafe-call': 'off',
      '@typescript-eslint/no-unsafe-member-access': 'off',
      '@typescript-eslint/no-unsafe-return': 'off',
    },
  },
];

```

--------------------------------------------------------------------------------
/src/utils/axe-helpers.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * AXe Helper Functions
 *
 * This utility module provides functions to resolve and execute AXe.
 * Prefers bundled AXe when present, but allows env and PATH fallback.
 */

import { accessSync, constants, existsSync } from 'fs';
import { dirname, join, resolve, delimiter } from 'path';
import { createTextResponse } from './validation.ts';
import { ToolResponse } from '../types/common.ts';
import type { CommandExecutor } from './execution/index.ts';
import { getDefaultCommandExecutor } from './execution/index.ts';

const AXE_PATH_ENV_VARS = ['XCODEBUILDMCP_AXE_PATH', 'AXE_PATH'] as const;

export type AxeBinarySource = 'env' | 'bundled' | 'path';

export type AxeBinary = {
  path: string;
  source: AxeBinarySource;
};

function getPackageRoot(): string {
  const entry = process.argv[1];
  if (entry) {
    const entryDir = dirname(entry);
    return dirname(entryDir);
  }
  return process.cwd();
}

// In the npm package, build/index.js is at the same level as bundled/
// So we go up one level from build/ to get to the package root
const bundledAxePath = join(getPackageRoot(), 'bundled', 'axe');

function isExecutable(path: string): boolean {
  try {
    accessSync(path, constants.X_OK);
    return true;
  } catch {
    return false;
  }
}

function resolveAxePathFromEnv(): string | null {
  for (const envVar of AXE_PATH_ENV_VARS) {
    const value = process.env[envVar];
    if (!value) continue;
    const resolved = resolve(value);
    if (isExecutable(resolved)) {
      return resolved;
    }
  }
  return null;
}

function resolveBundledAxePath(): string | null {
  const entry = process.argv[1];
  const candidates = new Set<string>();
  if (entry) {
    const entryDir = dirname(entry);
    candidates.add(join(dirname(entryDir), 'bundled', 'axe'));
    candidates.add(join(entryDir, 'bundled', 'axe'));
  }
  candidates.add(bundledAxePath);
  candidates.add(join(process.cwd(), 'bundled', 'axe'));

  for (const candidate of candidates) {
    if (existsSync(candidate)) {
      return candidate;
    }
  }
  return null;
}

function resolveAxePathFromPath(): string | null {
  const pathValue = process.env.PATH ?? '';
  const entries = pathValue.split(delimiter).filter(Boolean);
  for (const entry of entries) {
    const candidate = join(entry, 'axe');
    if (isExecutable(candidate)) {
      return candidate;
    }
  }
  return null;
}

export function resolveAxeBinary(): AxeBinary | null {
  const envPath = resolveAxePathFromEnv();
  if (envPath) {
    return { path: envPath, source: 'env' };
  }

  const bundledPath = resolveBundledAxePath();
  if (bundledPath) {
    return { path: bundledPath, source: 'bundled' };
  }

  const pathBinary = resolveAxePathFromPath();
  if (pathBinary) {
    return { path: pathBinary, source: 'path' };
  }

  return null;
}

/**
 * Get the path to the available axe binary
 */
export function getAxePath(): string | null {
  return resolveAxeBinary()?.path ?? null;
}

/**
 * Get environment variables needed for bundled AXe to run
 */
export function getBundledAxeEnvironment(): Record<string, string> {
  // No special environment variables needed - bundled AXe binary
  // has proper @rpath configuration to find frameworks
  return {};
}

/**
 * Check if axe tool is available (bundled, env override, or PATH)
 */
export function areAxeToolsAvailable(): boolean {
  return getAxePath() !== null;
}

export function createAxeNotAvailableResponse(): ToolResponse {
  return createTextResponse(
    'AXe tool not found. UI automation features are not available.\n\n' +
      'Install AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\n' +
      'If you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
    true,
  );
}

/**
 * Compare two semver strings a and b.
 * Returns 1 if a > b, -1 if a < b, 0 if equal.
 */
function compareSemver(a: string, b: string): number {
  const pa = a.split('.').map((n) => parseInt(n, 10));
  const pb = b.split('.').map((n) => parseInt(n, 10));
  const len = Math.max(pa.length, pb.length);
  for (let i = 0; i < len; i++) {
    const da = Number.isFinite(pa[i]) ? pa[i] : 0;
    const db = Number.isFinite(pb[i]) ? pb[i] : 0;
    if (da > db) return 1;
    if (da < db) return -1;
  }
  return 0;
}

/**
 * Determine whether the bundled AXe meets a minimum version requirement.
 * Runs `axe --version` and parses a semantic version (e.g., "1.1.0").
 * If AXe is missing or the version cannot be parsed, returns false.
 */
export async function isAxeAtLeastVersion(
  required: string,
  executor?: CommandExecutor,
): Promise<boolean> {
  const axePath = getAxePath();
  if (!axePath) return false;

  const exec = executor ?? getDefaultCommandExecutor();
  try {
    const res = await exec([axePath, '--version'], 'AXe Version', true);
    if (!res.success) return false;

    const output = res.output ?? '';
    const versionMatch = output.match(/(\d+\.\d+\.\d+)/);
    if (!versionMatch) return false;

    const current = versionMatch[1];
    return compareSemver(current, required) >= 0;
  } catch {
    return false;
  }
}

```

--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Logger Utility - Simple logging implementation for the application
 *
 * This utility module provides a lightweight logging system that directs log
 * messages to stderr rather than stdout, ensuring they don't interfere with
 * the MCP protocol communication which uses stdout.
 *
 * Responsibilities:
 * - Formatting log messages with timestamps and level indicators
 * - Directing all logs to stderr to avoid MCP protocol interference
 * - Supporting different log levels (info, warning, error, debug)
 * - Providing a simple, consistent logging interface throughout the application
 * - Sending error-level logs to Sentry for monitoring and alerting
 *
 * While intentionally minimal, this logger provides the essential functionality
 * needed for operational monitoring and debugging throughout the application.
 * It's used by virtually all other modules for status reporting and error logging.
 */

import { createRequire } from 'node:module';
import { resolve } from 'node:path';
// Note: Removed "import * as Sentry from '@sentry/node'" to prevent native module loading at import time

const SENTRY_ENABLED =
  process.env.SENTRY_DISABLED !== 'true' && process.env.XCODEBUILDMCP_SENTRY_DISABLED !== 'true';

// Log levels in order of severity (lower number = more severe)
const LOG_LEVELS = {
  emergency: 0,
  alert: 1,
  critical: 2,
  error: 3,
  warning: 4,
  notice: 5,
  info: 6,
  debug: 7,
} as const;

export type LogLevel = keyof typeof LOG_LEVELS;

/**
 * Optional context for logging to control Sentry capture
 */
export interface LogContext {
  sentry?: boolean;
}

// Client-requested log level (null means no filtering)
let clientLogLevel: LogLevel | null = null;

function isTestEnv(): boolean {
  return (
    process.env.VITEST === 'true' ||
    process.env.NODE_ENV === 'test' ||
    process.env.XCODEBUILDMCP_SILENCE_LOGS === 'true'
  );
}

type SentryModule = typeof import('@sentry/node');

const require = createRequire(
  typeof __filename === 'string' ? __filename : resolve(process.cwd(), 'package.json'),
);
let cachedSentry: SentryModule | null = null;

function loadSentrySync(): SentryModule | null {
  if (!SENTRY_ENABLED || isTestEnv()) return null;
  if (cachedSentry) return cachedSentry;
  try {
    cachedSentry = require('@sentry/node') as SentryModule;
    return cachedSentry;
  } catch {
    // If @sentry/node is not installed in some environments, fail silently.
    return null;
  }
}

function withSentry(cb: (s: SentryModule) => void): void {
  const s = loadSentrySync();
  if (!s) return;
  try {
    cb(s);
  } catch {
    // no-op: avoid throwing inside logger
  }
}

if (!SENTRY_ENABLED) {
  if (process.env.SENTRY_DISABLED === 'true') {
    log('info', 'Sentry disabled due to SENTRY_DISABLED environment variable');
  } else if (process.env.XCODEBUILDMCP_SENTRY_DISABLED === 'true') {
    log('info', 'Sentry disabled due to XCODEBUILDMCP_SENTRY_DISABLED environment variable');
  }
}

/**
 * Set the minimum log level for client-requested filtering
 * @param level The minimum log level to output
 */
export function setLogLevel(level: LogLevel): void {
  clientLogLevel = level;
  log('debug', `Log level set to: ${level}`);
}

/**
 * Get the current client-requested log level
 * @returns The current log level or null if no filtering is active
 */
export function getLogLevel(): LogLevel | null {
  return clientLogLevel;
}

/**
 * Check if a log level should be output based on client settings
 * @param level The log level to check
 * @returns true if the message should be logged
 */
function shouldLog(level: string): boolean {
  // Suppress logging during tests to keep test output clean
  if (isTestEnv()) {
    return false;
  }

  // If no client level set, log everything
  if (clientLogLevel === null) {
    return true;
  }

  // Check if the level is valid
  const levelKey = level.toLowerCase() as LogLevel;
  if (!(levelKey in LOG_LEVELS)) {
    return true; // Log unknown levels
  }

  // Only log if the message level is at or above the client's requested level
  return LOG_LEVELS[levelKey] <= LOG_LEVELS[clientLogLevel];
}

/**
 * Log a message with the specified level
 * @param level The log level (emergency, alert, critical, error, warning, notice, info, debug)
 * @param message The message to log
 * @param context Optional context to control Sentry capture and other behavior
 */
export function log(level: string, message: string, context?: LogContext): void {
  // Check if we should log this level
  if (!shouldLog(level)) {
    return;
  }

  const timestamp = new Date().toISOString();
  const logMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`;

  // Default: error level goes to Sentry
  // But respect explicit override from context
  const captureToSentry = SENTRY_ENABLED && (context?.sentry ?? level === 'error');

  if (captureToSentry) {
    withSentry((s) => s.captureMessage(logMessage));
  }

  // It's important to use console.error here to ensure logs don't interfere with MCP protocol communication
  // see https://modelcontextprotocol.io/docs/tools/debugging#server-side-logging
  console.error(logMessage);
}

```

--------------------------------------------------------------------------------
/src/mcp/tools/project-discovery/list_schemes.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Project Discovery Plugin: List Schemes (Unified)
 *
 * Lists available schemes for either a project or workspace using xcodebuild.
 * Accepts mutually exclusive `projectPath` or `workspacePath`.
 */

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

// Unified schema: XOR between projectPath and workspacePath
const baseSchemaObject = z.object({
  projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
  workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
});

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

export type ListSchemesParams = z.infer<typeof listSchemesSchema>;

const createTextBlock = (text: string) => ({ type: 'text', text }) as const;

/**
 * Business logic for listing schemes in a project or workspace.
 * Exported for direct testing and reuse.
 */
export async function listSchemesLogic(
  params: ListSchemesParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  log('info', 'Listing schemes');

  try {
    // For listing schemes, we can't use executeXcodeBuild directly since it's not a standard action
    // We need to create a custom command with -list flag
    const command = ['xcodebuild', '-list'];

    const hasProjectPath = typeof params.projectPath === 'string';
    const projectOrWorkspace = hasProjectPath ? 'project' : 'workspace';
    const path = hasProjectPath ? params.projectPath : params.workspacePath;

    if (hasProjectPath) {
      command.push('-project', params.projectPath!);
    } else {
      command.push('-workspace', params.workspacePath!);
    }

    const result = await executor(command, 'List Schemes', true);

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

    // Extract schemes from the output
    const schemesMatch = result.output.match(/Schemes:([\s\S]*?)(?=\n\n|$)/);

    if (!schemesMatch) {
      return createTextResponse('No schemes found in the output', true);
    }

    const schemeLines = schemesMatch[1].trim().split('\n');
    const schemes = schemeLines.map((line) => line.trim()).filter((line) => line);

    // Prepare next steps with the first scheme if available
    let nextStepsText = '';
    let hintText = '';
    if (schemes.length > 0) {
      const firstScheme = schemes[0];

      // Note: After Phase 2, these will be unified tool names too
      nextStepsText = `Next Steps:
1. Build the app: build_macos({ ${projectOrWorkspace}Path: "${path}", scheme: "${firstScheme}" })
   or for iOS: build_sim({ ${projectOrWorkspace}Path: "${path}", scheme: "${firstScheme}", simulatorName: "iPhone 16" })
2. Show build settings: show_build_settings({ ${projectOrWorkspace}Path: "${path}", scheme: "${firstScheme}" })`;

      hintText =
        `Hint: Consider saving a default scheme with session-set-defaults ` +
        `{ scheme: "${firstScheme}" } to avoid repeating it.`;
    }

    const content = [
      createTextBlock('✅ Available schemes:'),
      createTextBlock(schemes.join('\n')),
      createTextBlock(nextStepsText),
    ];
    if (hintText.length > 0) {
      content.push(createTextBlock(hintText));
    }

    return {
      content,
      isError: false,
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error listing schemes: ${errorMessage}`);
    return createTextResponse(`Error listing schemes: ${errorMessage}`, true);
  }
}

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

export default {
  name: 'list_schemes',
  description: 'Lists schemes for a project or workspace.',
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: baseSchemaObject,
  }),
  annotations: {
    title: 'List Schemes',
    readOnlyHint: true,
  },
  handler: createSessionAwareTool<ListSchemesParams>({
    internalSchema: listSchemesSchema as unknown as z.ZodType<ListSchemesParams, unknown>,
    logicFunction: listSchemesLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
    ],
    exclusivePairs: [['projectPath', 'workspacePath']],
  }),
};

```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/stop_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 stop (e.g., 'com.example.MyApp')"),
});

const stopAppSimSchema = 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 StopAppSimParams = z.infer<typeof stopAppSimSchema>;

export async function stop_app_simLogic(
  params: StopAppSimParams,
  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', `Stopping app ${params.bundleId} in simulator ${simulatorId}`);

  try {
    const command = ['xcrun', 'simctl', 'terminate', simulatorId, params.bundleId];
    const result = await executor(command, 'Stop App in Simulator', true, undefined);

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

    return {
      content: [
        {
          type: 'text',
          text: `✅ App ${params.bundleId} stopped successfully in simulator ${simulatorDisplayName || simulatorId}`,
        },
      ],
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error stopping app in simulator: ${errorMessage}`);
    return {
      content: [
        {
          type: 'text',
          text: `Stop app in simulator operation failed: ${errorMessage}`,
        },
      ],
      isError: true,
    };
  }
}

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

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

```

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

```typescript
import { join } from 'path';
import { tmpdir } from 'os';
import { randomUUID } from 'crypto';
import { log } from './logger.ts';
import { iOSTemplateVersion, macOSTemplateVersion } from '../version.ts';
import { CommandExecutor } from './command.ts';
import { FileSystemExecutor } from './FileSystemExecutor.ts';

/**
 * Template manager for downloading and managing project templates
 */
export class TemplateManager {
  private static readonly GITHUB_ORG = 'cameroncooke';
  private static readonly IOS_TEMPLATE_REPO = 'XcodeBuildMCP-iOS-Template';
  private static readonly MACOS_TEMPLATE_REPO = 'XcodeBuildMCP-macOS-Template';

  /**
   * Get the template path for a specific platform
   * Checks for local override via environment variable first
   */
  static async getTemplatePath(
    platform: 'iOS' | 'macOS',
    commandExecutor: CommandExecutor,
    fileSystemExecutor: FileSystemExecutor,
  ): Promise<string> {
    // Check for local override
    const envVar =
      platform === 'iOS' ? 'XCODEBUILDMCP_IOS_TEMPLATE_PATH' : 'XCODEBUILDMCP_MACOS_TEMPLATE_PATH';

    const localPath = process.env[envVar];
    log('debug', `[TemplateManager] Checking env var '${envVar}'. Value: '${localPath}'`);

    if (localPath) {
      const pathExists = fileSystemExecutor.existsSync(localPath);
      log('debug', `[TemplateManager] Env var set. Path '${localPath}' exists? ${pathExists}`);
      if (pathExists) {
        const templateSubdir = join(localPath, 'template');
        const subdirExists = fileSystemExecutor.existsSync(templateSubdir);
        log(
          'debug',
          `[TemplateManager] Checking for subdir '${templateSubdir}'. Exists? ${subdirExists}`,
        );
        if (subdirExists) {
          log('info', `Using local ${platform} template from: ${templateSubdir}`);
          return templateSubdir;
        } else {
          log('info', `Template directory not found in ${localPath}, using GitHub release`);
        }
      }
    }

    log('debug', '[TemplateManager] Env var not set or path invalid, proceeding to download.');
    // Download from GitHub release
    return await this.downloadTemplate(platform, commandExecutor, fileSystemExecutor);
  }

  /**
   * Download template from GitHub release
   */
  private static async downloadTemplate(
    platform: 'iOS' | 'macOS',
    commandExecutor: CommandExecutor,
    fileSystemExecutor: FileSystemExecutor,
  ): Promise<string> {
    const repo = platform === 'iOS' ? this.IOS_TEMPLATE_REPO : this.MACOS_TEMPLATE_REPO;
    const defaultVersion = platform === 'iOS' ? iOSTemplateVersion : macOSTemplateVersion;
    const envVarName =
      platform === 'iOS'
        ? 'XCODEBUILD_MCP_IOS_TEMPLATE_VERSION'
        : 'XCODEBUILD_MCP_MACOS_TEMPLATE_VERSION';
    const version = String(
      process.env[envVarName] ?? process.env.XCODEBUILD_MCP_TEMPLATE_VERSION ?? defaultVersion,
    );

    // Create temp directory for download
    const tempDir = join(tmpdir(), `xcodebuild-mcp-template-${randomUUID()}`);
    await fileSystemExecutor.mkdir(tempDir, { recursive: true });

    try {
      const downloadUrl = `https://github.com/${this.GITHUB_ORG}/${repo}/releases/download/${version}/${repo}-${version.substring(1)}.zip`;
      const zipPath = join(tempDir, 'template.zip');

      log('info', `Downloading ${platform} template ${version} from GitHub...`);
      log('info', `Download URL: ${downloadUrl}`);

      // Download the release artifact
      const curlResult = await commandExecutor(
        ['curl', '-L', '-f', '-o', zipPath, downloadUrl],
        'Download Template',
        true,
        undefined,
      );

      if (!curlResult.success) {
        throw new Error(`Failed to download template: ${curlResult.error}`);
      }

      // Extract the zip file
      // Temporarily change to temp directory for extraction
      const originalCwd = process.cwd();
      try {
        process.chdir(tempDir);
        const unzipResult = await commandExecutor(
          ['unzip', '-q', zipPath],
          'Extract Template',
          true,
          undefined,
        );

        if (!unzipResult.success) {
          throw new Error(`Failed to extract template: ${unzipResult.error}`);
        }
      } finally {
        process.chdir(originalCwd);
      }

      // Find the extracted directory and return the template subdirectory
      const extractedDir = join(tempDir, `${repo}-${version.substring(1)}`);
      if (!fileSystemExecutor.existsSync(extractedDir)) {
        throw new Error(`Expected template directory not found: ${extractedDir}`);
      }

      log('info', `Successfully downloaded ${platform} template ${version}`);
      return extractedDir;
    } catch (error) {
      // Clean up on error
      log('error', `Failed to download ${platform} template ${version}: ${error}`);
      await this.cleanup(tempDir, fileSystemExecutor);
      throw error;
    }
  }

  /**
   * Clean up downloaded template directory
   */
  static async cleanup(
    templatePath: string,
    fileSystemExecutor: FileSystemExecutor,
  ): Promise<void> {
    // Only clean up if it's in temp directory
    if (templatePath.startsWith(tmpdir())) {
      await fileSystemExecutor.rm(templatePath, { recursive: true, force: true });
    }
  }
}

```

--------------------------------------------------------------------------------
/docs/dev/ESLINT_TYPE_SAFETY.md:
--------------------------------------------------------------------------------

```markdown
# ESLint Type Safety Rules

This document explains the ESLint rules added to prevent TypeScript anti-patterns and improve type safety.

## Rules Added

### Error-Level Rules (Block CI/Deployment)

These rules prevent dangerous type casting patterns that can lead to runtime errors:

#### `@typescript-eslint/consistent-type-assertions`
- **Purpose**: Prevents dangerous object literal type assertions
- **Example**: Prevents `{ foo: 'bar' } as ComplexType`
- **Rationale**: Object literal assertions can hide missing properties

#### `@typescript-eslint/no-unsafe-*` (5 rules)
- **no-unsafe-argument**: Prevents passing `any` to typed parameters
- **no-unsafe-assignment**: Prevents assigning `any` to typed variables  
- **no-unsafe-call**: Prevents calling `any` as a function
- **no-unsafe-member-access**: Prevents accessing properties on `any`
- **no-unsafe-return**: Prevents returning `any` from typed functions

**Example of prevented anti-pattern:**
```typescript
// ❌ BAD - This would now be an ESLint error
function handleParams(args: Record<string, unknown>) {
  const typedParams = args as MyToolParams; // Unsafe casting
  return typedParams.someProperty as string; // Unsafe member access
}

// ✅ GOOD - Proper validation approach
function handleParams(args: Record<string, unknown>) {
  const typedParams = MyToolParamsSchema.parse(args); // Runtime validation
  return typedParams.someProperty; // Type-safe access
}
```

#### `@typescript-eslint/ban-ts-comment`
- **Purpose**: Prevents unsafe TypeScript comments
- **Blocks**: `@ts-ignore`, `@ts-nocheck`
- **Allows**: `@ts-expect-error` (with description)

### Warning-Level Rules (Encourage Best Practices)

These rules encourage modern TypeScript patterns but don't block builds:

#### `@typescript-eslint/prefer-nullish-coalescing`
- **Purpose**: Prefer `??` over `||` for default values
- **Example**: `value ?? 'default'` instead of `value || 'default'`
- **Rationale**: More precise handling of falsy values (0, '', false)

#### `@typescript-eslint/prefer-optional-chain`
- **Purpose**: Prefer `?.` for safe property access
- **Example**: `obj?.prop` instead of `obj && obj.prop`
- **Rationale**: More concise and readable

#### `@typescript-eslint/prefer-as-const`
- **Purpose**: Prefer `as const` for literal types
- **Example**: `['a', 'b'] as const` instead of `['a', 'b'] as string[]`

## Test File Exceptions

Test files (`.test.ts`) have relaxed rules for flexibility:
- All `no-unsafe-*` rules are disabled
- `no-explicit-any` is disabled
- Tests often need to test error conditions and edge cases

## Impact on Codebase

### Current Status (Post-Implementation)
- **387 total issues detected**
  - **207 errors**: Require fixing for type safety
  - **180 warnings**: Can be gradually improved

### Gradual Migration Strategy

1. **Phase 1** (Immediate): Error-level rules prevent new anti-patterns
2. **Phase 2** (Ongoing): Gradually fix warning-level violations
3. **Phase 3** (Future): Consider promoting warnings to errors

### Benefits

1. **Prevents Regression**: New code can't introduce the anti-patterns we just fixed
2. **Runtime Safety**: Catches potential runtime errors at compile time  
3. **Code Quality**: Encourages modern TypeScript best practices
4. **Developer Experience**: Better IDE support and autocomplete

## Related Issues Fixed

These rules prevent the specific anti-patterns identified in PR review:

1. **✅ Type Casting in Parameters**: `args as SomeType` patterns now flagged
2. **✅ Unsafe Property Access**: `params.field as string` patterns prevented
3. **✅ Missing Validation**: Encourages schema validation over casting
4. **✅ Return Type Mismatches**: Function signature inconsistencies caught
5. **✅ Nullish Coalescing**: Promotes safer default value handling

## Agent Orchestration for ESLint Fixes

### Parallel Agent Strategy

When fixing ESLint issues across the codebase:

1. **Deploy Multiple Agents**: Run agents in parallel on different files
2. **Single File Focus**: Each agent works on ONE tool file at a time
3. **Individual Linting**: Agents run `npm run lint path/to/single/file.ts` only
4. **Immediate Commits**: Commit each agent's work as soon as they complete
5. **Never Wait**: Don't wait for all agents to finish before committing
6. **Avoid Full Linting**: Never run `npm run lint` without a file path (eats context)
7. **Progress Tracking**: Update todo list and periodically check overall status
8. **Loop Until Done**: Keep deploying agents until all issues are resolved

### Example Commands for Agents

```bash
# Single file linting (what agents should run)
npm run lint src/mcp/tools/device-project/test_device_proj.ts

# NOT this (too much context)
npm run lint
```

### Commit Strategy

- **Individual commits**: One commit per agent completion
- **Clear messages**: `fix: resolve ESLint errors in tool_name.ts`
- **Never batch**: Don't wait to commit multiple files together
- **Progress preservation**: Each fix is immediately saved

## Future Improvements

Consider adding these rules in future iterations:

- `@typescript-eslint/strict-boolean-expressions`: Stricter boolean logic
- `@typescript-eslint/prefer-reduce-type-parameter`: Better generic usage
- `@typescript-eslint/switch-exhaustiveness-check`: Complete switch statements
```

--------------------------------------------------------------------------------
/src/mcp/tools/session-management/__tests__/session_set_defaults.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { sessionStore } from '../../../../utils/session-store.ts';
import plugin, { sessionSetDefaultsLogic } from '../session_set_defaults.ts';

describe('session-set-defaults tool', () => {
  beforeEach(() => {
    sessionStore.clear();
  });

  describe('Export Field Validation (Literal)', () => {
    it('should have correct name', () => {
      expect(plugin.name).toBe('session-set-defaults');
    });

    it('should have correct description', () => {
      expect(plugin.description).toBe(
        'Set the session defaults needed by many tools. Most tools require one or more session defaults to be set before they can be used. Agents should set all relevant defaults up front in a single call (e.g., project/workspace, scheme, simulator or device ID, useLatestOS) to avoid iterative prompts; only set the keys your workflow needs.',
      );
    });

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

    it('should have schema object', () => {
      expect(plugin.schema).toBeDefined();
      expect(typeof plugin.schema).toBe('object');
    });
  });

  describe('Handler Behavior', () => {
    it('should set provided defaults and return updated state', async () => {
      const result = await sessionSetDefaultsLogic({
        scheme: 'MyScheme',
        simulatorName: 'iPhone 16',
        useLatestOS: true,
        arch: 'arm64',
      });

      expect(result.isError).toBe(false);
      expect(result.content[0].text).toContain('Defaults updated:');

      const current = sessionStore.getAll();
      expect(current.scheme).toBe('MyScheme');
      expect(current.simulatorName).toBe('iPhone 16');
      expect(current.useLatestOS).toBe(true);
      expect(current.arch).toBe('arm64');
    });

    it('should validate parameter types via Zod', async () => {
      const result = await plugin.handler({
        useLatestOS: 'yes' as unknown as boolean,
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Parameter validation failed');
      expect(result.content[0].text).toContain('useLatestOS');
    });

    it('should clear workspacePath when projectPath is set', async () => {
      sessionStore.setDefaults({ workspacePath: '/old/App.xcworkspace' });
      const result = await sessionSetDefaultsLogic({ projectPath: '/new/App.xcodeproj' });
      const current = sessionStore.getAll();
      expect(current.projectPath).toBe('/new/App.xcodeproj');
      expect(current.workspacePath).toBeUndefined();
      expect(result.content[0].text).toContain(
        'Cleared workspacePath because projectPath was set.',
      );
    });

    it('should clear projectPath when workspacePath is set', async () => {
      sessionStore.setDefaults({ projectPath: '/old/App.xcodeproj' });
      const result = await sessionSetDefaultsLogic({ workspacePath: '/new/App.xcworkspace' });
      const current = sessionStore.getAll();
      expect(current.workspacePath).toBe('/new/App.xcworkspace');
      expect(current.projectPath).toBeUndefined();
      expect(result.content[0].text).toContain(
        'Cleared projectPath because workspacePath was set.',
      );
    });

    it('should clear simulatorName when simulatorId is set', async () => {
      sessionStore.setDefaults({ simulatorName: 'iPhone 16' });
      const result = await sessionSetDefaultsLogic({ simulatorId: 'SIM-UUID' });
      const current = sessionStore.getAll();
      expect(current.simulatorId).toBe('SIM-UUID');
      expect(current.simulatorName).toBeUndefined();
      expect(result.content[0].text).toContain(
        'Cleared simulatorName because simulatorId was set.',
      );
    });

    it('should clear simulatorId when simulatorName is set', async () => {
      sessionStore.setDefaults({ simulatorId: 'SIM-UUID' });
      const result = await sessionSetDefaultsLogic({ simulatorName: 'iPhone 16' });
      const current = sessionStore.getAll();
      expect(current.simulatorName).toBe('iPhone 16');
      expect(current.simulatorId).toBeUndefined();
      expect(result.content[0].text).toContain(
        'Cleared simulatorId because simulatorName was set.',
      );
    });

    it('should prefer workspacePath when both projectPath and workspacePath are provided', async () => {
      const res = await sessionSetDefaultsLogic({
        projectPath: '/app/App.xcodeproj',
        workspacePath: '/app/App.xcworkspace',
      });
      const current = sessionStore.getAll();
      expect(current.workspacePath).toBe('/app/App.xcworkspace');
      expect(current.projectPath).toBeUndefined();
      expect(res.content[0].text).toContain(
        'Both projectPath and workspacePath were provided; keeping workspacePath and ignoring projectPath.',
      );
    });

    it('should prefer simulatorId when both simulatorId and simulatorName are provided', async () => {
      const res = await sessionSetDefaultsLogic({
        simulatorId: 'SIM-1',
        simulatorName: 'iPhone 16',
      });
      const current = sessionStore.getAll();
      expect(current.simulatorId).toBe('SIM-1');
      expect(current.simulatorName).toBeUndefined();
      expect(res.content[0].text).toContain(
        'Both simulatorId and simulatorName were provided; keeping simulatorId and ignoring simulatorName.',
      );
    });
  });
});

```

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

```typescript
import { describe, it, expect, vi, afterEach } from 'vitest';

// Import the tool and logic
import tool, { record_sim_videoLogic } from '../record_sim_video.ts';
import { createMockFileSystemExecutor } from '../../../../test-utils/mock-executors.ts';

const DUMMY_EXECUTOR: any = (async () => ({ success: true })) as any; // CommandExecutor stub
const VALID_SIM_ID = '00000000-0000-0000-0000-000000000000';

afterEach(() => {
  vi.restoreAllMocks();
});

describe('record_sim_video tool - validation', () => {
  it('errors when start and stop are both true (mutually exclusive)', async () => {
    const res = await tool.handler({
      simulatorId: VALID_SIM_ID,
      start: true,
      stop: true,
    } as any);

    expect(res.isError).toBe(true);
    const text = (res.content?.[0] as any)?.text ?? '';
    expect(text.toLowerCase()).toContain('mutually exclusive');
  });

  it('errors when stop=true but outputFile is missing', async () => {
    const res = await tool.handler({
      simulatorId: VALID_SIM_ID,
      stop: true,
    } as any);

    expect(res.isError).toBe(true);
    const text = (res.content?.[0] as any)?.text ?? '';
    expect(text.toLowerCase()).toContain('outputfile is required');
  });
});

describe('record_sim_video logic - start behavior', () => {
  it('starts with default fps (30) and warns when outputFile is provided on start (ignored)', async () => {
    const video: any = {
      startSimulatorVideoCapture: async () => ({
        started: true,
        sessionId: 'sess-123',
      }),
      stopSimulatorVideoCapture: async () => ({
        stopped: false,
      }),
    };

    // DI for AXe helpers: available and version OK
    const axe = {
      areAxeToolsAvailable: () => true,
      isAxeAtLeastVersion: async () => true,
      createAxeNotAvailableResponse: () => ({
        content: [{ type: 'text' as const, text: 'AXe not available' }],
        isError: true,
      }),
    };

    const fs = createMockFileSystemExecutor();

    const res = await record_sim_videoLogic(
      {
        simulatorId: VALID_SIM_ID,
        start: true,
        // fps omitted to hit default 30
        outputFile: '/tmp/ignored.mp4', // should be ignored with a note
      } as any,
      DUMMY_EXECUTOR,
      axe,
      video,
      fs,
    );

    expect(res.isError).toBe(false);
    const texts = (res.content ?? []).map((c: any) => c.text).join('\n');

    expect(texts).toContain('🎥');
    expect(texts).toMatch(/30\s*fps/i);
    expect(texts.toLowerCase()).toContain('outputfile is ignored');
    expect(texts).toContain('Next Steps');
    expect(texts).toContain('stop: true');
    expect(texts).toContain('outputFile');
  });
});

describe('record_sim_video logic - end-to-end stop with rename', () => {
  it('stops, parses stdout path, and renames to outputFile', async () => {
    const video: any = {
      startSimulatorVideoCapture: async () => ({
        started: true,
        sessionId: 'sess-abc',
      }),
      stopSimulatorVideoCapture: async () => ({
        stopped: true,
        parsedPath: '/tmp/recorded.mp4',
        stdout: 'Saved to /tmp/recorded.mp4',
      }),
    };

    const fs = createMockFileSystemExecutor();

    const axe = {
      areAxeToolsAvailable: () => true,
      isAxeAtLeastVersion: async () => true,
      createAxeNotAvailableResponse: () => ({
        content: [{ type: 'text' as const, text: 'AXe not available' }],
        isError: true,
      }),
    };

    // Start (not strictly required for stop path, but included to mimic flow)
    const startRes = await record_sim_videoLogic(
      {
        simulatorId: VALID_SIM_ID,
        start: true,
      } as any,
      DUMMY_EXECUTOR,
      axe,
      video,
      fs,
    );
    expect(startRes.isError).toBe(false);

    // Stop and rename
    const outputFile = '/var/videos/final.mp4';
    const stopRes = await record_sim_videoLogic(
      {
        simulatorId: VALID_SIM_ID,
        stop: true,
        outputFile,
      } as any,
      DUMMY_EXECUTOR,
      axe,
      video,
      fs,
    );

    expect(stopRes.isError).toBe(false);
    const texts = (stopRes.content ?? []).map((c: any) => c.text).join('\n');
    expect(texts).toContain('Original file: /tmp/recorded.mp4');
    expect(texts).toContain(`Saved to: ${outputFile}`);

    // _meta should include final saved path
    expect((stopRes as any)._meta?.outputFile).toBe(outputFile);
  });
});

describe('record_sim_video logic - version gate', () => {
  it('errors when AXe version is below 1.1.0', async () => {
    const axe = {
      areAxeToolsAvailable: () => true,
      isAxeAtLeastVersion: async () => false,
      createAxeNotAvailableResponse: () => ({
        content: [{ type: 'text' as const, text: 'AXe not available' }],
        isError: true,
      }),
    };

    const video: any = {
      startSimulatorVideoCapture: async () => ({
        started: true,
        sessionId: 'sess-xyz',
      }),
      stopSimulatorVideoCapture: async () => ({
        stopped: true,
      }),
    };

    const fs = createMockFileSystemExecutor();

    const res = await record_sim_videoLogic(
      {
        simulatorId: VALID_SIM_ID,
        start: true,
      } as any,
      DUMMY_EXECUTOR,
      axe,
      video,
      fs,
    );

    expect(res.isError).toBe(true);
    const text = (res.content?.[0] as any)?.text ?? '';
    expect(text).toContain('AXe v1.1.0');
  });
});

```

--------------------------------------------------------------------------------
/src/mcp/tools/swift-package/__tests__/active-processes.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tests for active-processes module
 * Following CLAUDE.md testing standards with literal validation
 */

import { describe, it, expect, beforeEach } from 'vitest';
import {
  activeProcesses,
  getProcess,
  addProcess,
  removeProcess,
  clearAllProcesses,
  type ProcessInfo,
} from '../active-processes.ts';

describe('active-processes module', () => {
  // Clear the map before each test
  beforeEach(() => {
    clearAllProcesses();
  });

  describe('activeProcesses Map', () => {
    it('should be a Map instance', () => {
      expect(activeProcesses instanceof Map).toBe(true);
    });

    it('should start empty after clearing', () => {
      expect(activeProcesses.size).toBe(0);
    });
  });

  describe('getProcess function', () => {
    it('should return undefined for non-existent process', () => {
      const result = getProcess(12345);
      expect(result).toBe(undefined);
    });

    it('should return process info for existing process', () => {
      const mockProcess = {
        kill: () => {},
        on: () => {},
        pid: 12345,
      };
      const startedAt = new Date('2023-01-01T10:00:00.000Z');
      const processInfo: ProcessInfo = {
        process: mockProcess,
        startedAt: startedAt,
      };

      activeProcesses.set(12345, processInfo);
      const result = getProcess(12345);

      expect(result).toEqual({
        process: mockProcess,
        startedAt: startedAt,
      });
    });
  });

  describe('addProcess function', () => {
    it('should add process to the map', () => {
      const mockProcess = {
        kill: () => {},
        on: () => {},
        pid: 67890,
      };
      const startedAt = new Date('2023-02-15T14:30:00.000Z');
      const processInfo: ProcessInfo = {
        process: mockProcess,
        startedAt: startedAt,
      };

      addProcess(67890, processInfo);

      expect(activeProcesses.size).toBe(1);
      expect(activeProcesses.get(67890)).toEqual(processInfo);
    });

    it('should overwrite existing process with same pid', () => {
      const mockProcess1 = {
        kill: () => {},
        on: () => {},
        pid: 11111,
      };
      const mockProcess2 = {
        kill: () => {},
        on: () => {},
        pid: 11111,
      };
      const startedAt1 = new Date('2023-01-01T10:00:00.000Z');
      const startedAt2 = new Date('2023-01-01T11:00:00.000Z');

      addProcess(11111, { process: mockProcess1, startedAt: startedAt1 });
      addProcess(11111, { process: mockProcess2, startedAt: startedAt2 });

      expect(activeProcesses.size).toBe(1);
      expect(activeProcesses.get(11111)).toEqual({
        process: mockProcess2,
        startedAt: startedAt2,
      });
    });
  });

  describe('removeProcess function', () => {
    it('should return false for non-existent process', () => {
      const result = removeProcess(99999);
      expect(result).toBe(false);
    });

    it('should return true and remove existing process', () => {
      const mockProcess = {
        kill: () => {},
        on: () => {},
        pid: 54321,
      };
      const processInfo: ProcessInfo = {
        process: mockProcess,
        startedAt: new Date('2023-03-20T09:15:00.000Z'),
      };

      addProcess(54321, processInfo);
      expect(activeProcesses.size).toBe(1);

      const result = removeProcess(54321);

      expect(result).toBe(true);
      expect(activeProcesses.size).toBe(0);
      expect(activeProcesses.get(54321)).toBe(undefined);
    });
  });

  describe('clearAllProcesses function', () => {
    it('should clear all processes from the map', () => {
      const mockProcess1 = {
        kill: () => {},
        on: () => {},
        pid: 1111,
      };
      const mockProcess2 = {
        kill: () => {},
        on: () => {},
        pid: 2222,
      };

      addProcess(1111, { process: mockProcess1, startedAt: new Date() });
      addProcess(2222, { process: mockProcess2, startedAt: new Date() });

      expect(activeProcesses.size).toBe(2);

      clearAllProcesses();

      expect(activeProcesses.size).toBe(0);
    });

    it('should work on already empty map', () => {
      expect(activeProcesses.size).toBe(0);
      clearAllProcesses();
      expect(activeProcesses.size).toBe(0);
    });
  });

  describe('ProcessInfo interface', () => {
    it('should work with complete process object', () => {
      const mockProcess = {
        kill: () => {},
        on: () => {},
        pid: 12345,
      };
      const startedAt = new Date('2023-01-01T10:00:00.000Z');
      const processInfo: ProcessInfo = {
        process: mockProcess,
        startedAt: startedAt,
      };

      addProcess(12345, processInfo);
      const retrieved = getProcess(12345);

      expect(retrieved).toEqual({
        process: {
          kill: expect.any(Function),
          on: expect.any(Function),
          pid: 12345,
        },
        startedAt: startedAt,
      });
    });

    it('should work with minimal process object', () => {
      const mockProcess = {
        kill: () => {},
        on: () => {},
      };
      const startedAt = new Date('2023-01-01T10:00:00.000Z');
      const processInfo: ProcessInfo = {
        process: mockProcess,
        startedAt: startedAt,
      };

      addProcess(98765, processInfo);
      const retrieved = getProcess(98765);

      expect(retrieved).toEqual({
        process: {
          kill: expect.any(Function),
          on: expect.any(Function),
        },
        startedAt: startedAt,
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/src/mcp/tools/session-management/session_set_defaults.ts:
--------------------------------------------------------------------------------

```typescript
import * as z from 'zod';
import { sessionStore, type SessionDefaults } from '../../../utils/session-store.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import type { ToolResponse } from '../../../types/common.ts';

const baseSchema = z.object({
  projectPath: z
    .string()
    .optional()
    .describe(
      'Xcode project (.xcodeproj) path. Mutually exclusive with workspacePath. Required for most build/test tools when workspacePath is not set.',
    ),
  workspacePath: z
    .string()
    .optional()
    .describe(
      'Xcode workspace (.xcworkspace) path. Mutually exclusive with projectPath. Required for most build/test tools when projectPath is not set.',
    ),
  scheme: z
    .string()
    .optional()
    .describe(
      'Xcode scheme. Required by most build/test tools. Use list_schemes to discover available schemes before setting.',
    ),
  configuration: z.string().optional().describe('Build configuration, e.g. Debug or Release.'),
  simulatorName: z
    .string()
    .optional()
    .describe(
      'Simulator device name for simulator workflows. If simulatorId is also provided, simulatorId is preferred and simulatorName is ignored.',
    ),
  simulatorId: z
    .string()
    .optional()
    .describe(
      'Simulator UUID for simulator workflows. Preferred over simulatorName when both are provided.',
    ),
  deviceId: z.string().optional().describe('Physical device ID for device workflows.'),
  useLatestOS: z
    .boolean()
    .optional()
    .describe('When true, prefer the latest available OS for simulatorName lookups.'),
  arch: z.enum(['arm64', 'x86_64']).optional().describe('Target architecture for macOS builds.'),
  suppressWarnings: z
    .boolean()
    .optional()
    .describe('When true, warning messages are filtered from build output to conserve context'),
});

const schemaObj = baseSchema;

type Params = z.infer<typeof schemaObj>;

export async function sessionSetDefaultsLogic(params: Params): Promise<ToolResponse> {
  const notices: string[] = [];
  const current = sessionStore.getAll();
  const nextParams: Partial<SessionDefaults> = { ...params };

  const hasProjectPath =
    Object.prototype.hasOwnProperty.call(params, 'projectPath') && params.projectPath !== undefined;
  const hasWorkspacePath =
    Object.prototype.hasOwnProperty.call(params, 'workspacePath') &&
    params.workspacePath !== undefined;
  const hasSimulatorId =
    Object.prototype.hasOwnProperty.call(params, 'simulatorId') && params.simulatorId !== undefined;
  const hasSimulatorName =
    Object.prototype.hasOwnProperty.call(params, 'simulatorName') &&
    params.simulatorName !== undefined;

  if (hasProjectPath && hasWorkspacePath) {
    delete nextParams.projectPath;
    notices.push(
      'Both projectPath and workspacePath were provided; keeping workspacePath and ignoring projectPath.',
    );
  }

  if (hasSimulatorId && hasSimulatorName) {
    delete nextParams.simulatorName;
    notices.push(
      'Both simulatorId and simulatorName were provided; keeping simulatorId and ignoring simulatorName.',
    );
  }

  // Clear mutually exclusive counterparts before merging new defaults
  const toClear = new Set<keyof SessionDefaults>();
  if (
    Object.prototype.hasOwnProperty.call(nextParams, 'projectPath') &&
    nextParams.projectPath !== undefined
  ) {
    toClear.add('workspacePath');
    if (current.workspacePath !== undefined) {
      notices.push('Cleared workspacePath because projectPath was set.');
    }
  }
  if (
    Object.prototype.hasOwnProperty.call(nextParams, 'workspacePath') &&
    nextParams.workspacePath !== undefined
  ) {
    toClear.add('projectPath');
    if (current.projectPath !== undefined) {
      notices.push('Cleared projectPath because workspacePath was set.');
    }
  }
  if (
    Object.prototype.hasOwnProperty.call(nextParams, 'simulatorId') &&
    nextParams.simulatorId !== undefined
  ) {
    toClear.add('simulatorName');
    if (current.simulatorName !== undefined) {
      notices.push('Cleared simulatorName because simulatorId was set.');
    }
  }
  if (
    Object.prototype.hasOwnProperty.call(nextParams, 'simulatorName') &&
    nextParams.simulatorName !== undefined
  ) {
    toClear.add('simulatorId');
    if (current.simulatorId !== undefined) {
      notices.push('Cleared simulatorId because simulatorName was set.');
    }
  }

  if (toClear.size > 0) {
    sessionStore.clear(Array.from(toClear));
  }

  sessionStore.setDefaults(nextParams as Partial<SessionDefaults>);
  const updated = sessionStore.getAll();
  const noticeText = notices.length > 0 ? `\nNotices:\n- ${notices.join('\n- ')}` : '';
  return {
    content: [
      {
        type: 'text',
        text: `Defaults updated:\n${JSON.stringify(updated, null, 2)}${noticeText}`,
      },
    ],
    isError: false,
  };
}

export default {
  name: 'session-set-defaults',
  description:
    'Set the session defaults needed by many tools. Most tools require one or more session defaults to be set before they can be used. Agents should set all relevant defaults up front in a single call (e.g., project/workspace, scheme, simulator or device ID, useLatestOS) to avoid iterative prompts; only set the keys your workflow needs.',
  schema: baseSchema.shape,
  annotations: {
    title: 'Set Session Defaults',
    destructiveHint: true,
  },
  handler: createTypedTool(schemaObj, sessionSetDefaultsLogic, getDefaultCommandExecutor),
};

```

--------------------------------------------------------------------------------
/src/mcp/tools/utilities/__tests__/clean.test.ts:
--------------------------------------------------------------------------------

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

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

  it('exports correct name/description/schema/handler', () => {
    expect(tool.name).toBe('clean');
    expect(tool.description).toBe('Cleans build products with xcodebuild.');
    expect(typeof tool.handler).toBe('function');

    const schema = z.strictObject(tool.schema);
    expect(schema.safeParse({}).success).toBe(true);
    expect(
      schema.safeParse({
        derivedDataPath: '/tmp/Derived',
        extraArgs: ['--quiet'],
        preferXcodebuild: true,
        platform: 'iOS Simulator',
      }).success,
    ).toBe(true);
    expect(schema.safeParse({ configuration: 'Debug' }).success).toBe(false);

    const schemaKeys = Object.keys(tool.schema).sort();
    expect(schemaKeys).toEqual(
      ['derivedDataPath', 'extraArgs', 'platform', 'preferXcodebuild'].sort(),
    );
  });

  it('handler validation: error when neither projectPath nor workspacePath provided', async () => {
    const result = await (tool as any).handler({});
    expect(result.isError).toBe(true);
    const text = String(result.content?.[0]?.text ?? '');
    expect(text).toContain('Missing required session defaults');
    expect(text).toContain('Provide a project or workspace');
  });

  it('handler validation: error when both projectPath and workspacePath provided', async () => {
    const result = await (tool as any).handler({
      projectPath: '/p.xcodeproj',
      workspacePath: '/w.xcworkspace',
    });
    expect(result.isError).toBe(true);
    const text = String(result.content?.[0]?.text ?? '');
    expect(text).toContain('Mutually exclusive parameters provided');
  });

  it('runs project-path flow via logic', async () => {
    const mock = createMockExecutor({ success: true, output: 'ok' });
    const result = await cleanLogic({ projectPath: '/p.xcodeproj', scheme: 'App' } as any, mock);
    expect(result.isError).not.toBe(true);
  });

  it('runs workspace-path flow via logic', async () => {
    const mock = createMockExecutor({ success: true, output: 'ok' });
    const result = await cleanLogic(
      { workspacePath: '/w.xcworkspace', scheme: 'App' } as any,
      mock,
    );
    expect(result.isError).not.toBe(true);
  });

  it('handler validation: requires scheme when workspacePath is provided', async () => {
    const result = await (tool as any).handler({ workspacePath: '/w.xcworkspace' });
    expect(result.isError).toBe(true);
    const text = String(result.content?.[0]?.text ?? '');
    expect(text).toContain('Parameter validation failed');
    expect(text).toContain('scheme is required when workspacePath is provided');
  });

  it('uses iOS platform by default', async () => {
    let capturedCommand: string[] = [];
    const mockExecutor = async (command: string[]) => {
      capturedCommand = command;
      return createMockCommandResponse({ success: true, output: 'clean success' });
    };

    const result = await cleanLogic(
      { projectPath: '/p.xcodeproj', scheme: 'App' } as any,
      mockExecutor,
    );
    expect(result.isError).not.toBe(true);

    // Check that the command contains iOS platform destination
    const commandStr = capturedCommand.join(' ');
    expect(commandStr).toContain('-destination');
    expect(commandStr).toContain('platform=iOS');
  });

  it('accepts custom platform parameter', async () => {
    let capturedCommand: string[] = [];
    const mockExecutor = async (command: string[]) => {
      capturedCommand = command;
      return createMockCommandResponse({ success: true, output: 'clean success' });
    };

    const result = await cleanLogic(
      {
        projectPath: '/p.xcodeproj',
        scheme: 'App',
        platform: 'macOS',
      } as any,
      mockExecutor,
    );
    expect(result.isError).not.toBe(true);

    // Check that the command contains macOS platform destination
    const commandStr = capturedCommand.join(' ');
    expect(commandStr).toContain('-destination');
    expect(commandStr).toContain('platform=macOS');
  });

  it('accepts iOS Simulator platform parameter (maps to iOS for clean)', async () => {
    let capturedCommand: string[] = [];
    const mockExecutor = async (command: string[]) => {
      capturedCommand = command;
      return createMockCommandResponse({ success: true, output: 'clean success' });
    };

    const result = await cleanLogic(
      {
        projectPath: '/p.xcodeproj',
        scheme: 'App',
        platform: 'iOS Simulator',
      } as any,
      mockExecutor,
    );
    expect(result.isError).not.toBe(true);

    // For clean operations, iOS Simulator should be mapped to iOS platform
    const commandStr = capturedCommand.join(' ');
    expect(commandStr).toContain('-destination');
    expect(commandStr).toContain('platform=iOS');
  });

  it('handler validation: rejects invalid platform values', async () => {
    const result = await (tool as any).handler({
      projectPath: '/p.xcodeproj',
      scheme: 'App',
      platform: 'InvalidPlatform',
    });
    expect(result.isError).toBe(true);
    const text = String(result.content?.[0]?.text ?? '');
    expect(text).toContain('Parameter validation failed');
    expect(text).toContain('platform');
  });
});

```

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

```typescript
/**
 * Simulator Test Plugin: Test Simulator (Unified)
 *
 * Runs tests for a project or workspace on a simulator by UUID or name.
 * Accepts mutually exclusive `projectPath` or `workspacePath`.
 * Accepts mutually exclusive `simulatorId` or `simulatorName`.
 */

import * as z from 'zod';
import { handleTestLogic } from '../../../utils/test/index.ts';
import { log } from '../../../utils/logging/index.ts';
import { XcodePlatform } from '../../../types/common.ts';
import { ToolResponse } from '../../../types/common.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';

// Define base schema object with all fields
const baseSchemaObject = z.object({
  projectPath: z
    .string()
    .optional()
    .describe('Path to .xcodeproj file. Provide EITHER this OR workspacePath, not both'),
  workspacePath: z
    .string()
    .optional()
    .describe('Path to .xcworkspace file. Provide EITHER this OR projectPath, not both'),
  scheme: z.string().describe('The scheme to use (Required)'),
  simulatorId: z
    .string()
    .optional()
    .describe(
      'UUID of the simulator (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",
    ),
  configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
  derivedDataPath: z
    .string()
    .optional()
    .describe('Path where build products and other derived data will go'),
  extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'),
  useLatestOS: z
    .boolean()
    .optional()
    .describe('Whether to use the latest OS version for the named simulator'),
  preferXcodebuild: z
    .boolean()
    .optional()
    .describe(
      'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.',
    ),
  testRunnerEnv: z
    .record(z.string(), z.string())
    .optional()
    .describe(
      'Environment variables to pass to the test runner (TEST_RUNNER_ prefix added automatically)',
    ),
});

// Apply XOR validation: exactly one of projectPath OR workspacePath, and exactly one of simulatorId OR simulatorName required
const testSimulatorSchema = 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.',
    })
    .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.',
    }),
);

// Use z.infer for type safety
type TestSimulatorParams = z.infer<typeof testSimulatorSchema>;

export async function test_simLogic(
  params: TestSimulatorParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  // Log warning if useLatestOS is provided with simulatorId
  if (params.simulatorId && params.useLatestOS !== undefined) {
    log(
      'warning',
      `useLatestOS parameter is ignored when using simulatorId (UUID implies exact device/OS)`,
    );
  }

  return handleTestLogic(
    {
      projectPath: params.projectPath,
      workspacePath: params.workspacePath,
      scheme: params.scheme,
      simulatorId: params.simulatorId,
      simulatorName: params.simulatorName,
      configuration: params.configuration ?? 'Debug',
      derivedDataPath: params.derivedDataPath,
      extraArgs: params.extraArgs,
      useLatestOS: params.simulatorId ? false : (params.useLatestOS ?? false),
      preferXcodebuild: params.preferXcodebuild ?? false,
      platform: XcodePlatform.iOSSimulator,
      testRunnerEnv: params.testRunnerEnv,
    },
    executor,
  );
}

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

export default {
  name: 'test_sim',
  description: 'Runs tests on an iOS simulator.',
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: baseSchemaObject,
  }),
  annotations: {
    title: 'Test Simulator',
    destructiveHint: true,
  },
  handler: createSessionAwareTool<TestSimulatorParams>({
    internalSchema: testSimulatorSchema as unknown as z.ZodType<TestSimulatorParams, unknown>,
    logicFunction: test_simLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { allOf: ['scheme'], message: 'scheme is required' },
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
      { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
    ],
    exclusivePairs: [
      ['projectPath', 'workspacePath'],
      ['simulatorId', 'simulatorName'],
    ],
  }),
};

```

--------------------------------------------------------------------------------
/src/utils/__tests__/test-runner-env-integration.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Integration tests for TEST_RUNNER_ environment variable passing
 *
 * These tests verify that testRunnerEnv parameters are correctly processed
 * and passed through the execution chain. We focus on testing the core
 * functionality that matters most: environment variable normalization.
 */

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

describe('TEST_RUNNER_ Environment Variable Integration', () => {
  describe('Core normalization functionality', () => {
    it('should normalize environment variables correctly for real scenarios', () => {
      // Test the GitHub issue scenario: USE_DEV_MODE -> TEST_RUNNER_USE_DEV_MODE
      const gitHubIssueScenario = { USE_DEV_MODE: 'YES' };
      const normalized = normalizeTestRunnerEnv(gitHubIssueScenario);

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

    it('should handle mixed prefixed and unprefixed variables', () => {
      const mixedVars = {
        USE_DEV_MODE: 'YES', // Should be prefixed
        TEST_RUNNER_SKIP_ANIMATIONS: '1', // Already prefixed, preserve
        DEBUG_MODE: 'true', // Should be prefixed
      };

      const normalized = normalizeTestRunnerEnv(mixedVars);

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

    it('should filter out null and undefined values', () => {
      const varsWithNulls = {
        VALID_VAR: 'value1',
        NULL_VAR: null as any,
        UNDEFINED_VAR: undefined as any,
        ANOTHER_VALID: 'value2',
      };

      const normalized = normalizeTestRunnerEnv(varsWithNulls);

      expect(normalized).toEqual({
        TEST_RUNNER_VALID_VAR: 'value1',
        TEST_RUNNER_ANOTHER_VALID: 'value2',
      });

      // Ensure null/undefined vars are not present
      expect(normalized).not.toHaveProperty('TEST_RUNNER_NULL_VAR');
      expect(normalized).not.toHaveProperty('TEST_RUNNER_UNDEFINED_VAR');
    });

    it('should handle special characters in keys and values', () => {
      const specialChars = {
        'VAR_WITH-DASH': 'value-with-dash',
        'VAR.WITH.DOTS': 'value/with/slashes',
        VAR_WITH_SPACES: 'value with spaces',
        TEST_RUNNER_PRE_EXISTING: 'already=prefixed=value',
      };

      const normalized = normalizeTestRunnerEnv(specialChars);

      expect(normalized).toEqual({
        'TEST_RUNNER_VAR_WITH-DASH': 'value-with-dash',
        'TEST_RUNNER_VAR.WITH.DOTS': 'value/with/slashes',
        TEST_RUNNER_VAR_WITH_SPACES: 'value with spaces',
        TEST_RUNNER_PRE_EXISTING: 'already=prefixed=value',
      });
    });

    it('should handle empty values correctly', () => {
      const emptyValues = {
        EMPTY_STRING: '',
        NORMAL_VAR: 'normal_value',
      };

      const normalized = normalizeTestRunnerEnv(emptyValues);

      expect(normalized).toEqual({
        TEST_RUNNER_EMPTY_STRING: '',
        TEST_RUNNER_NORMAL_VAR: 'normal_value',
      });
    });

    it('should handle edge case prefix variations', () => {
      const prefixEdgeCases = {
        TEST_RUN: 'not_quite_prefixed', // Should get prefixed
        TEST_RUNNER: 'no_underscore', // Should get prefixed
        TEST_RUNNER_CORRECT: 'already_good', // Should stay as-is
        test_runner_lowercase: 'lowercase', // Should get prefixed (case sensitive)
      };

      const normalized = normalizeTestRunnerEnv(prefixEdgeCases);

      expect(normalized).toEqual({
        TEST_RUNNER_TEST_RUN: 'not_quite_prefixed',
        TEST_RUNNER_TEST_RUNNER: 'no_underscore',
        TEST_RUNNER_CORRECT: 'already_good',
        TEST_RUNNER_test_runner_lowercase: 'lowercase',
      });
    });

    it('should preserve immutability of input object', () => {
      const originalInput = { FOO: 'bar', BAZ: 'qux' };
      const inputCopy = { ...originalInput };

      const normalized = normalizeTestRunnerEnv(originalInput);

      // Original should be unchanged
      expect(originalInput).toEqual(inputCopy);

      // Result should be different
      expect(normalized).not.toEqual(originalInput);
      expect(normalized).toEqual({
        TEST_RUNNER_FOO: 'bar',
        TEST_RUNNER_BAZ: 'qux',
      });
    });

    it('should handle the complete test environment workflow', () => {
      // Simulate a comprehensive test environment setup
      const fullTestEnv = {
        // Core testing flags
        USE_DEV_MODE: 'YES',
        SKIP_ANIMATIONS: '1',
        FAST_MODE: 'true',

        // Already prefixed variables (user might provide these)
        TEST_RUNNER_TIMEOUT: '30',
        TEST_RUNNER_RETRIES: '3',

        // UI testing specific
        UI_TESTING_MODE: 'enabled',
        SCREENSHOT_MODE: 'disabled',

        // Performance testing
        PERFORMANCE_TESTS: 'false',
        MEMORY_TESTING: 'true',

        // Special values
        EMPTY_VAR: '',
        PATH_VAR: '/usr/local/bin:/usr/bin',
      };

      const normalized = normalizeTestRunnerEnv(fullTestEnv);

      expect(normalized).toEqual({
        TEST_RUNNER_USE_DEV_MODE: 'YES',
        TEST_RUNNER_SKIP_ANIMATIONS: '1',
        TEST_RUNNER_FAST_MODE: 'true',
        TEST_RUNNER_TIMEOUT: '30',
        TEST_RUNNER_RETRIES: '3',
        TEST_RUNNER_UI_TESTING_MODE: 'enabled',
        TEST_RUNNER_SCREENSHOT_MODE: 'disabled',
        TEST_RUNNER_PERFORMANCE_TESTS: 'false',
        TEST_RUNNER_MEMORY_TESTING: 'true',
        TEST_RUNNER_EMPTY_VAR: '',
        TEST_RUNNER_PATH_VAR: '/usr/local/bin:/usr/bin',
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/src/utils/debugger/backends/__tests__/dap-backend.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, expect, it } from 'vitest';

import type { DapEvent, DapRequest, DapResponse } from '../../dap/types.ts';
import { createDapBackend } from '../dap-backend.ts';
import {
  createMockExecutor,
  createMockInteractiveSpawner,
  type MockInteractiveSession,
} from '../../../../test-utils/mock-executors.ts';
import type { BreakpointSpec } from '../../types.ts';

type ResponsePlan = {
  body?: Record<string, unknown>;
  events?: DapEvent[];
};

function encodeMessage(message: Record<string, unknown>): string {
  const payload = JSON.stringify(message);
  return `Content-Length: ${Buffer.byteLength(payload, 'utf8')}\r\n\r\n${payload}`;
}

function createDapSpawner(handlers: Record<string, (request: DapRequest) => ResponsePlan>) {
  let buffer = Buffer.alloc(0);
  let responseSeq = 1000;

  return createMockInteractiveSpawner({
    onWrite: (data: string, session: MockInteractiveSession) => {
      buffer = Buffer.concat([buffer, Buffer.from(data, 'utf8')]);
      while (true) {
        const headerEnd = buffer.indexOf('\r\n\r\n');
        if (headerEnd === -1) return;
        const header = buffer.slice(0, headerEnd).toString('utf8');
        const match = header.match(/Content-Length:\s*(\d+)/i);
        if (!match) {
          buffer = buffer.slice(headerEnd + 4);
          continue;
        }
        const length = Number(match[1]);
        const bodyStart = headerEnd + 4;
        const bodyEnd = bodyStart + length;
        if (buffer.length < bodyEnd) return;

        const body = buffer.slice(bodyStart, bodyEnd).toString('utf8');
        buffer = buffer.slice(bodyEnd);
        const request = JSON.parse(body) as DapRequest;
        const handler = handlers[request.command];
        if (!handler) {
          throw new Error(`Unexpected DAP request: ${request.command}`);
        }
        const plan = handler(request);
        if (plan.events) {
          for (const event of plan.events) {
            session.stdout.write(encodeMessage(event));
          }
        }
        const response: DapResponse = {
          seq: responseSeq++,
          type: 'response',
          request_seq: request.seq,
          success: true,
          command: request.command,
          body: plan.body,
        };
        session.stdout.write(encodeMessage(response));
      }
    },
  });
}

function createDefaultHandlers() {
  return {
    initialize: () => ({ body: { supportsConfigurationDoneRequest: true } }),
    attach: () => ({ body: {} }),
    configurationDone: () => ({ body: {} }),
    threads: () => ({ body: { threads: [{ id: 1, name: 'main' }] } }),
    stackTrace: () => ({
      body: {
        stackFrames: [
          {
            id: 11,
            name: 'main',
            source: { path: '/tmp/main.swift' },
            line: 42,
          },
        ],
      },
    }),
    scopes: () => ({
      body: {
        scopes: [{ name: 'Locals', variablesReference: 100 }],
      },
    }),
    variables: () => ({
      body: {
        variables: [{ name: 'answer', value: '42', type: 'Int' }],
      },
    }),
    evaluate: () => ({
      body: {
        result: 'ok',
        output: 'evaluated',
      },
    }),
    setBreakpoints: (request: DapRequest) => {
      const args = request.arguments as { breakpoints: Array<{ line: number }> };
      const breakpoints = (args?.breakpoints ?? []).map((bp, index) => ({
        id: 100 + index,
        line: bp.line,
        verified: true,
      }));
      return { body: { breakpoints } };
    },
    setFunctionBreakpoints: (request: DapRequest) => {
      const args = request.arguments as { breakpoints: Array<{ name: string }> };
      const breakpoints = (args?.breakpoints ?? []).map((bp, index) => ({
        id: 200 + index,
        verified: true,
      }));
      return { body: { breakpoints } };
    },
    disconnect: () => ({ body: {} }),
  } satisfies Record<string, (request: DapRequest) => ResponsePlan>;
}

describe('DapBackend', () => {
  it('maps stack, variables, and evaluate', async () => {
    const handlers = createDefaultHandlers();
    const spawner = createDapSpawner(handlers);
    const executor = createMockExecutor({ success: true, output: '/usr/bin/lldb-dap' });

    const backend = await createDapBackend({ executor, spawner, requestTimeoutMs: 1_000 });
    await backend.attach({ pid: 4242, simulatorId: 'sim-1' });

    const stack = await backend.getStack();
    expect(stack).toContain('frame #0: main at /tmp/main.swift:42');

    const vars = await backend.getVariables();
    expect(vars).toContain('Locals');
    expect(vars).toContain('answer (Int) = 42');

    const output = await backend.runCommand('frame variable');
    expect(output).toContain('evaluated');

    await backend.detach();
    await backend.dispose();
  });

  it('adds and removes breakpoints', async () => {
    const handlers = createDefaultHandlers();
    const spawner = createDapSpawner(handlers);
    const executor = createMockExecutor({ success: true, output: '/usr/bin/lldb-dap' });

    const backend = await createDapBackend({ executor, spawner, requestTimeoutMs: 1_000 });
    await backend.attach({ pid: 4242, simulatorId: 'sim-1' });

    const fileSpec: BreakpointSpec = { kind: 'file-line', file: '/tmp/main.swift', line: 12 };
    const fileBreakpoint = await backend.addBreakpoint(fileSpec, { condition: 'answer == 42' });
    expect(fileBreakpoint.id).toBe(100);

    await backend.removeBreakpoint(fileBreakpoint.id);

    const fnSpec: BreakpointSpec = { kind: 'function', name: 'doWork' };
    const fnBreakpoint = await backend.addBreakpoint(fnSpec);
    expect(fnBreakpoint.id).toBe(200);

    await backend.removeBreakpoint(fnBreakpoint.id);

    await backend.detach();
    await backend.dispose();
  });
});

```

--------------------------------------------------------------------------------
/src/mcp/tools/swift-package/__tests__/swift_package_clean.test.ts:
--------------------------------------------------------------------------------

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

import { describe, it, expect } from 'vitest';
import {
  createMockExecutor,
  createMockFileSystemExecutor,
  createNoopExecutor,
  createMockCommandResponse,
} from '../../../../test-utils/mock-executors.ts';
import swiftPackageClean, { swift_package_cleanLogic } from '../swift_package_clean.ts';
import type { CommandExecutor } from '../../../../utils/execution/index.ts';

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

    it('should have correct description', () => {
      expect(swiftPackageClean.description).toBe(
        'Cleans Swift Package build artifacts and derived data',
      );
    });

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

    it('should validate schema correctly', () => {
      // Test required fields
      expect(swiftPackageClean.schema.packagePath.safeParse('/test/package').success).toBe(true);
      expect(swiftPackageClean.schema.packagePath.safeParse('').success).toBe(true);

      // Test invalid inputs
      expect(swiftPackageClean.schema.packagePath.safeParse(null).success).toBe(false);
      expect(swiftPackageClean.schema.packagePath.safeParse(undefined).success).toBe(false);
    });
  });

  describe('Command Generation Testing', () => {
    it('should build correct command for clean', async () => {
      const calls: Array<{
        command: string[];
        description?: string;
        useShell?: boolean;
        opts?: { env?: Record<string, string>; cwd?: string };
      }> = [];

      const mockExecutor: CommandExecutor = async (command, description, useShell, opts) => {
        calls.push({ command, description, useShell, opts });
        return createMockCommandResponse({
          success: true,
          output: 'Clean succeeded',
          error: undefined,
        });
      };

      await swift_package_cleanLogic(
        {
          packagePath: '/test/package',
        },
        mockExecutor,
      );

      expect(calls).toHaveLength(1);
      expect(calls[0]).toEqual({
        command: ['swift', 'package', '--package-path', '/test/package', 'clean'],
        description: 'Swift Package Clean',
        useShell: true,
        opts: undefined,
      });
    });
  });

  describe('Response Logic Testing', () => {
    it('should handle valid params without validation errors in logic function', async () => {
      // Note: The logic function assumes valid params since createTypedTool handles validation
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Package cleaned successfully',
      });

      const result = await swift_package_cleanLogic(
        {
          packagePath: '/test/package',
        },
        mockExecutor,
      );

      expect(result.isError).toBe(false);
      expect(result.content[0].text).toBe('✅ Swift package cleaned successfully.');
    });

    it('should return successful clean response', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Package cleaned successfully',
      });

      const result = await swift_package_cleanLogic(
        {
          packagePath: '/test/package',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          { type: 'text', text: '✅ Swift package cleaned successfully.' },
          {
            type: 'text',
            text: '💡 Build artifacts and derived data removed. Ready for fresh build.',
          },
          { type: 'text', text: 'Package cleaned successfully' },
        ],
        isError: false,
      });
    });

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

      const result = await swift_package_cleanLogic(
        {
          packagePath: '/test/package',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          { type: 'text', text: '✅ Swift package cleaned successfully.' },
          {
            type: 'text',
            text: '💡 Build artifacts and derived data removed. Ready for fresh build.',
          },
          { type: 'text', text: '(clean completed silently)' },
        ],
        isError: false,
      });
    });

    it('should return error response for clean failure', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Permission denied',
      });

      const result = await swift_package_cleanLogic(
        {
          packagePath: '/test/package',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Swift package clean failed\nDetails: Permission denied',
          },
        ],
        isError: true,
      });
    });

    it('should handle spawn error', async () => {
      const mockExecutor = async () => {
        throw new Error('spawn ENOENT');
      };

      const result = await swift_package_cleanLogic(
        {
          packagePath: '/test/package',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Failed to execute swift package clean\nDetails: spawn ENOENT',
          },
        ],
        isError: true,
      });
    });
  });
});

```

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

```typescript
/**
 * Tests for launch_app_logs_sim plugin (session-aware version)
 * Follows CLAUDE.md guidance with literal validation and DI.
 */

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

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

  describe('Export Field Validation (Literal)', () => {
    it('should expose correct metadata', () => {
      expect(launchAppLogsSim.name).toBe('launch_app_logs_sim');
      expect(launchAppLogsSim.description).toBe(
        'Launches an app in an iOS simulator and captures its logs.',
      );
    });

    it('should expose only non-session fields in public schema', () => {
      const schema = z.object(launchAppLogsSim.schema);

      expect(schema.safeParse({ bundleId: 'com.example.app' }).success).toBe(true);
      expect(schema.safeParse({ bundleId: 'com.example.app', args: ['--debug'] }).success).toBe(
        true,
      );
      expect(schema.safeParse({}).success).toBe(false);
      expect(schema.safeParse({ bundleId: 42 }).success).toBe(false);

      expect(Object.keys(launchAppLogsSim.schema).sort()).toEqual(['args', 'bundleId'].sort());

      const withSimId = schema.safeParse({
        simulatorId: 'abc123',
        bundleId: 'com.example.app',
      });
      expect(withSimId.success).toBe(true);
      expect('simulatorId' in (withSimId.data as Record<string, unknown>)).toBe(false);
    });
  });

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

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Missing required session defaults');
      expect(result.content[0].text).toContain('simulatorId is required');
      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 launchAppLogsSim.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',
      );
    });
  });

  describe('Logic Behavior (Literal Returns)', () => {
    it('should handle successful app launch with log capture', async () => {
      let capturedParams: unknown = null;
      const logCaptureStub: LogCaptureFunction = async (params) => {
        capturedParams = params;
        return {
          sessionId: 'test-session-123',
          logFilePath: '/tmp/xcodemcp_sim_log_test-session-123.log',
          processes: [],
          error: undefined,
        };
      };

      const mockExecutor = createMockExecutor({ success: true, output: '' });

      const result = await launch_app_logs_simLogic(
        {
          simulatorId: 'test-uuid-123',
          bundleId: 'com.example.testapp',
        },
        mockExecutor,
        logCaptureStub,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: `App launched successfully in simulator test-uuid-123 with log capture enabled.\n\nLog capture session ID: test-session-123\n\nNext Steps:\n1. Interact with your app in the simulator.\n2. Use 'stop_and_get_simulator_log({ logSessionId: "test-session-123" })' to stop capture and retrieve logs.`,
          },
        ],
        isError: false,
      });

      expect(capturedParams).toEqual({
        simulatorUuid: 'test-uuid-123',
        bundleId: 'com.example.testapp',
        captureConsole: true,
      });
    });

    it('should include passthrough args in log capture setup', async () => {
      let capturedParams: unknown = null;
      const logCaptureStub: LogCaptureFunction = async (params) => {
        capturedParams = params;
        return {
          sessionId: 'test-session-456',
          logFilePath: '/tmp/xcodemcp_sim_log_test-session-456.log',
          processes: [],
          error: undefined,
        };
      };

      const mockExecutor = createMockExecutor({ success: true, output: '' });

      await launch_app_logs_simLogic(
        {
          simulatorId: 'test-uuid-123',
          bundleId: 'com.example.testapp',
          args: ['--debug'],
        },
        mockExecutor,
        logCaptureStub,
      );

      expect(capturedParams).toEqual({
        simulatorUuid: 'test-uuid-123',
        bundleId: 'com.example.testapp',
        captureConsole: true,
        args: ['--debug'],
      });
    });

    it('should surface log capture failure', async () => {
      const logCaptureStub: LogCaptureFunction = async () => ({
        sessionId: '',
        logFilePath: '',
        processes: [],
        error: 'Failed to start log capture',
      });

      const mockExecutor = createMockExecutor({ success: true, output: '' });

      const result = await launch_app_logs_simLogic(
        {
          simulatorId: 'test-uuid-123',
          bundleId: 'com.example.testapp',
        },
        mockExecutor,
        logCaptureStub,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'App was launched but log capture failed: Failed to start log capture',
          },
        ],
        isError: true,
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/docs/investigations/issue-163.md:
--------------------------------------------------------------------------------

```markdown
# Investigation: UI automation tools unavailable with Smithery install (issue #163)

## Summary
Smithery installs ship only the compiled entrypoint, while the server hard-requires a bundled `bundled/axe` path derived from `process.argv[1]`. This makes UI automation (and simulator video capture) fail even when system `axe` exists on PATH, and Doctor can report contradictory statuses.

## Symptoms
- UI automation tools (`describe_ui`, `tap`, `swipe`, etc.) fail with "Bundled axe tool not found. UI automation features are not available."
- `doctor` reports system axe present, but UI automation unavailable due to missing bundled binary.
- Smithery cache lacks `bundled/axe` directory; only `index.cjs`, `manifest.json`, `.metadata.json` present.

## Investigation Log

### 2026-01-06 - Initial Assessment
**Hypothesis:** Smithery packaging omits bundled binaries and server does not fallback to system axe.
**Findings:** Issue report indicates bundled path is computed relative to `process.argv[1]` and Smithery cache lacks `bundled/`.
**Evidence:** GitHub issue #163 body (Smithery cache contents; bundled path logic).
**Conclusion:** Needs code and packaging investigation.

### 2026-01-06 - AXe path resolution and bundled-only assumption
**Hypothesis:** AXe resolution is bundled-only, so missing `bundled/axe` disables tools regardless of PATH.
**Findings:** `getAxePath()` computes `bundledAxePath` from `process.argv[1]` and returns it only if it exists; otherwise `null`. No PATH or env override.
**Evidence:** `src/utils/axe-helpers.ts:15-36`
**Conclusion:** Confirmed. Smithery layout lacking `bundled/` will always return null.

### 2026-01-06 - UI automation and video capture gating
**Hypothesis:** UI tools and video capture preflight fail when `getAxePath()` returns null.
**Findings:** UI tools call `getAxePath()` and throw `DependencyError` if absent; `record_sim_video` preflights `areAxeToolsAvailable()` and `isAxeAtLeastVersion()`; `startSimulatorVideoCapture` returns error if `getAxePath()` is null.
**Evidence:** `src/mcp/tools/ui-testing/describe_ui.ts:150-164`, `src/mcp/tools/simulator/record_sim_video.ts:80-88`, `src/utils/video_capture.ts:92-99`
**Conclusion:** Confirmed. Missing bundled binary blocks all UI automation and simulator video capture.

### 2026-01-06 - Doctor output inconsistency
**Hypothesis:** Doctor uses different checks for dependency presence vs feature availability.
**Findings:** Doctor uses `areAxeToolsAvailable()` (bundled-only) for UI automation feature status, while dependency check can succeed via `which axe` when bundled is missing.
**Evidence:** `src/mcp/tools/doctor/doctor.ts:49-68`, `src/mcp/tools/doctor/lib/doctor.deps.ts:100-132`
**Conclusion:** Confirmed. Doctor can report `axe` dependency present but UI automation unsupported.

### 2026-01-06 - Packaging/Smithery artifact mismatch
**Hypothesis:** NPM releases include `bundled/`, Smithery builds do not.
**Findings:** `bundle:axe` creates `bundled/` and npm packaging includes it, but Smithery config has no asset inclusion hints. Release workflow bundles AXe before publish.
**Evidence:** `package.json:21-44`, `.github/workflows/release.yml:48-55`, `smithery.yaml:1-3`, `smithery.config.js:1-6`
**Conclusion:** Confirmed. Smithery build output likely omits bundled artifacts unless explicitly configured.

### 2026-01-06 - Smithery local server deployment flow
**Hypothesis:** Smithery deploys local servers from GitHub pushes and expects build-time packaging to include assets.
**Findings:** README install flow uses Smithery CLI; `smithery.yaml` targets `local`. `bundled/` is gitignored, so it must be produced during Smithery’s deployment build. Current `npm run build` does not run `bundle:axe`.
**Evidence:** `README.md:11-74`, `smithery.yaml:1-3`, `.github/workflows/release.yml:48-62`, `.gitignore:66-68`
**Conclusion:** Confirmed. Smithery deploy must run `bundle:axe` and explicitly include `bundled/` in the produced bundle.

### 2026-01-06 - Smithery config constraints and bundling workaround
**Hypothesis:** Adding esbuild plugins in `smithery.config.js` overrides Smithery’s bootstrap plugin.
**Findings:** Smithery CLI merges config via spread and replaces `plugins`, causing `virtual:bootstrap` resolution to fail when custom plugins are supplied. Side-effect bundling in `smithery.config.js` avoids plugin override and can copy `bundled/` into `.smithery/`.
**Evidence:** `node_modules/@smithery/cli/dist/index.js:~2716600-2717500`, `smithery.config.js:1-47`
**Conclusion:** Confirmed. Bundling must run outside esbuild plugins; Linux builders must skip binary verification.

## Root Cause
Two coupled assumptions break Smithery installs:
1) `getAxePath()` is bundled-only and derives the path from `process.argv[1]`, which points into Smithery’s cache (missing `bundled/axe`), so it always returns null.  
2) Smithery packaging does not include the `bundled/` directory, so the bundled-only resolver can never succeed under Smithery even if AXe is installed system-wide.

## Recommendations
1. Add a robust AXe resolver: allow explicit env override and PATH fallback; keep bundled as preferred but not exclusive.
2. Distinguish bundled vs system AXe in UI tools and video capture; only apply bundled-specific env when the bundled binary is used.
3. Align Doctor output: show both bundled availability and PATH availability, and use that in the UI automation supported status.
4. Update Smithery build to run `bundle:axe` and copy `bundled/` into the Smithery bundle output; skip binary verification on non-mac builders to avoid build failures.

## Preventive Measures
- Add tests for AXe resolution precedence (bundled, env override, PATH) and for Doctor output consistency.
- Document Smithery-specific install requirements and verify `bundled/` presence in Smithery artifacts during CI.

```

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

```typescript
/**
 * Screenshot tool plugin - Capture screenshots from iOS Simulator
 */
import * as path from 'path';
import { tmpdir } from 'os';
import * as z from 'zod';
import { v4 as uuidv4 } from 'uuid';
import { ToolResponse, createImageContent } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { createErrorResponse, SystemError } from '../../../utils/responses/index.ts';
import type { CommandExecutor, FileSystemExecutor } from '../../../utils/execution/index.ts';
import {
  getDefaultFileSystemExecutor,
  getDefaultCommandExecutor,
} from '../../../utils/execution/index.ts';
import {
  createSessionAwareTool,
  getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';

const LOG_PREFIX = '[Screenshot]';

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

// Use z.infer for type safety
type ScreenshotParams = z.infer<typeof screenshotSchema>;

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

export async function screenshotLogic(
  params: ScreenshotParams,
  executor: CommandExecutor,
  fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(),
  pathUtils: { tmpdir: () => string; join: (...paths: string[]) => string } = { ...path, tmpdir },
  uuidUtils: { v4: () => string } = { v4: uuidv4 },
): Promise<ToolResponse> {
  const { simulatorId } = params;
  const tempDir = pathUtils.tmpdir();
  const screenshotFilename = `screenshot_${uuidUtils.v4()}.png`;
  const screenshotPath = pathUtils.join(tempDir, screenshotFilename);
  const optimizedFilename = `screenshot_optimized_${uuidUtils.v4()}.jpg`;
  const optimizedPath = pathUtils.join(tempDir, optimizedFilename);
  // Use xcrun simctl to take screenshot
  const commandArgs: string[] = [
    'xcrun',
    'simctl',
    'io',
    simulatorId,
    'screenshot',
    screenshotPath,
  ];

  log('info', `${LOG_PREFIX}/screenshot: Starting capture to ${screenshotPath} on ${simulatorId}`);

  try {
    // Execute the screenshot command
    const result = await executor(commandArgs, `${LOG_PREFIX}: screenshot`, false);

    if (!result.success) {
      throw new SystemError(`Failed to capture screenshot: ${result.error ?? result.output}`);
    }

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

    try {
      // Optimize the image for LLM consumption: resize to max 800px width and convert to JPEG
      const optimizeArgs = [
        'sips',
        '-Z',
        '800', // Resize to max 800px (maintains aspect ratio)
        '-s',
        'format',
        'jpeg', // Convert to JPEG
        '-s',
        'formatOptions',
        '75', // 75% quality compression
        screenshotPath,
        '--out',
        optimizedPath,
      ];

      const optimizeResult = await executor(optimizeArgs, `${LOG_PREFIX}: optimize image`, false);

      if (!optimizeResult.success) {
        log('warning', `${LOG_PREFIX}/screenshot: Image optimization failed, using original PNG`);
        // Fallback to original PNG if optimization fails
        const base64Image = await fileSystemExecutor.readFile(screenshotPath, 'base64');

        // Clean up
        try {
          await fileSystemExecutor.rm(screenshotPath);
        } catch (err) {
          log('warning', `${LOG_PREFIX}/screenshot: Failed to delete temp file: ${err}`);
        }

        return {
          content: [createImageContent(base64Image, 'image/png')],
          isError: false,
        };
      }

      log('info', `${LOG_PREFIX}/screenshot: Image optimized successfully`);

      // Read the optimized image file as base64
      const base64Image = await fileSystemExecutor.readFile(optimizedPath, 'base64');

      log('info', `${LOG_PREFIX}/screenshot: Successfully encoded image as Base64`);

      // Clean up both temporary files
      try {
        await fileSystemExecutor.rm(screenshotPath);
        await fileSystemExecutor.rm(optimizedPath);
      } catch (err) {
        log('warning', `${LOG_PREFIX}/screenshot: Failed to delete temporary files: ${err}`);
      }

      // Return the optimized image (JPEG format, smaller size)
      return {
        content: [createImageContent(base64Image, 'image/jpeg')],
        isError: false,
      };
    } catch (fileError) {
      log('error', `${LOG_PREFIX}/screenshot: Failed to process image file: ${fileError}`);
      return createErrorResponse(
        `Screenshot captured but failed to process image file: ${fileError instanceof Error ? fileError.message : String(fileError)}`,
      );
    }
  } catch (_error) {
    log('error', `${LOG_PREFIX}/screenshot: Failed - ${_error}`);
    if (_error instanceof SystemError) {
      return createErrorResponse(
        `System error executing screenshot: ${_error.message}`,
        _error.originalError?.stack,
      );
    }
    return createErrorResponse(
      `An unexpected error occurred: ${_error instanceof Error ? _error.message : String(_error)}`,
    );
  }
}

export default {
  name: 'screenshot',
  description:
    "Captures screenshot for visual verification. For UI coordinates, use describe_ui instead (don't determine coordinates from screenshots).",
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: screenshotSchema,
  }),
  annotations: {
    title: 'Screenshot',
    readOnlyHint: true,
  },
  handler: createSessionAwareTool<ScreenshotParams>({
    internalSchema: screenshotSchema as unknown as z.ZodType<ScreenshotParams, unknown>,
    logicFunction: (params: ScreenshotParams, executor: CommandExecutor) => {
      return screenshotLogic(params, executor);
    },
    getExecutor: getDefaultCommandExecutor,
    requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
  }),
};

```

--------------------------------------------------------------------------------
/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Pure dependency injection test for stop_mac_app plugin
 *
 * Tests plugin structure and macOS app stopping functionality including parameter validation,
 * command generation, and response formatting.
 *
 * Uses manual call tracking instead of vitest mocking.
 * NO VITEST MOCKING ALLOWED - Only manual stubs
 */

import { describe, it, expect } from 'vitest';
import * as z from 'zod';

import stopMacApp, { stop_mac_appLogic } from '../stop_mac_app.ts';

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

    it('should have correct description', () => {
      expect(stopMacApp.description).toBe(
        'Stops a running macOS application. Can stop by app name or process ID.',
      );
    });

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

    it('should validate schema correctly', () => {
      // Test optional fields
      expect(stopMacApp.schema.appName.safeParse('Calculator').success).toBe(true);
      expect(stopMacApp.schema.appName.safeParse(undefined).success).toBe(true);
      expect(stopMacApp.schema.processId.safeParse(1234).success).toBe(true);
      expect(stopMacApp.schema.processId.safeParse(undefined).success).toBe(true);

      // Test invalid inputs
      expect(stopMacApp.schema.appName.safeParse(null).success).toBe(false);
      expect(stopMacApp.schema.processId.safeParse('not-number').success).toBe(false);
      expect(stopMacApp.schema.processId.safeParse(null).success).toBe(false);
    });
  });

  describe('Input Validation', () => {
    it('should return exact validation error for missing parameters', async () => {
      const mockExecutor = async () => ({ success: true, output: '', process: {} as any });
      const result = await stop_mac_appLogic({}, mockExecutor);

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Either appName or processId must be provided.',
          },
        ],
        isError: true,
      });
    });
  });

  describe('Command Generation', () => {
    it('should generate correct command for process ID', async () => {
      const calls: any[] = [];
      const mockExecutor = async (command: string[]) => {
        calls.push({ command });
        return { success: true, output: '', process: {} as any };
      };

      await stop_mac_appLogic(
        {
          processId: 1234,
        },
        mockExecutor,
      );

      expect(calls).toHaveLength(1);
      expect(calls[0].command).toEqual(['kill', '1234']);
    });

    it('should generate correct command for app name', async () => {
      const calls: any[] = [];
      const mockExecutor = async (command: string[]) => {
        calls.push({ command });
        return { success: true, output: '', process: {} as any };
      };

      await stop_mac_appLogic(
        {
          appName: 'Calculator',
        },
        mockExecutor,
      );

      expect(calls).toHaveLength(1);
      expect(calls[0].command).toEqual([
        'sh',
        '-c',
        'pkill -f "Calculator" || osascript -e \'tell application "Calculator" to quit\'',
      ]);
    });

    it('should prioritize processId over appName', async () => {
      const calls: any[] = [];
      const mockExecutor = async (command: string[]) => {
        calls.push({ command });
        return { success: true, output: '', process: {} as any };
      };

      await stop_mac_appLogic(
        {
          appName: 'Calculator',
          processId: 1234,
        },
        mockExecutor,
      );

      expect(calls).toHaveLength(1);
      expect(calls[0].command).toEqual(['kill', '1234']);
    });
  });

  describe('Response Processing', () => {
    it('should return exact successful stop response by app name', async () => {
      const mockExecutor = async () => ({ success: true, output: '', process: {} as any });

      const result = await stop_mac_appLogic(
        {
          appName: 'Calculator',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '✅ macOS app stopped successfully: Calculator',
          },
        ],
      });
    });

    it('should return exact successful stop response by process ID', async () => {
      const mockExecutor = async () => ({ success: true, output: '', process: {} as any });

      const result = await stop_mac_appLogic(
        {
          processId: 1234,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '✅ macOS app stopped successfully: PID 1234',
          },
        ],
      });
    });

    it('should return exact successful stop response with both parameters (processId takes precedence)', async () => {
      const mockExecutor = async () => ({ success: true, output: '', process: {} as any });

      const result = await stop_mac_appLogic(
        {
          appName: 'Calculator',
          processId: 1234,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '✅ macOS app stopped successfully: PID 1234',
          },
        ],
      });
    });

    it('should handle execution errors', async () => {
      const mockExecutor = async () => {
        throw new Error('Process not found');
      };

      const result = await stop_mac_appLogic(
        {
          processId: 9999,
        },
        mockExecutor,
      );

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

```

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

```typescript
import { describe, it, expect } from 'vitest';
import { determineSimulatorUuid } from '../simulator-utils.ts';
import { createMockExecutor } from '../../test-utils/mock-executors.ts';

describe('determineSimulatorUuid', () => {
  const mockSimulatorListOutput = JSON.stringify({
    devices: {
      'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
        {
          udid: 'ABC-123-UUID',
          name: 'iPhone 16',
          isAvailable: true,
        },
        {
          udid: 'DEF-456-UUID',
          name: 'iPhone 15',
          isAvailable: false,
        },
      ],
      'com.apple.CoreSimulator.SimRuntime.iOS-16-0': [
        {
          udid: 'GHI-789-UUID',
          name: 'iPhone 14',
          isAvailable: true,
        },
      ],
    },
  });

  describe('UUID provided directly', () => {
    it('should return UUID when simulatorUuid is provided', async () => {
      const mockExecutor = createMockExecutor(
        new Error('Should not call executor when UUID provided'),
      );

      const result = await determineSimulatorUuid(
        { simulatorUuid: 'DIRECT-UUID-123' },
        mockExecutor,
      );

      expect(result.uuid).toBe('DIRECT-UUID-123');
      expect(result.warning).toBeUndefined();
      expect(result.error).toBeUndefined();
    });

    it('should prefer simulatorUuid when both UUID and name are provided', async () => {
      const mockExecutor = createMockExecutor(
        new Error('Should not call executor when UUID provided'),
      );

      const result = await determineSimulatorUuid(
        { simulatorUuid: 'DIRECT-UUID', simulatorName: 'iPhone 16' },
        mockExecutor,
      );

      expect(result.uuid).toBe('DIRECT-UUID');
    });
  });

  describe('Name that looks like UUID', () => {
    it('should detect and use UUID-like name directly', async () => {
      const mockExecutor = createMockExecutor(
        new Error('Should not call executor for UUID-like name'),
      );
      const uuidLikeName = '12345678-1234-1234-1234-123456789abc';

      const result = await determineSimulatorUuid({ simulatorName: uuidLikeName }, mockExecutor);

      expect(result.uuid).toBe(uuidLikeName);
      expect(result.warning).toContain('appears to be a UUID');
      expect(result.error).toBeUndefined();
    });

    it('should detect uppercase UUID-like name', async () => {
      const mockExecutor = createMockExecutor(
        new Error('Should not call executor for UUID-like name'),
      );
      const uuidLikeName = '12345678-1234-1234-1234-123456789ABC';

      const result = await determineSimulatorUuid({ simulatorName: uuidLikeName }, mockExecutor);

      expect(result.uuid).toBe(uuidLikeName);
      expect(result.warning).toContain('appears to be a UUID');
    });
  });

  describe('Name resolution via simctl', () => {
    it('should resolve name to UUID for available simulator', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: mockSimulatorListOutput,
      });

      const result = await determineSimulatorUuid({ simulatorName: 'iPhone 16' }, mockExecutor);

      expect(result.uuid).toBe('ABC-123-UUID');
      expect(result.warning).toBeUndefined();
      expect(result.error).toBeUndefined();
    });

    it('should find simulator across different runtimes', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: mockSimulatorListOutput,
      });

      const result = await determineSimulatorUuid({ simulatorName: 'iPhone 14' }, mockExecutor);

      expect(result.uuid).toBe('GHI-789-UUID');
      expect(result.error).toBeUndefined();
    });

    it('should error for unavailable simulator', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: mockSimulatorListOutput,
      });

      const result = await determineSimulatorUuid({ simulatorName: 'iPhone 15' }, mockExecutor);

      expect(result.uuid).toBeUndefined();
      expect(result.error).toBeDefined();
      expect(result.error?.content[0].text).toContain('exists but is not available');
    });

    it('should error for non-existent simulator', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: mockSimulatorListOutput,
      });

      const result = await determineSimulatorUuid({ simulatorName: 'iPhone 99' }, mockExecutor);

      expect(result.uuid).toBeUndefined();
      expect(result.error).toBeDefined();
      expect(result.error?.content[0].text).toContain('not found');
    });

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

      const result = await determineSimulatorUuid({ simulatorName: 'iPhone 16' }, mockExecutor);

      expect(result.uuid).toBeUndefined();
      expect(result.error).toBeDefined();
      expect(result.error?.content[0].text).toContain('Failed to list simulators');
    });

    it('should handle invalid JSON from simctl', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'invalid json {',
      });

      const result = await determineSimulatorUuid({ simulatorName: 'iPhone 16' }, mockExecutor);

      expect(result.uuid).toBeUndefined();
      expect(result.error).toBeDefined();
      expect(result.error?.content[0].text).toContain('Failed to parse simulator list');
    });
  });

  describe('No identifier provided', () => {
    it('should error when neither UUID nor name is provided', async () => {
      const mockExecutor = createMockExecutor(
        new Error('Should not call executor when no identifier'),
      );

      const result = await determineSimulatorUuid({}, mockExecutor);

      expect(result.uuid).toBeUndefined();
      expect(result.error).toBeDefined();
      expect(result.error?.content[0].text).toContain('No simulator identifier provided');
    });
  });
});

```

--------------------------------------------------------------------------------
/scripts/bundle-axe.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash

# Build script for AXe artifacts
# This script downloads pre-built AXe artifacts from GitHub releases and bundles them

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
BUNDLED_DIR="$PROJECT_ROOT/bundled"
AXE_LOCAL_DIR="/Volumes/Developer/AXe"
AXE_TEMP_DIR="/tmp/axe-download-$$"

echo "🔨 Preparing AXe artifacts for bundling..."

# Single source of truth for AXe version (overridable)
# 1) Use $AXE_VERSION if provided in env
# 2) Else, use repo-level pin from .axe-version if present
# 3) Else, fall back to default below
DEFAULT_AXE_VERSION="1.1.1"
VERSION_FILE="$PROJECT_ROOT/.axe-version"
if [ -n "${AXE_VERSION}" ]; then
    PINNED_AXE_VERSION="${AXE_VERSION}"
elif [ -f "$VERSION_FILE" ]; then
    PINNED_AXE_VERSION="$(cat "$VERSION_FILE" | tr -d ' \n\r')"
else
    PINNED_AXE_VERSION="$DEFAULT_AXE_VERSION"
fi
echo "📌 Using AXe version: $PINNED_AXE_VERSION"

# Clean up any existing bundled directory
if [ -d "$BUNDLED_DIR" ]; then
    echo "🧹 Cleaning existing bundled directory..."
    rm -rf "$BUNDLED_DIR"
fi

# Create bundled directory
mkdir -p "$BUNDLED_DIR"

# Use local AXe build if available (unless AXE_FORCE_REMOTE=1), otherwise download from GitHub releases
if [ -z "${AXE_FORCE_REMOTE}" ] && [ -d "$AXE_LOCAL_DIR" ] && [ -f "$AXE_LOCAL_DIR/Package.swift" ]; then
    echo "🏠 Using local AXe source at $AXE_LOCAL_DIR"
    cd "$AXE_LOCAL_DIR"

    # Build AXe in release configuration
    echo "🔨 Building AXe in release configuration..."
    swift build --configuration release

    # Check if build succeeded
    if [ ! -f ".build/release/axe" ]; then
        echo "❌ AXe build failed - binary not found"
        exit 1
    fi

    echo "✅ AXe build completed successfully"

    # Copy binary to bundled directory
    echo "📦 Copying AXe binary..."
    cp ".build/release/axe" "$BUNDLED_DIR/"

    # Fix rpath to find frameworks in Frameworks/ subdirectory
    echo "🔧 Configuring AXe binary rpath for bundled frameworks..."
    install_name_tool -add_rpath "@executable_path/Frameworks" "$BUNDLED_DIR/axe"

    # Create Frameworks directory and copy frameworks
    echo "📦 Copying frameworks..."
    mkdir -p "$BUNDLED_DIR/Frameworks"

    # Copy frameworks with better error handling
    for framework in .build/release/*.framework; do
        if [ -d "$framework" ]; then
            echo "📦 Copying framework: $(basename "$framework")"
            cp -r "$framework" "$BUNDLED_DIR/Frameworks/"

            # Only copy nested frameworks if they exist
            if [ -d "$framework/Frameworks" ]; then
                echo "📦 Found nested frameworks in $(basename "$framework")"
                cp -r "$framework/Frameworks"/* "$BUNDLED_DIR/Frameworks/" 2>/dev/null || true
            fi
        fi
    done
else
    echo "📥 Downloading latest AXe release from GitHub..."

    # Construct release download URL from pinned version
    AXE_RELEASE_URL="https://github.com/cameroncooke/AXe/releases/download/v${PINNED_AXE_VERSION}/AXe-macOS-v${PINNED_AXE_VERSION}.tar.gz"

    # Create temp directory
    mkdir -p "$AXE_TEMP_DIR"
    cd "$AXE_TEMP_DIR"

    # Download and extract the release
    echo "📥 Downloading AXe release archive ($AXE_RELEASE_URL)..."
    curl -L -o "axe-release.tar.gz" "$AXE_RELEASE_URL"

    echo "📦 Extracting AXe release archive..."
    tar -xzf "axe-release.tar.gz"

    # Find the extracted directory (might be named differently)
    EXTRACTED_DIR=$(find . -type d \( -name "*AXe*" -o -name "*axe*" \) | head -1)
    if [ -z "$EXTRACTED_DIR" ]; then
        # If no AXe directory found, assume files are in current directory
        EXTRACTED_DIR="."
    fi

    cd "$EXTRACTED_DIR"

    # Copy binary
    if [ -f "axe" ]; then
        echo "📦 Copying AXe binary..."
        cp "axe" "$BUNDLED_DIR/"
        chmod +x "$BUNDLED_DIR/axe"
    elif [ -f "bin/axe" ]; then
        echo "📦 Copying AXe binary from bin/..."
        cp "bin/axe" "$BUNDLED_DIR/"
        chmod +x "$BUNDLED_DIR/axe"
    else
        echo "❌ AXe binary not found in release archive"
        ls -la
        exit 1
    fi

    # Copy frameworks if they exist
    echo "📦 Copying frameworks..."
    mkdir -p "$BUNDLED_DIR/Frameworks"

    if [ -d "Frameworks" ]; then
        cp -r Frameworks/* "$BUNDLED_DIR/Frameworks/"
    elif [ -d "lib" ]; then
        # Look for frameworks in lib directory
        find lib -name "*.framework" -exec cp -r {} "$BUNDLED_DIR/Frameworks/" \;
    else
        echo "⚠️  No frameworks directory found in release archive"
        echo "📂 Contents of release archive:"
        find . -type f -name "*.framework" -o -name "*.dylib" | head -10
    fi
fi

# Verify frameworks were copied
FRAMEWORK_COUNT=$(find "$BUNDLED_DIR/Frameworks" -name "*.framework" | wc -l)
echo "📦 Copied $FRAMEWORK_COUNT frameworks"

# List the frameworks for verification
echo "🔍 Bundled frameworks:"
ls -la "$BUNDLED_DIR/Frameworks/"

# Verify binary can run with bundled frameworks (macOS only)
OS_NAME="$(uname -s)"
if [ "$OS_NAME" = "Darwin" ]; then
    echo "🧪 Testing bundled AXe binary..."
    if DYLD_FRAMEWORK_PATH="$BUNDLED_DIR/Frameworks" "$BUNDLED_DIR/axe" --version > /dev/null 2>&1; then
        echo "✅ Bundled AXe binary test passed"
    else
        echo "❌ Bundled AXe binary test failed"
        exit 1
    fi

    # Get AXe version for logging
    AXE_VERSION=$(DYLD_FRAMEWORK_PATH="$BUNDLED_DIR/Frameworks" "$BUNDLED_DIR/axe" --version 2>/dev/null || echo "unknown")
else
    echo "⚠️  Skipping AXe binary verification on non-macOS (detected $OS_NAME)"
    AXE_VERSION="unknown (verification skipped)"
fi
echo "📋 AXe version: $AXE_VERSION"

# Clean up temp directory if it was used
if [ -d "$AXE_TEMP_DIR" ]; then
    echo "🧹 Cleaning up temporary files..."
    rm -rf "$AXE_TEMP_DIR"
fi

# Show final bundle size
BUNDLE_SIZE=$(du -sh "$BUNDLED_DIR" | cut -f1)
echo "📊 Final bundle size: $BUNDLE_SIZE"

echo "🎉 AXe bundling completed successfully!"
echo "📁 Bundled artifacts location: $BUNDLED_DIR"

```

--------------------------------------------------------------------------------
/docs/dev/RELEASE_PROCESS.md:
--------------------------------------------------------------------------------

```markdown
# Release Process

## Step-by-Step Development Workflow

### 1. Starting New Work

**Always start by syncing with main:**
```bash
git checkout main
git pull origin main
```

**Create feature branch using standardized naming convention:**
```bash
git checkout -b feature/issue-123-add-new-feature
git checkout -b bugfix/issue-456-fix-simulator-crash
```

### 2. Development & Commits

**Before committing, ALWAYS run quality checks:**
```bash
npm run build      # Ensure code compiles
npm run typecheck  # MANDATORY: Fix all TypeScript errors
npm run lint       # Fix linting issues
npm run test       # Ensure tests pass
```

**🚨 CRITICAL: TypeScript errors are BLOCKING:**
- **ZERO tolerance** for TypeScript errors in commits
- The `npm run typecheck` command must pass with no errors
- Fix all `ts(XXXX)` errors before committing
- Do not ignore or suppress TypeScript errors without explicit approval

**Make logical, atomic commits:**
- Each commit should represent a single logical change  
- Write short, descriptive commit summaries
- Commit frequently to your feature branch

```bash
# Always run quality checks first
npm run typecheck && npm run lint && npm run test

# Then commit your changes
git add .
git commit -m "feat: add simulator boot validation logic"
git commit -m "fix: handle null response in device list parser"
```

### 3. Pushing Changes

**🚨 CRITICAL: Always ask permission before pushing**
- **NEVER push without explicit user permission**
- **NEVER force push without explicit permission**
- Pushing without permission is a fatal error resulting in termination

```bash
# Only after getting permission:
git push origin feature/your-branch-name
```

### 4. Pull Request Creation

**Use GitHub CLI tool exclusively:**
```bash
gh pr create --title "feat: add simulator boot validation" --body "$(cat <<'EOF'
## Summary
Brief description of what this PR does and why.

## Background/Details
### For New Features:
- Detailed explanation of the new feature
- Context and requirements that led to this implementation
- Design decisions and approach taken

### For Bug Fixes:
- **Root Cause Analysis**: Detailed explanation of what caused the bug
- Specific conditions that trigger the issue
- Why the current code fails in these scenarios

## Solution
- How the root cause was addressed
- Technical approach and implementation details
- Key changes made to resolve the issue

## Testing
- **Reproduction Steps**: How to reproduce the original issue (for bugs)
- **Validation Method**: How you verified the fix works
- **Test Coverage**: What tests were added or modified
- **Manual Testing**: Steps taken to validate the solution
- **Edge Cases**: Additional scenarios tested

## Notes
- Any important considerations for reviewers
- Potential impacts or side effects
- Future improvements or technical debt
- Deployment considerations
EOF
)"
```

**After PR creation, add automated review trigger:**
```bash
gh pr comment --body "Cursor review"
```

### 5. Branch Management & Rebasing

**Keep branch up to date with main:**
```bash
git checkout main
git pull origin main
git checkout your-feature-branch
git rebase main
```

**If rebase creates conflicts:**
- Resolve conflicts manually
- `git add .` resolved files
- `git rebase --continue`
- **Ask permission before force pushing rebased branch**

### 6. Merge Process

**Only merge via Pull Requests:**
- No direct merges to `main`
- Maintain linear commit history through rebasing
- Use "Squash and merge" or "Rebase and merge" as appropriate
- Delete feature branch after successful merge

## Pull Request Template Structure

Every PR must include these sections in order:

1. **Summary**: Brief overview of changes and purpose
2. **Background/Details**: 
   - New Feature: Requirements, context, design decisions
   - Bug Fix: Detailed root cause analysis
3. **Solution**: Technical approach and implementation details  
4. **Testing**: Reproduction steps, validation methods, test coverage
5. **Notes**: Additional considerations, impacts, future work

## Critical Rules

### ❌ FATAL ERRORS (Result in Termination)
- **NEVER push to `main` directly**
- **NEVER push without explicit user permission**
- **NEVER force push without explicit permission**
- **NEVER commit code with TypeScript errors**

### ✅ Required Practices
- Always pull from `main` before creating branches
- **MANDATORY: Run `npm run typecheck` before every commit**
- **MANDATORY: Fix all TypeScript errors before committing**
- Use `gh` CLI tool for all PR operations
- Add "Cursor review" comment after PR creation
- Maintain linear commit history via rebasing
- Ask permission before any push operation
- Use standardized branch naming conventions

## Branch Naming Conventions

- `feature/issue-xxx-description` - New features
- `bugfix/issue-xxx-description` - Bug fixes  
- `hotfix/critical-issue-description` - Critical production fixes
- `docs/update-readme` - Documentation updates
- `refactor/improve-error-handling` - Code refactoring

## Automated Quality Gates

### CI/CD Pipeline
Our GitHub Actions CI pipeline automatically enforces these quality checks:
1. `npm run build` - Compilation check
2. `npm run lint` - ESLint validation  
3. `npm run format:check` - Prettier formatting check
4. `npm run typecheck` - **TypeScript error validation**
5. `npm run test` - Test suite execution

**All checks must pass before PR merge is allowed.**

### Optional: Pre-commit Hook Setup
To catch TypeScript errors before committing locally:

```bash
# Create pre-commit hook
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
echo "🔍 Running pre-commit checks..."

# Run TypeScript type checking
echo "📝 Checking TypeScript..."
npm run typecheck
if [ $? -ne 0 ]; then
  echo "❌ TypeScript errors found. Please fix before committing."
  exit 1
fi

# Run linting
echo "🧹 Running linter..."
npm run lint
if [ $? -ne 0 ]; then
  echo "❌ Linting errors found. Please fix before committing."
  exit 1
fi

echo "✅ Pre-commit checks passed!"
EOF

# Make it executable  
chmod +x .git/hooks/pre-commit
```

This hook will automatically run `typecheck` and `lint` before every commit, preventing TypeScript errors from being committed.
```

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

```typescript
/**
 * UI Testing Plugin: Type Text
 *
 * Types text into the iOS Simulator using keyboard input.
 * Supports standard US keyboard characters.
 */

import * as z from 'zod';
import { ToolResponse } from '../../../types/common.ts';
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 {
  createSessionAwareTool,
  getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';

const LOG_PREFIX = '[AXe]';

// Define schema as ZodObject
const typeTextSchema = z.object({
  simulatorId: z.uuid({ message: 'Invalid Simulator UUID format' }),
  text: z.string().min(1, { message: 'Text cannot be empty' }),
});

// Use z.infer for type safety
type TypeTextParams = z.infer<typeof typeTextSchema>;

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

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

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

  // Params are already validated by the factory, use directly
  const { simulatorId, text } = params;
  const guard = await guardUiAutomationAgainstStoppedDebugger({
    debugger: debuggerManager,
    simulatorId,
    toolName,
  });
  if (guard.blockedResponse) return guard.blockedResponse;

  const commandArgs = ['type', text];

  log(
    'info',
    `${LOG_PREFIX}/${toolName}: Starting type "${text.substring(0, 20)}..." on ${simulatorId}`,
  );

  try {
    await executeAxeCommand(commandArgs, simulatorId, 'type', executor, axeHelpers);
    log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`);
    const message = 'Text typing simulated successfully.';
    if (guard.warningText) {
      return createTextResponse(`${message}\n\n${guard.warningText}`);
    }
    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 simulate text typing: ${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: 'type_text',
  description:
    'Type text (supports US keyboard characters). Use describe_ui to find text field, tap to focus, then type.',
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: typeTextSchema,
  }),
  annotations: {
    title: 'Type Text',
    destructiveHint: true,
  },
  handler: createSessionAwareTool<TypeTextParams>({
    internalSchema: typeTextSchema as unknown as z.ZodType<TypeTextParams, unknown>,
    logicFunction: (params: TypeTextParams, executor: CommandExecutor) =>
      type_textLogic(params, executor),
    getExecutor: getDefaultCommandExecutor,
    requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
  }), // Safe factory
};

// 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 provided helpers or defaults
  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/mcp/resources/__tests__/simulators.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import * as z from 'zod';

import simulatorsResource, { simulatorsResourceLogic } from '../simulators.ts';
import {
  createMockCommandResponse,
  createMockExecutor,
} from '../../../test-utils/mock-executors.ts';

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

    it('should export correct description', () => {
      expect(simulatorsResource.description).toBe(
        'Available iOS simulators with their UUIDs and states',
      );
    });

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

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

  describe('Handler Functionality', () => {
    it('should handle successful simulator data retrieval', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: JSON.stringify({
          devices: {
            'iOS 17.0': [
              {
                name: 'iPhone 15 Pro',
                udid: 'ABC123-DEF456-GHI789',
                state: 'Shutdown',
                isAvailable: true,
              },
            ],
          },
        }),
      });

      const result = await simulatorsResourceLogic(mockExecutor);

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0].text).toContain('Available iOS Simulators:');
      expect(result.contents[0].text).toContain('iPhone 15 Pro');
      expect(result.contents[0].text).toContain('ABC123-DEF456-GHI789');
    });

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

      const result = await simulatorsResourceLogic(mockExecutor);

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0].text).toContain('Failed to list simulators');
      expect(result.contents[0].text).toContain('Command failed');
    });

    it('should handle JSON parsing errors and fall back to text parsing', async () => {
      const mockTextOutput = `== Devices ==
-- iOS 17.0 --
    iPhone 15 (test-uuid-123) (Shutdown)`;

      const mockExecutor = async (command: string[]) => {
        // JSON command returns invalid JSON
        if (command.includes('--json')) {
          return createMockCommandResponse({
            success: true,
            output: 'invalid json',
            error: undefined,
          });
        }

        // Text command returns valid text output
        return createMockCommandResponse({
          success: true,
          output: mockTextOutput,
          error: undefined,
        });
      };

      const result = await simulatorsResourceLogic(mockExecutor);

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0].text).toContain('iPhone 15 (test-uuid-123)');
      expect(result.contents[0].text).toContain('iOS 17.0');
    });

    it('should handle spawn errors', async () => {
      const mockExecutor = createMockExecutor(new Error('spawn xcrun ENOENT'));

      const result = await simulatorsResourceLogic(mockExecutor);

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0].text).toContain('Failed to list simulators');
      expect(result.contents[0].text).toContain('spawn xcrun ENOENT');
    });

    it('should handle empty simulator data', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: JSON.stringify({ devices: {} }),
      });

      const result = await simulatorsResourceLogic(mockExecutor);

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0].text).toContain('Available iOS Simulators:');
    });

    it('should handle booted simulators correctly', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: JSON.stringify({
          devices: {
            'iOS 17.0': [
              {
                name: 'iPhone 15 Pro',
                udid: 'ABC123-DEF456-GHI789',
                state: 'Booted',
                isAvailable: true,
              },
            ],
          },
        }),
      });

      const result = await simulatorsResourceLogic(mockExecutor);

      expect(result.contents[0].text).toContain('[Booted]');
    });

    it('should filter out unavailable simulators', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: JSON.stringify({
          devices: {
            'iOS 17.0': [
              {
                name: 'iPhone 15 Pro',
                udid: 'ABC123-DEF456-GHI789',
                state: 'Shutdown',
                isAvailable: true,
              },
              {
                name: 'iPhone 14',
                udid: 'XYZ789-UVW456-RST123',
                state: 'Shutdown',
                isAvailable: false,
              },
            ],
          },
        }),
      });

      const result = await simulatorsResourceLogic(mockExecutor);

      expect(result.contents[0].text).toContain('iPhone 15 Pro');
      expect(result.contents[0].text).not.toContain('iPhone 14');
    });

    it('should include next steps guidance', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: JSON.stringify({
          devices: {
            'iOS 17.0': [
              {
                name: 'iPhone 15 Pro',
                udid: 'ABC123-DEF456-GHI789',
                state: 'Shutdown',
                isAvailable: true,
              },
            ],
          },
        }),
      });

      const result = await simulatorsResourceLogic(mockExecutor);

      expect(result.contents[0].text).toContain('Next Steps:');
      expect(result.contents[0].text).toContain('boot_sim');
      expect(result.contents[0].text).toContain('open_sim');
      expect(result.contents[0].text).toContain('build_sim');
      expect(result.contents[0].text).toContain('get_sim_app_path');
    });
  });
});

```

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

```typescript
/**
 * Simulator Build Plugin: Build Simulator (Unified)
 *
 * Builds an app from a project or workspace for a specific simulator by UUID or name.
 * Accepts mutually exclusive `projectPath` or `workspacePath`.
 * Accepts mutually exclusive `simulatorId` or `simulatorName`.
 */

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

// Unified schema: XOR between projectPath and workspacePath, and XOR between simulatorId and simulatorName
const baseOptions = {
  scheme: z.string().describe('The scheme to use (Required)'),
  simulatorId: z
    .string()
    .optional()
    .describe(
      'UUID of the simulator (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",
    ),
  configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
  derivedDataPath: z
    .string()
    .optional()
    .describe('Path where build products and other derived data will go'),
  extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'),
  useLatestOS: z
    .boolean()
    .optional()
    .describe('Whether to use the latest OS version for the named simulator'),
  preferXcodebuild: z
    .boolean()
    .optional()
    .describe(
      'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.',
    ),
};

const baseSchemaObject = z.object({
  projectPath: z
    .string()
    .optional()
    .describe('Path to .xcodeproj file. Provide EITHER this OR workspacePath, not both'),
  workspacePath: z
    .string()
    .optional()
    .describe('Path to .xcworkspace file. Provide EITHER this OR projectPath, not both'),
  ...baseOptions,
});

const buildSimulatorSchema = 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.',
    })
    .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 BuildSimulatorParams = z.infer<typeof buildSimulatorSchema>;

// Internal logic for building Simulator apps.
async function _handleSimulatorBuildLogic(
  params: BuildSimulatorParams,
  executor: CommandExecutor = getDefaultCommandExecutor(),
): Promise<ToolResponse> {
  const projectType = params.projectPath ? 'project' : 'workspace';
  const filePath = params.projectPath ?? params.workspacePath;

  // Log warning if useLatestOS is provided with simulatorId
  if (params.simulatorId && params.useLatestOS !== undefined) {
    log(
      'warning',
      `useLatestOS parameter is ignored when using simulatorId (UUID implies exact device/OS)`,
    );
  }

  log(
    'info',
    `Starting iOS Simulator build for scheme ${params.scheme} from ${projectType}: ${filePath}`,
  );

  // Ensure configuration has a default value for SharedBuildParams compatibility
  const sharedBuildParams = {
    ...params,
    configuration: params.configuration ?? 'Debug',
  };

  // executeXcodeBuildCommand handles both simulatorId and simulatorName
  return executeXcodeBuildCommand(
    sharedBuildParams,
    {
      platform: XcodePlatform.iOSSimulator,
      simulatorName: params.simulatorName,
      simulatorId: params.simulatorId,
      useLatestOS: params.simulatorId ? false : params.useLatestOS, // Ignore useLatestOS with ID
      logPrefix: 'iOS Simulator Build',
    },
    params.preferXcodebuild ?? false,
    'build',
    executor,
  );
}

export async function build_simLogic(
  params: BuildSimulatorParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  // Provide defaults
  const processedParams: BuildSimulatorParams = {
    ...params,
    configuration: params.configuration ?? 'Debug',
    useLatestOS: params.useLatestOS ?? true, // May be ignored if simulatorId is provided
    preferXcodebuild: params.preferXcodebuild ?? false,
  };

  return _handleSimulatorBuildLogic(processedParams, executor);
}

// Public schema = internal minus session-managed fields
const publicSchemaObject = baseSchemaObject.omit({
  projectPath: true,
  workspacePath: true,
  scheme: true,
  configuration: true,
  simulatorId: true,
  simulatorName: true,
  useLatestOS: true,
} as const);

export default {
  name: 'build_sim',
  description: 'Builds an app for an iOS simulator.',
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: baseSchemaObject,
  }), // MCP SDK compatibility (public inputs only)
  annotations: {
    title: 'Build Simulator',
    destructiveHint: true,
  },
  handler: createSessionAwareTool<BuildSimulatorParams>({
    internalSchema: buildSimulatorSchema as unknown as z.ZodType<BuildSimulatorParams, unknown>,
    logicFunction: build_simLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { allOf: ['scheme'], message: 'scheme is required' },
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
      { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
    ],
    exclusivePairs: [
      ['projectPath', 'workspacePath'],
      ['simulatorId', 'simulatorName'],
    ],
  }),
};

```

--------------------------------------------------------------------------------
/src/mcp/tools/utilities/clean.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Utilities Plugin: Clean (Unified)
 *
 * Cleans build products for either a project or workspace using xcodebuild.
 * Accepts mutually exclusive `projectPath` or `workspacePath`.
 */

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

// Unified schema: XOR between projectPath and workspacePath, sharing common options
const baseOptions = {
  scheme: z.string().optional().describe('Optional: The scheme to clean'),
  configuration: z
    .string()
    .optional()
    .describe('Optional: Build configuration to clean (Debug, Release, etc.)'),
  derivedDataPath: z
    .string()
    .optional()
    .describe('Optional: Path where derived data might be located'),
  extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'),
  preferXcodebuild: z
    .boolean()
    .optional()
    .describe(
      'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.',
    ),
  platform: z
    .enum([
      'macOS',
      'iOS',
      'iOS Simulator',
      'watchOS',
      'watchOS Simulator',
      'tvOS',
      'tvOS Simulator',
      'visionOS',
      'visionOS Simulator',
    ])
    .optional()
    .describe(
      'Optional: Platform to clean for (defaults to iOS). Choose from macOS, iOS, iOS Simulator, watchOS, watchOS Simulator, tvOS, tvOS Simulator, visionOS, visionOS Simulator',
    ),
};

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 cleanSchema = 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.',
    })
    .refine((val) => !(val.workspacePath && !val.scheme), {
      message: 'scheme is required when workspacePath is provided.',
      path: ['scheme'],
    }),
);

export type CleanParams = z.infer<typeof cleanSchema>;

export async function cleanLogic(
  params: CleanParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  // Extra safety: ensure workspace path has a scheme (xcodebuild requires it)
  if (params.workspacePath && !params.scheme) {
    return createErrorResponse(
      'Parameter validation failed',
      'Invalid parameters:\nscheme: scheme is required when workspacePath is provided.',
    );
  }

  // Use provided platform or default to iOS
  const targetPlatform = params.platform ?? 'iOS';

  // Map human-friendly platform names to XcodePlatform enum values
  // This is safer than direct key lookup and handles the space-containing simulator names
  const platformMap = {
    macOS: XcodePlatform.macOS,
    iOS: XcodePlatform.iOS,
    'iOS Simulator': XcodePlatform.iOSSimulator,
    watchOS: XcodePlatform.watchOS,
    'watchOS Simulator': XcodePlatform.watchOSSimulator,
    tvOS: XcodePlatform.tvOS,
    'tvOS Simulator': XcodePlatform.tvOSSimulator,
    visionOS: XcodePlatform.visionOS,
    'visionOS Simulator': XcodePlatform.visionOSSimulator,
  };

  const platformEnum = platformMap[targetPlatform];
  if (!platformEnum) {
    return createErrorResponse(
      'Parameter validation failed',
      `Invalid parameters:\nplatform: unsupported value "${targetPlatform}".`,
    );
  }

  const hasProjectPath = typeof params.projectPath === 'string';
  const typedParams: SharedBuildParams = {
    ...(hasProjectPath
      ? { projectPath: params.projectPath as string }
      : { workspacePath: params.workspacePath as string }),
    // scheme may be omitted for project; when omitted we do not pass -scheme
    // Provide empty string to satisfy type, executeXcodeBuildCommand only emits -scheme when non-empty
    scheme: params.scheme ?? '',
    configuration: params.configuration ?? 'Debug',
    derivedDataPath: params.derivedDataPath,
    extraArgs: params.extraArgs,
  };

  // For clean operations, simulator platforms should be mapped to their device equivalents
  // since clean works at the build product level, not runtime level, and build products
  // are shared between device and simulator platforms
  const cleanPlatformMap: Partial<Record<XcodePlatform, XcodePlatform>> = {
    [XcodePlatform.iOSSimulator]: XcodePlatform.iOS,
    [XcodePlatform.watchOSSimulator]: XcodePlatform.watchOS,
    [XcodePlatform.tvOSSimulator]: XcodePlatform.tvOS,
    [XcodePlatform.visionOSSimulator]: XcodePlatform.visionOS,
  };

  const cleanPlatform = cleanPlatformMap[platformEnum] ?? platformEnum;

  return executeXcodeBuildCommand(
    typedParams,
    {
      platform: cleanPlatform,
      logPrefix: 'Clean',
    },
    false,
    'clean',
    executor,
  );
}

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

export default {
  name: 'clean',
  description: 'Cleans build products with xcodebuild.',
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: baseSchemaObject,
  }),
  annotations: {
    title: 'Clean',
    destructiveHint: true,
  },
  handler: createSessionAwareTool<CleanParams>({
    internalSchema: cleanSchema as unknown as z.ZodType<CleanParams, unknown>,
    logicFunction: cleanLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
    ],
    exclusivePairs: [['projectPath', 'workspacePath']],
  }),
};

```

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

```typescript
import * as z from 'zod';
import type { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { createTextResponse, createErrorResponse } 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-helpers.ts';
import { DependencyError, AxeError, SystemError } from '../../../utils/errors.ts';
import {
  createSessionAwareTool,
  getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';

// Define schema as ZodObject
const buttonSchema = z.object({
  simulatorId: z.uuid({ message: 'Invalid Simulator UUID format' }),
  buttonType: z.enum(['apple-pay', 'home', 'lock', 'side-button', 'siri']),
  duration: z.number().min(0, { message: 'Duration must be non-negative' }).optional(),
});

// Use z.infer for type safety
type ButtonParams = z.infer<typeof buttonSchema>;

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

const LOG_PREFIX = '[AXe]';

export async function buttonLogic(
  params: ButtonParams,
  executor: CommandExecutor,
  axeHelpers: AxeHelpers = {
    getAxePath,
    getBundledAxeEnvironment,
    createAxeNotAvailableResponse,
  },
  debuggerManager: DebuggerManager = getDefaultDebuggerManager(),
): Promise<ToolResponse> {
  const toolName = 'button';
  const { simulatorId, buttonType, duration } = params;

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

  const commandArgs = ['button', buttonType];
  if (duration !== undefined) {
    commandArgs.push('--duration', String(duration));
  }

  log('info', `${LOG_PREFIX}/${toolName}: Starting ${buttonType} button press on ${simulatorId}`);

  try {
    await executeAxeCommand(commandArgs, simulatorId, 'button', executor, axeHelpers);
    log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`);
    const message = `Hardware button '${buttonType}' pressed 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 press button '${buttonType}': ${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(buttonSchema.omit({ simulatorId: true } as const).shape);

export default {
  name: 'button',
  description:
    'Press hardware button on iOS simulator. Supported buttons: apple-pay, home, lock, side-button, siri',
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: buttonSchema,
  }),
  annotations: {
    title: 'Hardware Button',
    destructiveHint: true,
  },
  handler: createSessionAwareTool<ButtonParams>({
    internalSchema: buttonSchema as unknown as z.ZodType<ButtonParams, unknown>,
    logicFunction: (params: ButtonParams, executor: CommandExecutor) =>
      buttonLogic(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/mcp/tools/ui-testing/key_press.ts:
--------------------------------------------------------------------------------

```typescript
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 keyPressSchema = z.object({
  simulatorId: z.uuid({ message: 'Invalid Simulator UUID format' }),
  keyCode: z.number().int({ message: 'HID keycode to press (0-255)' }).min(0).max(255),
  duration: z.number().min(0, { message: 'Duration must be non-negative' }).optional(),
});

// Use z.infer for type safety
type KeyPressParams = z.infer<typeof keyPressSchema>;

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

const LOG_PREFIX = '[AXe]';

export async function key_pressLogic(
  params: KeyPressParams,
  executor: CommandExecutor,
  axeHelpers: AxeHelpers = {
    getAxePath,
    getBundledAxeEnvironment,
    createAxeNotAvailableResponse,
  },
  debuggerManager: DebuggerManager = getDefaultDebuggerManager(),
): Promise<ToolResponse> {
  const toolName = 'key_press';
  const { simulatorId, keyCode, duration } = params;

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

  const commandArgs = ['key', String(keyCode)];
  if (duration !== undefined) {
    commandArgs.push('--duration', String(duration));
  }

  log('info', `${LOG_PREFIX}/${toolName}: Starting key press ${keyCode} on ${simulatorId}`);

  try {
    await executeAxeCommand(commandArgs, simulatorId, 'key', executor, axeHelpers);
    log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`);
    const message = `Key press (code: ${keyCode}) simulated 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 simulate key press (code: ${keyCode}): ${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(
  keyPressSchema.omit({ simulatorId: true } as const).shape,
);

export default {
  name: 'key_press',
  description:
    'Press a single key by keycode on the simulator. Common keycodes: 40=Return, 42=Backspace, 43=Tab, 44=Space, 58-67=F1-F10.',
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: keyPressSchema,
  }),
  annotations: {
    title: 'Key Press',
    destructiveHint: true,
  },
  handler: createSessionAwareTool<KeyPressParams>({
    internalSchema: keyPressSchema as unknown as z.ZodType<KeyPressParams, unknown>,
    logicFunction: (params: KeyPressParams, executor: CommandExecutor) =>
      key_pressLogic(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)}`);
  }
}

```
Page 3/12FirstPrevNextLast