#
tokens: 48876/50000 25/337 files (page 4/11)
lines: off (toggle) GitHub
raw markdown copy
This is page 4 of 11. Use http://codebase.md/cameroncooke/xcodebuildmcp?page={x} to view the full context.

# Directory Structure

```
├── .axe-version
├── .claude
│   └── agents
│       └── xcodebuild-mcp-qa-tester.md
├── .cursor
│   ├── BUGBOT.md
│   └── environment.json
├── .cursorrules
├── .github
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   └── feature_request.yml
│   └── workflows
│       ├── ci.yml
│       ├── claude-code-review.yml
│       ├── claude-dispatch.yml
│       ├── claude.yml
│       ├── droid-code-review.yml
│       ├── README.md
│       ├── release.yml
│       └── sentry.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
├── Dockerfile
├── docs
│   ├── ARCHITECTURE.md
│   ├── CODE_QUALITY.md
│   ├── CONTRIBUTING.md
│   ├── ESLINT_TYPE_SAFETY.md
│   ├── MANUAL_TESTING.md
│   ├── NODEJS_2025.md
│   ├── PLUGIN_DEVELOPMENT.md
│   ├── RELEASE_PROCESS.md
│   ├── RELOADEROO_FOR_XCODEBUILDMCP.md
│   ├── RELOADEROO_XCODEBUILDMCP_PRIMER.md
│   ├── RELOADEROO.md
│   ├── session_management_plan.md
│   ├── session-aware-migration-todo.md
│   ├── TEST_RUNNER_ENV_IMPLEMENTATION_PLAN.md
│   ├── TESTING.md
│   └── TOOLS.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
│   │   ├── 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
│   └── 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
│   ├── release.sh
│   ├── tools-cli.ts
│   └── update-tools-docs.ts
├── server.json
├── smithery.yaml
├── src
│   ├── core
│   │   ├── __tests__
│   │   │   └── resources.test.ts
│   │   ├── dynamic-tools.ts
│   │   ├── plugin-registry.ts
│   │   ├── plugin-types.ts
│   │   └── resources.ts
│   ├── doctor-cli.ts
│   ├── index.ts
│   ├── mcp
│   │   ├── resources
│   │   │   ├── __tests__
│   │   │   │   ├── devices.test.ts
│   │   │   │   ├── doctor.test.ts
│   │   │   │   └── simulators.test.ts
│   │   │   ├── devices.ts
│   │   │   ├── doctor.ts
│   │   │   └── simulators.ts
│   │   └── tools
│   │       ├── 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
│   │       ├── discovery
│   │       │   ├── __tests__
│   │       │   │   └── discover_tools.test.ts
│   │       │   ├── discover_tools.ts
│   │       │   └── index.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
│   │   └── server.ts
│   ├── test-utils
│   │   └── mock-executors.ts
│   ├── types
│   │   └── common.ts
│   └── utils
│       ├── __tests__
│       │   ├── build-utils.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
│       ├── axe
│       │   └── index.ts
│       ├── axe-helpers.ts
│       ├── build
│       │   └── index.ts
│       ├── build-utils.ts
│       ├── capabilities.ts
│       ├── command.ts
│       ├── CommandExecutor.ts
│       ├── environment.ts
│       ├── errors.ts
│       ├── execution
│       │   └── index.ts
│       ├── FileSystemExecutor.ts
│       ├── log_capture.ts
│       ├── log-capture
│       │   └── index.ts
│       ├── logger.ts
│       ├── logging
│       │   └── index.ts
│       ├── plugin-registry
│       │   └── index.ts
│       ├── responses
│       │   └── index.ts
│       ├── schema-helpers.ts
│       ├── sentry.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
│       ├── xcode.ts
│       ├── xcodemake
│       │   └── index.ts
│       └── xcodemake.ts
├── tsconfig.json
├── tsconfig.test.json
├── tsup.config.ts
└── vitest.config.ts
```

# Files

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

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

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

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

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

const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```

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

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

import { 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 {
  createAxeNotAvailableResponse,
  getAxePath,
  getBundledAxeEnvironment,
} from '../../../utils/axe/index.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';

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

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

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

const LOG_PREFIX = '[AXe]';

export async function long_pressLogic(
  params: LongPressParams,
  executor: CommandExecutor,
  axeHelpers: AxeHelpers = {
    getAxePath,
    getBundledAxeEnvironment,
    createAxeNotAvailableResponse,
  },
): Promise<ToolResponse> {
  const toolName = 'long_press';
  const { simulatorUuid, x, y, duration } = params;
  // AXe uses touch command with --down, --up, and --delay for long press
  const delayInSeconds = Number(duration) / 1000; // Convert ms to seconds
  const commandArgs = [
    'touch',
    '-x',
    String(x),
    '-y',
    String(y),
    '--down',
    '--up',
    '--delay',
    String(delayInSeconds),
  ];

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

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

    const warning = getCoordinateWarning(simulatorUuid);
    const message = `Long press at (${x}, ${y}) for ${duration}ms simulated successfully.`;

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

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

export default {
  name: 'long_press',
  description:
    "Long press at specific coordinates for given duration (ms). Use describe_ui for precise coordinates (don't guess from screenshots).",
  schema: longPressSchema.shape, // MCP SDK compatibility
  handler: createTypedTool(
    longPressSchema,
    (params: LongPressParams, executor: CommandExecutor) => {
      return long_pressLogic(params, executor, {
        getAxePath,
        getBundledAxeEnvironment,
        createAxeNotAvailableResponse,
      });
    },
    getDefaultCommandExecutor,
  ),
};

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

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

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

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

  return null;
}

// Helper function for executing axe commands (inlined from src/tools/axe/index.ts)
async function executeAxeCommand(
  commandArgs: string[],
  simulatorUuid: 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', simulatorUuid];

  // 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);

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

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

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

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

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

```

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

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

import { z } from 'zod';
import { log } from '../../../utils/logging/index.ts';
import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.ts';
import { DependencyError, AxeError, SystemError } from '../../../utils/errors.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
  createAxeNotAvailableResponse,
  getAxePath,
  getBundledAxeEnvironment,
} from '../../../utils/axe-helpers.ts';
import { ToolResponse } from '../../../types/common.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';

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

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

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

const LOG_PREFIX = '[AXe]';

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

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

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

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

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

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

    const warning = getCoordinateWarning(simulatorUuid);
    const message = `Touch event (${actionText}) at (${x}, ${y}) executed successfully.`;

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

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

export default {
  name: 'touch',
  description:
    "Perform touch down/up events at specific coordinates. Use describe_ui for precise coordinates (don't guess from screenshots).",
  schema: touchSchema.shape, // MCP SDK compatibility
  handler: createTypedTool(touchSchema, touchLogic, getDefaultCommandExecutor),
};

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

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

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

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

  return null;
}

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

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

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

  // 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);

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

    // 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/simulator/list_sims.ts:
--------------------------------------------------------------------------------

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

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

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

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

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

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

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

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

  return devices;
}

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

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

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

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

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

  return true;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      responseText += '\n';
    }

    responseText += 'Next Steps:\n';
    responseText += "1. Boot a simulator: boot_sim({ simulatorUuid: 'UUID_FROM_ABOVE' })\n";
    responseText += '2. Open the simulator UI: open_sim({})\n';
    responseText +=
      "3. Build for simulator: build_sim({ scheme: 'YOUR_SCHEME', simulatorId: 'UUID_FROM_ABOVE' })\n";
    responseText +=
      "4. Get app path: get_sim_app_path({ scheme: 'YOUR_SCHEME', platform: 'iOS Simulator', simulatorId: 'UUID_FROM_ABOVE' })";

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

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

```

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

```typescript
/**
 * Type-safe tool factory for XcodeBuildMCP
 *
 * This module provides a factory function to create MCP tool handlers that safely
 * convert from the generic Record<string, unknown> signature required by the MCP SDK
 * to strongly-typed parameters using runtime validation with Zod.
 *
 * This eliminates the need for unsafe type assertions while maintaining full
 * compatibility with the MCP SDK's tool handler signature requirements.
 */

import { z } from 'zod';
import { ToolResponse } from '../types/common.ts';
import type { CommandExecutor } from './execution/index.ts';
import { createErrorResponse } from './responses/index.ts';
import { sessionStore, type SessionDefaults } from './session-store.ts';

/**
 * Creates a type-safe tool handler that validates parameters at runtime
 * before passing them to the typed logic function.
 *
 * This is the ONLY safe way to cross the type boundary from the generic
 * MCP handler signature to our typed domain logic.
 *
 * @param schema - Zod schema for parameter validation
 * @param logicFunction - The typed logic function to execute
 * @param getExecutor - Function to get the command executor (must be provided)
 * @returns A handler function compatible with MCP SDK requirements
 */
export function createTypedTool<TParams>(
  schema: z.ZodType<TParams>,
  logicFunction: (params: TParams, executor: CommandExecutor) => Promise<ToolResponse>,
  getExecutor: () => CommandExecutor,
) {
  return async (args: Record<string, unknown>): Promise<ToolResponse> => {
    try {
      // Runtime validation - the ONLY safe way to cross the type boundary
      // This provides both compile-time and runtime type safety
      const validatedParams = schema.parse(args);

      // Now we have guaranteed type safety - no assertions needed!
      return await logicFunction(validatedParams, getExecutor());
    } catch (error) {
      if (error instanceof z.ZodError) {
        // Format validation errors in a user-friendly way
        const errorMessages = error.errors.map((e) => {
          const path = e.path.length > 0 ? `${e.path.join('.')}` : 'root';
          return `${path}: ${e.message}`;
        });

        return createErrorResponse(
          'Parameter validation failed',
          `Invalid parameters:\n${errorMessages.join('\n')}`,
        );
      }

      // Re-throw unexpected errors (they'll be caught by the MCP framework)
      throw error;
    }
  };
}

export type SessionRequirement =
  | { allOf: (keyof SessionDefaults)[]; message?: string }
  | { oneOf: (keyof SessionDefaults)[]; message?: string };

function missingFromMerged(
  keys: (keyof SessionDefaults)[],
  merged: Record<string, unknown>,
): string[] {
  return keys.filter((k) => merged[k] == null);
}

export function createSessionAwareTool<TParams>(opts: {
  internalSchema: z.ZodType<TParams>;
  logicFunction: (params: TParams, executor: CommandExecutor) => Promise<ToolResponse>;
  getExecutor: () => CommandExecutor;
  sessionKeys?: (keyof SessionDefaults)[];
  requirements?: SessionRequirement[];
  exclusivePairs?: (keyof SessionDefaults)[][]; // when args provide one side, drop conflicting session-default side(s)
}) {
  const {
    internalSchema,
    logicFunction,
    getExecutor,
    requirements = [],
    exclusivePairs = [],
  } = opts;

  return async (rawArgs: Record<string, unknown>): Promise<ToolResponse> => {
    try {
      // Sanitize args: treat null/undefined as "not provided" so they don't override session defaults
      const sanitizedArgs: Record<string, unknown> = {};
      for (const [k, v] of Object.entries(rawArgs)) {
        if (v === null || v === undefined) continue;
        if (typeof v === 'string' && v.trim() === '') continue;
        sanitizedArgs[k] = v;
      }

      // Factory-level mutual exclusivity check: if user provides multiple explicit values
      // within an exclusive group, reject early even if tool schema doesn't enforce XOR.
      for (const pair of exclusivePairs) {
        const provided = pair.filter((k) => Object.prototype.hasOwnProperty.call(sanitizedArgs, k));
        if (provided.length >= 2) {
          return createErrorResponse(
            'Parameter validation failed',
            `Invalid parameters:\nMutually exclusive parameters provided: ${provided.join(
              ', ',
            )}. Provide only one.`,
          );
        }
      }

      // Start with session defaults merged with explicit args (args override session)
      const merged: Record<string, unknown> = { ...sessionStore.getAll(), ...sanitizedArgs };

      // Apply exclusive pair pruning: only when caller provided a concrete (non-null/undefined) value
      // for any key in the pair. When activated, drop other keys in the pair coming from session defaults.
      for (const pair of exclusivePairs) {
        const userProvidedConcrete = pair.some((k) =>
          Object.prototype.hasOwnProperty.call(sanitizedArgs, k),
        );
        if (!userProvidedConcrete) continue;

        for (const k of pair) {
          if (!Object.prototype.hasOwnProperty.call(sanitizedArgs, k) && k in merged) {
            delete merged[k];
          }
        }
      }

      for (const req of requirements) {
        if ('allOf' in req) {
          const missing = missingFromMerged(req.allOf, merged);
          if (missing.length > 0) {
            return createErrorResponse(
              'Missing required session defaults',
              `${req.message ?? `Required: ${req.allOf.join(', ')}`}\n` +
                `Set with: session-set-defaults { ${missing
                  .map((k) => `"${k}": "..."`)
                  .join(', ')} }`,
            );
          }
        } else if ('oneOf' in req) {
          const satisfied = req.oneOf.some((k) => merged[k] != null);
          if (!satisfied) {
            const options = req.oneOf.join(', ');
            const setHints = req.oneOf
              .map((k) => `session-set-defaults { "${k}": "..." }`)
              .join(' OR ');
            return createErrorResponse(
              'Missing required session defaults',
              `${req.message ?? `Provide one of: ${options}`}\nSet with: ${setHints}`,
            );
          }
        }
      }

      const validated = internalSchema.parse(merged);
      return await logicFunction(validated, getExecutor());
    } catch (error) {
      if (error instanceof z.ZodError) {
        const errorMessages = error.errors.map((e) => {
          const path = e.path.length > 0 ? `${e.path.join('.')}` : 'root';
          return `${path}: ${e.message}`;
        });

        return createErrorResponse(
          'Parameter validation failed',
          `Invalid parameters:\n${errorMessages.join('\n')}\nTip: set session defaults via session-set-defaults`,
        );
      }
      throw error;
    }
  };
}

```

--------------------------------------------------------------------------------
/src/core/dynamic-tools.ts:
--------------------------------------------------------------------------------

```typescript
import { log } from '../utils/logger.ts';
import { getDefaultCommandExecutor, CommandExecutor } from '../utils/command.ts';
import { WORKFLOW_LOADERS, WorkflowName, WORKFLOW_METADATA } from './generated-plugins.ts';
import { ToolResponse } from '../types/common.ts';
import { PluginMeta } from './plugin-types.ts';
import { McpServer } from '@camsoft/mcp-sdk/server/mcp.js';
import {
  registerAndTrackTools,
  removeTrackedTools,
  isToolRegistered,
} from '../utils/tool-registry.ts';
import { ZodRawShape } from 'zod';

// Track enabled workflows and their tools for replacement functionality
const enabledWorkflows = new Set<string>();
const enabledTools = new Map<string, string>(); // toolName -> workflowName

// Type for the handler function from our tools
type ToolHandler = (
  args: Record<string, unknown>,
  executor: CommandExecutor,
) => Promise<ToolResponse>;

// Use the actual McpServer type from the SDK instead of a custom interface

/**
 * Wrapper function to adapt MCP SDK handler calling convention to our dependency injection pattern
 * MCP SDK calls handlers with just (args), but our handlers expect (args, executor)
 */
function wrapHandlerWithExecutor(handler: ToolHandler) {
  return async (args: unknown): Promise<ToolResponse> => {
    return handler(args as Record<string, unknown>, getDefaultCommandExecutor());
  };
}

/**
 * Clear currently enabled workflows by actually removing registered tools
 */
export function clearEnabledWorkflows(): void {
  if (enabledTools.size === 0) {
    log('debug', 'No tools to clear');
    return;
  }

  const clearedWorkflows = Array.from(enabledWorkflows);
  const toolNamesToRemove = Array.from(enabledTools.keys());
  const clearedToolCount = toolNamesToRemove.length;

  log('info', `Removing ${clearedToolCount} tools from workflows: ${clearedWorkflows.join(', ')}`);

  // Actually remove the registered tools using the tool registry
  const removedTools = removeTrackedTools(toolNamesToRemove);

  // Clear our tracking
  enabledWorkflows.clear();
  enabledTools.clear();

  log('info', `✅ Removed ${removedTools.length} tools successfully`);
}

/**
 * Get currently enabled workflows
 */
export function getEnabledWorkflows(): string[] {
  return Array.from(enabledWorkflows);
}

/**
 * Enable workflows by registering their tools dynamically using generated loaders
 * @param server - MCP server instance
 * @param workflowNames - Array of workflow names to enable
 * @param additive - If true, add to existing workflows. If false (default), replace existing workflows
 */
export async function enableWorkflows(
  server: McpServer,
  workflowNames: string[],
  additive: boolean = false,
): Promise<void> {
  if (!server) {
    throw new Error('Server instance not available for dynamic tool registration');
  }

  // Clear existing workflow tracking unless in additive mode
  if (!additive && enabledWorkflows.size > 0) {
    log('info', `Replacing existing workflows: ${Array.from(enabledWorkflows).join(', ')}`);
    clearEnabledWorkflows();
  }

  let totalToolsAdded = 0;

  for (const workflowName of workflowNames) {
    const loader = WORKFLOW_LOADERS[workflowName as WorkflowName];

    if (!loader) {
      log('warn', `Workflow '${workflowName}' not found in available workflows`);
      continue;
    }

    try {
      log('info', `Loading workflow '${workflowName}' with code-splitting...`);

      // Dynamic import with code-splitting
      const workflowModule = (await loader()) as Record<string, unknown>;

      // Get tools count from the module (excluding 'workflow' key)
      const toolKeys = Object.keys(workflowModule).filter((key) => key !== 'workflow');

      log('info', `Enabling ${toolKeys.length} tools from '${workflowName}' workflow`);

      const toolsToRegister: Array<{
        name: string;
        config: {
          title?: string;
          description?: string;
          inputSchema?: ZodRawShape;
          outputSchema?: ZodRawShape;
          annotations?: Record<string, unknown>;
        };
        callback: (args: Record<string, unknown>) => Promise<ToolResponse>;
      }> = [];

      // Collect all tools from this workflow, filtering out already-registered tools
      for (const toolKey of toolKeys) {
        const tool = workflowModule[toolKey] as PluginMeta | undefined;

        if (tool?.name && typeof tool.handler === 'function') {
          // Always skip tools that are already registered (in all modes)
          if (isToolRegistered(tool.name)) {
            log('debug', `Skipping already registered tool: ${tool.name}`);
            continue;
          }

          toolsToRegister.push({
            name: tool.name,
            config: {
              description: tool.description ?? '',
              inputSchema: tool.schema,
            },
            callback: wrapHandlerWithExecutor(tool.handler as ToolHandler),
          });

          // Track the tool and workflow
          enabledTools.set(tool.name, workflowName);
          totalToolsAdded++;
        } else {
          log('warn', `Invalid tool definition for '${toolKey}' in workflow '${workflowName}'`);
        }
      }

      // Register all tools using bulk registration
      if (toolsToRegister.length > 0) {
        log(
          'info',
          `🚀 Registering ${toolsToRegister.length} tools from '${workflowName}' workflow`,
        );

        // Convert to proper tool registration format
        const toolRegistrations = toolsToRegister.map((tool) => ({
          name: tool.name,
          config: {
            description: tool.config.description,
            inputSchema: tool.config.inputSchema as unknown,
          },
          callback: (args: unknown): Promise<ToolResponse> =>
            tool.callback(args as Record<string, unknown>),
        }));

        // Use bulk registration - no fallback needed with proper duplicate handling
        const registeredTools = registerAndTrackTools(server, toolRegistrations);
        log('info', `✅ Registered ${registeredTools.length} tools from '${workflowName}'`);
      } else {
        log('info', `No new tools to register from '${workflowName}' (all already registered)`);
      }

      // Track the workflow as enabled
      enabledWorkflows.add(workflowName);
    } catch (error) {
      log(
        'error',
        `Failed to load workflow '${workflowName}': ${error instanceof Error ? error.message : 'Unknown error'}`,
      );
    }
  }

  // registerAndTrackTools() handles tool list change notifications automatically

  log(
    'info',
    `✅ Successfully enabled ${totalToolsAdded} tools from ${workflowNames.length} workflows`,
  );
}

/**
 * Get list of currently available workflows using generated metadata
 */
export function getAvailableWorkflows(): string[] {
  return Object.keys(WORKFLOW_LOADERS);
}

/**
 * Get workflow information for LLM prompt generation using generated metadata
 */
export function generateWorkflowDescriptions(): string {
  return Object.entries(WORKFLOW_METADATA)
    .map(([name, metadata]) => `- **${name.toUpperCase()}**: ${metadata.description}`)
    .join('\n');
}

```

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

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

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

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

  type Params = z.infer<typeof internalSchema>;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```

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

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

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

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

type RecordSimVideoParams = z.infer<typeof recordSimVideoSchema>;

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

  // using injected fs executor

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

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

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

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

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

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

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

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

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

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

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

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

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

```

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

```typescript
/**
 * UI Testing Plugin: Swipe
 *
 * Swipe from one coordinate to another on iOS simulator with customizable duration and delta.
 */

import { 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 {
  createAxeNotAvailableResponse,
  getAxePath,
  getBundledAxeEnvironment,
} from '../../../utils/axe-helpers.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';

// Define schema as ZodObject
const swipeSchema = z.object({
  simulatorUuid: z.string().uuid('Invalid Simulator UUID format'),
  x1: z.number().int('Start X coordinate'),
  y1: z.number().int('Start Y coordinate'),
  x2: z.number().int('End X coordinate'),
  y2: z.number().int('End Y coordinate'),
  duration: z.number().min(0, 'Duration must be non-negative').optional(),
  delta: z.number().min(0, 'Delta must be non-negative').optional(),
  preDelay: z.number().min(0, 'Pre-delay must be non-negative').optional(),
  postDelay: z.number().min(0, 'Post-delay must be non-negative').optional(),
});

// Use z.infer for type safety
type SwipeParams = z.infer<typeof swipeSchema>;

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

const LOG_PREFIX = '[AXe]';

/**
 * Core swipe logic implementation
 */
export async function swipeLogic(
  params: SwipeParams,
  executor: CommandExecutor,
  axeHelpers: AxeHelpers = {
    getAxePath,
    getBundledAxeEnvironment,
    createAxeNotAvailableResponse,
  },
): Promise<ToolResponse> {
  const toolName = 'swipe';

  const { simulatorUuid, x1, y1, x2, y2, duration, delta, preDelay, postDelay } = params;
  const commandArgs = [
    'swipe',
    '--start-x',
    String(x1),
    '--start-y',
    String(y1),
    '--end-x',
    String(x2),
    '--end-y',
    String(y2),
  ];
  if (duration !== undefined) {
    commandArgs.push('--duration', String(duration));
  }
  if (delta !== undefined) {
    commandArgs.push('--delta', String(delta));
  }
  if (preDelay !== undefined) {
    commandArgs.push('--pre-delay', String(preDelay));
  }
  if (postDelay !== undefined) {
    commandArgs.push('--post-delay', String(postDelay));
  }

  const optionsText = duration ? ` duration=${duration}s` : '';
  log(
    'info',
    `${LOG_PREFIX}/${toolName}: Starting swipe (${x1},${y1})->(${x2},${y2})${optionsText} on ${simulatorUuid}`,
  );

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

    const warning = getCoordinateWarning(simulatorUuid);
    const message = `Swipe from (${x1}, ${y1}) to (${x2}, ${y2})${optionsText} simulated successfully.`;

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

    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 swipe: ${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: 'swipe',
  description:
    "Swipe from one point to another. Use describe_ui for precise coordinates (don't guess from screenshots). Supports configurable timing.",
  schema: swipeSchema.shape, // MCP SDK compatibility
  handler: createTypedTool(
    swipeSchema,
    (params: SwipeParams, executor: CommandExecutor) => {
      return swipeLogic(params, executor, {
        getAxePath,
        getBundledAxeEnvironment,
        createAxeNotAvailableResponse,
      });
    },
    getDefaultCommandExecutor,
  ),
};

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

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

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

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

  return null;
}

// Helper function for executing axe commands (inlined from src/tools/axe/index.ts)
async function executeAxeCommand(
  commandArgs: string[],
  simulatorUuid: 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', simulatorUuid];

  // 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);

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

    // 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/gesture.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * UI Testing Plugin: Gesture
 *
 * Perform gesture on iOS simulator using preset gestures: scroll-up, scroll-down, scroll-left, scroll-right,
 * swipe-from-left-edge, swipe-from-right-edge, swipe-from-top-edge, swipe-from-bottom-edge.
 */

import { 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 {
  createAxeNotAvailableResponse,
  getAxePath,
  getBundledAxeEnvironment,
} from '../../../utils/axe/index.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';

// Define schema as ZodObject
const gestureSchema = z.object({
  simulatorUuid: z.string().uuid('Invalid Simulator UUID format'),
  preset: z
    .enum([
      'scroll-up',
      'scroll-down',
      'scroll-left',
      'scroll-right',
      'swipe-from-left-edge',
      'swipe-from-right-edge',
      'swipe-from-top-edge',
      'swipe-from-bottom-edge',
    ])
    .describe(
      'The gesture preset to perform. Must be one of: scroll-up, scroll-down, scroll-left, scroll-right, swipe-from-left-edge, swipe-from-right-edge, swipe-from-top-edge, swipe-from-bottom-edge.',
    ),
  screenWidth: z
    .number()
    .int()
    .min(1)
    .optional()
    .describe(
      'Optional: Screen width in pixels. Used for gesture calculations. Auto-detected if not provided.',
    ),
  screenHeight: z
    .number()
    .int()
    .min(1)
    .optional()
    .describe(
      'Optional: Screen height in pixels. Used for gesture calculations. Auto-detected if not provided.',
    ),
  duration: z
    .number()
    .min(0, 'Duration must be non-negative')
    .optional()
    .describe('Optional: Duration of the gesture in seconds.'),
  delta: z
    .number()
    .min(0, 'Delta must be non-negative')
    .optional()
    .describe('Optional: Distance to move in pixels.'),
  preDelay: z
    .number()
    .min(0, 'Pre-delay must be non-negative')
    .optional()
    .describe('Optional: Delay before starting the gesture in seconds.'),
  postDelay: z
    .number()
    .min(0, 'Post-delay must be non-negative')
    .optional()
    .describe('Optional: Delay after completing the gesture in seconds.'),
});

// Use z.infer for type safety
type GestureParams = z.infer<typeof gestureSchema>;

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

const LOG_PREFIX = '[AXe]';

export async function gestureLogic(
  params: GestureParams,
  executor: CommandExecutor,
  axeHelpers: AxeHelpers = {
    getAxePath,
    getBundledAxeEnvironment,
    createAxeNotAvailableResponse,
  },
): Promise<ToolResponse> {
  const toolName = 'gesture';
  const { simulatorUuid, preset, screenWidth, screenHeight, duration, delta, preDelay, postDelay } =
    params;
  const commandArgs = ['gesture', preset];

  if (screenWidth !== undefined) {
    commandArgs.push('--screen-width', String(screenWidth));
  }
  if (screenHeight !== undefined) {
    commandArgs.push('--screen-height', String(screenHeight));
  }
  if (duration !== undefined) {
    commandArgs.push('--duration', String(duration));
  }
  if (delta !== undefined) {
    commandArgs.push('--delta', String(delta));
  }
  if (preDelay !== undefined) {
    commandArgs.push('--pre-delay', String(preDelay));
  }
  if (postDelay !== undefined) {
    commandArgs.push('--post-delay', String(postDelay));
  }

  log('info', `${LOG_PREFIX}/${toolName}: Starting gesture '${preset}' on ${simulatorUuid}`);

  try {
    await executeAxeCommand(commandArgs, simulatorUuid, 'gesture', executor, axeHelpers);
    log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`);
    return createTextResponse(`Gesture '${preset}' executed successfully.`);
  } catch (error) {
    log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
    if (error instanceof DependencyError) {
      return axeHelpers.createAxeNotAvailableResponse();
    } else if (error instanceof AxeError) {
      return createErrorResponse(
        `Failed to execute gesture '${preset}': ${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: 'gesture',
  description:
    'Perform gesture on iOS simulator using preset gestures: scroll-up, scroll-down, scroll-left, scroll-right, swipe-from-left-edge, swipe-from-right-edge, swipe-from-top-edge, swipe-from-bottom-edge',
  schema: gestureSchema.shape, // MCP SDK compatibility
  handler: createTypedTool(
    gestureSchema,
    (params: GestureParams, executor: CommandExecutor) => {
      return gestureLogic(params, executor, {
        getAxePath,
        getBundledAxeEnvironment,
        createAxeNotAvailableResponse,
      });
    },
    getDefaultCommandExecutor,
  ),
};

// Helper function for executing axe commands (inlined from src/tools/axe/index.ts)
async function executeAxeCommand(
  commandArgs: string[],
  simulatorUuid: 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', simulatorUuid];

  // 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);

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

    // 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)}`);
  }
}

```

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

```yaml
name: Release

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

permissions:
  contents: write
  id-token: write

jobs:
  release:
    runs-on: macos-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

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

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

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

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

      - name: Build TypeScript
        run: npm run build

      - name: Run tests
        run: npm test

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

      - name: Create package
        run: npm pack

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

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

      - name: Create GitHub Release (production releases only)
        if: github.event_name == 'push'
        uses: softprops/action-gh-release@v1
        with:
          tag_name: v${{ steps.get_version.outputs.VERSION }}
          name: Release v${{ steps.get_version.outputs.VERSION }}
          body: |
            ## Release v${{ steps.get_version.outputs.VERSION }}
            
            ### Features
            - Bundled AXe binary and frameworks for zero-setup UI automation
            - No manual installation required - works out of the box
            
            ### Installation
            ```bash
            npm install -g xcodebuildmcp@${{ steps.get_version.outputs.VERSION }}
            ```
            
            Or use with npx:
            ```bash
            npx xcodebuildmcp@${{ steps.get_version.outputs.VERSION }}
            ```
            
            📦 **NPM Package**: https://www.npmjs.com/package/xcodebuildmcp/v/${{ steps.get_version.outputs.VERSION }}
            
            ### What's Included
            - Latest AXe binary from [cameroncooke/axe](https://github.com/cameroncooke/axe)
            - All required frameworks (FBControlCore, FBDeviceControl, FBSimulatorControl, XCTestBootstrap)
            - Full XcodeBuildMCP functionality with UI automation support
          files: |
            xcodebuildmcp-${{ steps.get_version.outputs.VERSION }}.tgz
          draft: false
          prerelease: false

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

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

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

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

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

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

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

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

```

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

```typescript
/**
 * Tests for build-utils Sentry classification logic
 */

import { describe, it, expect } from 'vitest';
import { createMockExecutor } from '../../test-utils/mock-executors.ts';
import { executeXcodeBuildCommand } from '../build-utils.ts';
import { XcodePlatform } from '../xcode.ts';

describe('build-utils Sentry Classification', () => {
  const mockPlatformOptions = {
    platform: XcodePlatform.macOS,
    logPrefix: 'Test Build',
  };

  const mockParams = {
    scheme: 'TestScheme',
    configuration: 'Debug',
    projectPath: '/path/to/project.xcodeproj',
  };

  describe('Exit Code 64 Classification (MCP Error)', () => {
    it('should trigger Sentry logging for exit code 64 (invalid arguments)', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'xcodebuild: error: invalid option',
        exitCode: 64,
      });

      const result = await executeXcodeBuildCommand(
        mockParams,
        mockPlatformOptions,
        false,
        'build',
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('❌ [stderr] xcodebuild: error: invalid option');
      expect(result.content[1].text).toContain('❌ Test Build build failed for scheme TestScheme');
    });
  });

  describe('Other Exit Codes Classification (User Error)', () => {
    it('should not trigger Sentry logging for exit code 65 (user error)', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Scheme TestScheme was not found',
        exitCode: 65,
      });

      const result = await executeXcodeBuildCommand(
        mockParams,
        mockPlatformOptions,
        false,
        'build',
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('❌ [stderr] Scheme TestScheme was not found');
      expect(result.content[1].text).toContain('❌ Test Build build failed for scheme TestScheme');
    });

    it('should not trigger Sentry logging for exit code 66 (file not found)', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'project.xcodeproj cannot be opened',
        exitCode: 66,
      });

      const result = await executeXcodeBuildCommand(
        mockParams,
        mockPlatformOptions,
        false,
        'build',
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('❌ [stderr] project.xcodeproj cannot be opened');
    });

    it('should not trigger Sentry logging for exit code 70 (destination error)', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Unable to find a destination matching the provided destination specifier',
        exitCode: 70,
      });

      const result = await executeXcodeBuildCommand(
        mockParams,
        mockPlatformOptions,
        false,
        'build',
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('❌ [stderr] Unable to find a destination matching');
    });

    it('should not trigger Sentry logging for exit code 1 (general build failure)', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Build failed with errors',
        exitCode: 1,
      });

      const result = await executeXcodeBuildCommand(
        mockParams,
        mockPlatformOptions,
        false,
        'build',
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('❌ [stderr] Build failed with errors');
    });
  });

  describe('Spawn Error Classification (Environment Error)', () => {
    it('should not trigger Sentry logging for ENOENT spawn error', async () => {
      const spawnError = new Error('spawn xcodebuild ENOENT') as NodeJS.ErrnoException;
      spawnError.code = 'ENOENT';

      const mockExecutor = createMockExecutor({
        success: false,
        error: '',
        shouldThrow: spawnError,
      });

      const result = await executeXcodeBuildCommand(
        mockParams,
        mockPlatformOptions,
        false,
        'build',
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain(
        'Error during Test Build build: spawn xcodebuild ENOENT',
      );
    });

    it('should not trigger Sentry logging for EACCES spawn error', async () => {
      const spawnError = new Error('spawn xcodebuild EACCES') as NodeJS.ErrnoException;
      spawnError.code = 'EACCES';

      const mockExecutor = createMockExecutor({
        success: false,
        error: '',
        shouldThrow: spawnError,
      });

      const result = await executeXcodeBuildCommand(
        mockParams,
        mockPlatformOptions,
        false,
        'build',
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain(
        'Error during Test Build build: spawn xcodebuild EACCES',
      );
    });

    it('should not trigger Sentry logging for EPERM spawn error', async () => {
      const spawnError = new Error('spawn xcodebuild EPERM') as NodeJS.ErrnoException;
      spawnError.code = 'EPERM';

      const mockExecutor = createMockExecutor({
        success: false,
        error: '',
        shouldThrow: spawnError,
      });

      const result = await executeXcodeBuildCommand(
        mockParams,
        mockPlatformOptions,
        false,
        'build',
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain(
        'Error during Test Build build: spawn xcodebuild EPERM',
      );
    });

    it('should trigger Sentry logging for non-spawn exceptions', async () => {
      const otherError = new Error('Unexpected internal error');

      const mockExecutor = createMockExecutor({
        success: false,
        error: '',
        shouldThrow: otherError,
      });

      const result = await executeXcodeBuildCommand(
        mockParams,
        mockPlatformOptions,
        false,
        'build',
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain(
        'Error during Test Build build: Unexpected internal error',
      );
    });
  });

  describe('Success Case (No Sentry Logging)', () => {
    it('should not trigger any error logging for successful builds', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'BUILD SUCCEEDED',
        exitCode: 0,
      });

      const result = await executeXcodeBuildCommand(
        mockParams,
        mockPlatformOptions,
        false,
        'build',
        mockExecutor,
      );

      expect(result.isError).toBeFalsy();
      expect(result.content[0].text).toContain(
        '✅ Test Build build succeeded for scheme TestScheme',
      );
    });
  });

  describe('Exit Code Undefined Cases', () => {
    it('should not trigger Sentry logging when exitCode is undefined', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Some error without exit code',
        exitCode: undefined,
      });

      const result = await executeXcodeBuildCommand(
        mockParams,
        mockPlatformOptions,
        false,
        'build',
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('❌ [stderr] Some error without exit code');
    });
  });
});

```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
# Changelog

## [1.14.0] - 2025-09-22
- Add video capture tool for simulators

## [1.13.1] - 2025-09-21
- Add simulator erase content and settings tool

## [1.12.3] - 2025-08-22
- Pass environment variables to test runs on device, simulator, and macOS via an optional testRunnerEnv input (auto-prefixed as TEST_RUNNER_).

## [1.12.2] - 2025-08-21
### Fixed
- **Clean tool**: Fixed issue where clean would fail for simulators

## [1.12.1] - 2025-08-18
### Improved
- **Sentry Logging**: No longer logs domain errors to Sentry, now only logs MCP server errors.

## [1.12.0] - 2025-08-17
### Added
- Unify project/workspace and sim id/name tools into a single tools reducing the number of tools from 81 to 59, this helps reduce the client agent's context window size by 27%!
- **Selective Workflow Loading**: New `XCODEBUILDMCP_ENABLED_WORKFLOWS` environment variable allows loading only specific workflow groups in static mode, reducing context window usage for clients that don't support MCP sampling (Thanks to @codeman9 for their first contribution!)
- Rename `diagnosics` tool and cli to `doctor`
- Add Sentry instrumentation to track MCP usage statistics (can be disabled by setting `XCODEBUILDMCP_SENTRY_DISABLED=true`)
- Add support for MCP setLevel handler to allow clients to control the log level of the MCP server

## [v1.11.2] - 2025-08-08
- Fixed "registerTools is not a function" errors during package upgrades

## [v1.11.1] - 2025-08-07
- Improved tool discovery to be more accurate and context-aware

## [v1.11.0] - 2025-08-07
- Major refactor/rewrite to improve code quality and maintainability in preparation for future development
- Added support for dynamic tools (VSCode only for now)
- Added support for MCP Resources (devices, simulators, environment info)
- Workaround for https://github.com/cameroncooke/XcodeBuildMCP/issues/66 and https://github.com/anthropics/claude-code/issues/1804 issues where Claude Code would only see the first text content from tool responses

## [v1.10.0] - 2025-06-10
### Added
- **App Lifecycle Management**: New tools for stopping running applications
  - `stop_app_device`: Stop apps running on physical Apple devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro)
  - `stop_app_sim`: Stop apps running on iOS/watchOS/tvOS/visionOS simulators
  - `stop_mac_app`: Stop macOS applications by name or process ID
- **Enhanced Launch Tools**: Device launch tools now return process IDs for better app management
- **Bundled AXe Distribution**: AXe binary and frameworks now included in npm package for zero-setup UI automation

### Fixed
- **WiFi Device Detection**: Improved detection of Apple devices connected over WiFi networks
- **Device Connectivity**: Better handling of paired devices with different connection states

### Improved
- **Simplified Installation**: No separate AXe installation required - everything works out of the box

## [v1.9.0] - 2025-06-09
- Added support for hardware devices over USB and Wi-Fi
- New tools for Apple device deployment:
  - `install_app_device`
  - `launch_app_device`
- Updated all simulator and device tools to be platform-agnostic, supporting all Apple platforms (iOS, iPadOS, watchOS, tvOS, visionOS)
- Changed `get_ios_bundle_id` to `get_app_bundle_id` with support for all Apple platforms

## [v1.8.0] - 2025-06-07
- Added support for running tests on macOS, iOS simulators, and iOS devices
- New tools for testing:
  - `test_macos_workspace`
  - `test_macos_project`
  - `test_ios_simulator_name_workspace`
  - `test_ios_simulator_name_project`
  - `test_ios_simulator_id_workspace`
  - `test_ios_simulator_id_project`
  - `test_ios_device_workspace`
  - `test_ios_device_project`

## [v1.7.0] - 2025-06-04
- Added support for Swift Package Manager (SPM)
- New tools for Swift Package Manager:
  - `swift_package_build`
  - `swift_package_clean`
  - `swift_package_test`
  - `swift_package_run`
  - `swift_package_list`
  - `swift_package_stop`

## [v1.6.1] - 2025-06-03
- Improve UI tool hints

## [v1.6.0] - 2025-06-03
- Moved project templates to external GitHub repositories for independent versioning
- Added support for downloading templates from GitHub releases
- Added local template override support via environment variables
- Added `scaffold_ios_project` and `scaffold_macos_project` tools for creating new projects
- Centralized template version management in package.json for easier updates

## [v1.5.0] - 2025-06-01
- UI automation is no longer in beta!
- Added support for AXe UI automation
- Revised default installation instructions to prefer npx instead of mise

## [v1.4.0] - 2025-05-11
- Merge the incremental build beta branch into main
- Add preferXcodebuild argument to build tools with improved error handling allowing the agent to force the use of xcodebuild over xcodemake for complex projects. It also adds a hint when incremental builds fail due to non-compiler errors, enabling the agent to automatically switch to xcodebuild for a recovery build attempt, improving reliability.

## [v1.3.7] - 2025-05-08
- Fix Claude Code issue due to long tool names

## [v1.4.0-beta.3] - 2025-05-07
- Fixed issue where incremental builds would only work for "Debug" build configurations
-
## [v1.4.0-beta.2] - 2025-05-07
- Same as beta 1 but has the latest features from the main release channel

## [v1.4.0-beta.1] - 2025-05-05
- Added experimental support for incremental builds (requires opt-in)

## [v1.3.6] - 2025-05-07
- Added support for enabling/disabling tools via environment variables

## [v1.3.5] - 2025-05-05
- Fixed the text input UI automation tool
- Improve the UI automation tool hints to reduce agent tool call errors
- Improved the project discovery tool to reduce agent tool call errors
- Added instructions for installing idb client manually

## [v1.3.4] - 2025-05-04
- Improved Sentry integration

## [v1.3.3] - 2025-05-04
- Added Sentry opt-out functionality

## [v1.3.1] - 2025-05-03
- Added Sentry integration for error reporting

## [v1.3.0] - 2025-04-28

- Added support for interacting with the simulator (tap, swipe etc.)
- Added support for capturing simulator screenshots

Please note that the UI automation features are an early preview and currently in beta your mileage may vary.

## [v1.2.4] - 2025-04-24
- Improved xcodebuild reporting of warnings and errors in tool response
- Refactor build utils and remove redundant code

## [v1.2.3] - 2025-04-23
- Added support for skipping macro validation

## [v1.2.2] - 2025-04-23
- Improved log readability with version information for easier debugging
- Enhanced overall stability and performance

## [v1.2.1] - 2025-04-23
- General stability improvements and bug fixes

## [v1.2.0] - 2025-04-14
### Added
- New simulator log capture feature: Easily view and debug your app's logs while running in the simulator
- Automatic project discovery: XcodeBuildMCP now finds your Xcode projects and workspaces automatically
- Support for both Intel and Apple Silicon Macs in macOS builds

### Improved
- Cleaner, more readable build output with better error messages
- Faster build times and more reliable build process
- Enhanced documentation with clearer usage examples

## [v1.1.0] - 2025-04-05
### Added
- Real-time build progress reporting
- Separate tools for iOS and macOS builds
- Better workspace and project support

### Improved
- Simplified build commands with better parameter handling
- More reliable clean operations for both projects and workspaces

## [v1.0.2] - 2025-04-02
- Improved documentation with better examples and clearer instructions
- Easier version tracking for compatibility checks

## [v1.0.1] - 2025-04-02
- Initial release of XcodeBuildMCP
- Basic support for building iOS and macOS applications

```

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

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

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

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

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

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

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

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

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

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

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Parameter validation failed');
      expect(result.content[0].text).toContain('bundleId: Required');
      expect(result.content[0].text).toContain(
        'Tip: set session defaults via session-set-defaults',
      );
    });

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

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

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

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

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

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

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

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

    it('should handle simulator lookup failure', async () => {
      const listExecutor = createMockExecutor({
        success: true,
        output: JSON.stringify({ devices: {} }),
        error: '',
      });

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

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

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

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

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

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

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

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

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

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

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

    it('should call correct terminate command', async () => {
      const calls: Array<{
        command: string[];
        description: string;
        suppressErrorLogging: boolean;
        timeout?: number;
      }> = [];

      const trackingExecutor = async (
        command: string[],
        description: string,
        suppressErrorLogging: boolean,
        timeout?: number,
      ) => {
        calls.push({ command, description, suppressErrorLogging, timeout });
        return {
          success: true,
          output: '',
          error: undefined,
          process: { pid: 12345 },
        };
      };

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

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

```

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  try {
    process.chdir(projectDir);

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

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

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

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

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

  return getDefaultCommandExecutor()(command, logPrefix);
}

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

```

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

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

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

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

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

  return { isValid: true };
}

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

  return { isValid: true };
}

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

  return { isValid: true };
}

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

  return { isValid: true };
}

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

  return { isValid: true };
}

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

  return { isValid: true };
}

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

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

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

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

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

  const consolidatedText = textParts.join('');

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

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

```

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        resolve(response);
      });

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

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

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

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

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

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

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

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

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

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

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

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

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

```

--------------------------------------------------------------------------------
/src/mcp/tools/doctor/lib/doctor.deps.ts:
--------------------------------------------------------------------------------

```typescript
import * as os from 'os';
import type { CommandExecutor } from '../../../../utils/execution/index.ts';
import {
  loadWorkflowGroups,
  loadPlugins,
  getEnabledWorkflows,
} from '../../../../utils/plugin-registry/index.ts';
import { areAxeToolsAvailable } from '../../../../utils/axe/index.ts';
import {
  isXcodemakeEnabled,
  isXcodemakeAvailable,
  doesMakefileExist,
} from '../../../../utils/xcodemake/index.ts';
import { getTrackedToolNames } from '../../../../utils/tool-registry.ts';

export interface BinaryChecker {
  checkBinaryAvailability(binary: string): Promise<{ available: boolean; version?: string }>;
}

export interface XcodeInfoProvider {
  getXcodeInfo(): Promise<
    | { version: string; path: string; selectedXcode: string; xcrunVersion: string }
    | { error: string }
  >;
}

export interface EnvironmentInfoProvider {
  getEnvironmentVariables(): Record<string, string | undefined>;
  getSystemInfo(): {
    platform: string;
    release: string;
    arch: string;
    cpus: string;
    memory: string;
    hostname: string;
    username: string;
    homedir: string;
    tmpdir: string;
  };
  getNodeInfo(): {
    version: string;
    execPath: string;
    pid: string;
    ppid: string;
    platform: string;
    arch: string;
    cwd: string;
    argv: string;
  };
}

export interface PluginInfoProvider {
  getPluginSystemInfo(): Promise<
    | {
        totalPlugins: number;
        pluginDirectories: number;
        pluginsByDirectory: Record<string, string[]>;
        systemMode: string;
      }
    | { error: string; systemMode: string }
  >;
}

export interface RuntimeInfoProvider {
  getRuntimeToolInfo(): Promise<
    | {
        mode: 'dynamic';
        enabledWorkflows: string[];
        enabledTools: string[];
        totalRegistered: number;
      }
    | {
        mode: 'static';
        enabledWorkflows: string[];
        enabledTools: string[];
        totalRegistered: number;
      }
  >;
}

export interface FeatureDetector {
  areAxeToolsAvailable(): boolean;
  isXcodemakeEnabled(): boolean;
  isXcodemakeAvailable(): Promise<boolean>;
  doesMakefileExist(path: string): boolean;
}

export interface DoctorDependencies {
  binaryChecker: BinaryChecker;
  xcode: XcodeInfoProvider;
  env: EnvironmentInfoProvider;
  plugins: PluginInfoProvider;
  runtime: RuntimeInfoProvider;
  features: FeatureDetector;
}

export function createDoctorDependencies(executor: CommandExecutor): DoctorDependencies {
  const binaryChecker: BinaryChecker = {
    async checkBinaryAvailability(binary: string) {
      // If bundled axe is available, reflect that in dependencies even if not on PATH
      if (binary === 'axe' && areAxeToolsAvailable()) {
        return { available: true, version: 'Bundled' };
      }
      try {
        const which = await executor(['which', binary], 'Check Binary Availability');
        if (!which.success) {
          return { available: false };
        }
      } catch {
        return { available: false };
      }

      let version: string | undefined;
      const versionCommands: Record<string, string> = {
        axe: 'axe --version',
        mise: 'mise --version',
      };

      if (binary in versionCommands) {
        try {
          const res = await executor(versionCommands[binary]!.split(' '), 'Get Binary Version');
          if (res.success && res.output) {
            version = res.output.trim();
          }
        } catch {
          // ignore
        }
      }

      return { available: true, version: version ?? 'Available (version info not available)' };
    },
  };

  const xcode: XcodeInfoProvider = {
    async getXcodeInfo() {
      try {
        const xcodebuild = await executor(['xcodebuild', '-version'], 'Get Xcode Version');
        if (!xcodebuild.success) throw new Error('xcodebuild command failed');
        const version = xcodebuild.output.trim().split('\n').slice(0, 2).join(' - ');

        const pathRes = await executor(['xcode-select', '-p'], 'Get Xcode Path');
        if (!pathRes.success) throw new Error('xcode-select command failed');
        const path = pathRes.output.trim();

        const selected = await executor(['xcrun', '--find', 'xcodebuild'], 'Find Xcodebuild');
        if (!selected.success) throw new Error('xcrun --find command failed');
        const selectedXcode = selected.output.trim();

        const xcrun = await executor(['xcrun', '--version'], 'Get Xcrun Version');
        if (!xcrun.success) throw new Error('xcrun --version command failed');
        const xcrunVersion = xcrun.output.trim();

        return { version, path, selectedXcode, xcrunVersion };
      } catch (error) {
        return { error: error instanceof Error ? error.message : String(error) };
      }
    },
  };

  const env: EnvironmentInfoProvider = {
    getEnvironmentVariables() {
      const relevantVars = [
        'INCREMENTAL_BUILDS_ENABLED',
        'PATH',
        'DEVELOPER_DIR',
        'HOME',
        'USER',
        'TMPDIR',
        'NODE_ENV',
        'SENTRY_DISABLED',
      ];

      const envVars: Record<string, string | undefined> = {};
      for (const varName of relevantVars) {
        envVars[varName] = process.env[varName];
      }

      Object.keys(process.env).forEach((key) => {
        if (key.startsWith('XCODEBUILDMCP_')) {
          envVars[key] = process.env[key];
        }
      });

      return envVars;
    },

    getSystemInfo() {
      return {
        platform: os.platform(),
        release: os.release(),
        arch: os.arch(),
        cpus: `${os.cpus().length} x ${os.cpus()[0]?.model ?? 'Unknown'}`,
        memory: `${Math.round(os.totalmem() / (1024 * 1024 * 1024))} GB`,
        hostname: os.hostname(),
        username: os.userInfo().username,
        homedir: os.homedir(),
        tmpdir: os.tmpdir(),
      };
    },

    getNodeInfo() {
      return {
        version: process.version,
        execPath: process.execPath,
        pid: process.pid.toString(),
        ppid: process.ppid.toString(),
        platform: process.platform,
        arch: process.arch,
        cwd: process.cwd(),
        argv: process.argv.join(' '),
      };
    },
  };

  const plugins: PluginInfoProvider = {
    async getPluginSystemInfo() {
      try {
        const workflows = await loadWorkflowGroups();
        const pluginsByDirectory: Record<string, string[]> = {};
        let totalPlugins = 0;

        for (const [dirName, wf] of workflows.entries()) {
          const toolNames = wf.tools.map((t) => t.name).filter(Boolean) as string[];
          totalPlugins += toolNames.length;
          pluginsByDirectory[dirName] = toolNames;
        }

        return {
          totalPlugins,
          pluginDirectories: workflows.size,
          pluginsByDirectory,
          systemMode: 'plugin-based',
        };
      } catch (error) {
        return {
          error: `Failed to load plugins: ${error instanceof Error ? error.message : 'Unknown error'}`,
          systemMode: 'error',
        };
      }
    },
  };

  const runtime: RuntimeInfoProvider = {
    async getRuntimeToolInfo() {
      const dynamic = process.env.XCODEBUILDMCP_DYNAMIC_TOOLS === 'true';

      if (dynamic) {
        const enabledWf = getEnabledWorkflows();
        const enabledTools = getTrackedToolNames();
        return {
          mode: 'dynamic',
          enabledWorkflows: enabledWf,
          enabledTools,
          totalRegistered: enabledTools.length,
        };
      }

      // Static mode: all tools are registered
      const workflows = await loadWorkflowGroups();
      const enabledWorkflows = Array.from(workflows.keys());
      const plugins = await loadPlugins();
      const enabledTools = Array.from(plugins.keys());
      return {
        mode: 'static',
        enabledWorkflows,
        enabledTools,
        totalRegistered: enabledTools.length,
      };
    },
  };

  const features: FeatureDetector = {
    areAxeToolsAvailable,
    isXcodemakeEnabled,
    isXcodemakeAvailable,
    doesMakefileExist,
  };

  return { binaryChecker, xcode, env, plugins, runtime, features };
}

export type { CommandExecutor };

export default {} as const;

```

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

```typescript
/**
 * macOS Shared Plugin: Build and Run macOS (Unified)
 *
 * Builds and runs a macOS app from a project or workspace in one step.
 * Accepts mutually exclusive `projectPath` or `workspacePath`.
 */

import { z } from 'zod';
import { log } from '../../../utils/logging/index.ts';
import { createTextResponse } from '../../../utils/responses/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 } from '../../../utils/typed-tool-factory.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';

// Unified schema: XOR between projectPath and workspacePath
const baseSchemaObject = z.object({
  projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
  workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
  scheme: z.string().describe('The scheme to use'),
  configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
  derivedDataPath: z
    .string()
    .optional()
    .describe('Path where build products and other derived data will go'),
  arch: z
    .enum(['arm64', 'x86_64'])
    .optional()
    .describe('Architecture to build for (arm64 or x86_64). For macOS only.'),
  extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'),
  preferXcodebuild: z
    .boolean()
    .optional()
    .describe('If true, prefers xcodebuild over the experimental incremental build system'),
});

const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);

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

const buildRunMacOSSchema = baseSchema
  .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 BuildRunMacOSParams = z.infer<typeof buildRunMacOSSchema>;

/**
 * Internal logic for building macOS apps.
 */
async function _handleMacOSBuildLogic(
  params: BuildRunMacOSParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  log('info', `Starting macOS build for scheme ${params.scheme} (internal)`);

  return executeXcodeBuildCommand(
    {
      ...params,
      configuration: params.configuration ?? 'Debug',
    },
    {
      platform: XcodePlatform.macOS,
      arch: params.arch,
      logPrefix: 'macOS Build',
    },
    params.preferXcodebuild ?? false,
    'build',
    executor,
  );
}

async function _getAppPathFromBuildSettings(
  params: BuildRunMacOSParams,
  executor: CommandExecutor,
): Promise<{ success: true; appPath: string } | { success: false; error: string }> {
  try {
    // Create the command array for xcodebuild
    const command = ['xcodebuild', '-showBuildSettings'];

    // Add the project or workspace
    if (params.projectPath) {
      command.push('-project', params.projectPath);
    } else if (params.workspacePath) {
      command.push('-workspace', params.workspacePath);
    }

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

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

    // Add extra args if provided
    if (params.extraArgs && params.extraArgs.length > 0) {
      command.push(...params.extraArgs);
    }

    // Execute the command directly
    const result = await executor(command, 'Get Build Settings for Launch', true, undefined);

    if (!result.success) {
      return {
        success: false,
        error: result.error ?? 'Failed to get build settings',
      };
    }

    // Parse the output to extract the app path
    const buildSettingsOutput = result.output;
    const builtProductsDirMatch = buildSettingsOutput.match(/^\s*BUILT_PRODUCTS_DIR\s*=\s*(.+)$/m);
    const fullProductNameMatch = buildSettingsOutput.match(/^\s*FULL_PRODUCT_NAME\s*=\s*(.+)$/m);

    if (!builtProductsDirMatch || !fullProductNameMatch) {
      return { success: false, error: 'Could not extract app path from build settings' };
    }

    const appPath = `${builtProductsDirMatch[1].trim()}/${fullProductNameMatch[1].trim()}`;
    return { success: true, appPath };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    return { success: false, error: errorMessage };
  }
}

/**
 * Business logic for building and running macOS apps.
 */
export async function buildRunMacOSLogic(
  params: BuildRunMacOSParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  log('info', 'Handling macOS build & run logic...');

  try {
    // First, build the app
    const buildResult = await _handleMacOSBuildLogic(params, executor);

    // 1. Check if the build itself failed
    if (buildResult.isError) {
      return buildResult; // Return build failure directly
    }
    const buildWarningMessages = buildResult.content?.filter((c) => c.type === 'text') ?? [];

    // 2. Build succeeded, now get the app path using the helper
    const appPathResult = await _getAppPathFromBuildSettings(params, executor);

    // 3. Check if getting the app path failed
    if (!appPathResult.success) {
      log('error', 'Build succeeded, but failed to get app path to launch.');
      const response = createTextResponse(
        `✅ Build succeeded, but failed to get app path to launch: ${appPathResult.error}`,
        false, // Build succeeded, so not a full error
      );
      if (response.content) {
        response.content.unshift(...buildWarningMessages);
      }
      return response;
    }

    const appPath = appPathResult.appPath; // success === true narrows to string
    log('info', `App path determined as: ${appPath}`);

    // 4. Launch the app using CommandExecutor
    const launchResult = await executor(['open', appPath], 'Launch macOS App', true);

    if (!launchResult.success) {
      log('error', `Build succeeded, but failed to launch app ${appPath}: ${launchResult.error}`);
      const errorResponse = createTextResponse(
        `✅ Build succeeded, but failed to launch app ${appPath}. Error: ${launchResult.error}`,
        false, // Build succeeded
      );
      if (errorResponse.content) {
        errorResponse.content.unshift(...buildWarningMessages);
      }
      return errorResponse;
    }

    log('info', `✅ macOS app launched successfully: ${appPath}`);
    const successResponse: ToolResponse = {
      content: [
        ...buildWarningMessages,
        {
          type: 'text',
          text: `✅ macOS build and run succeeded for scheme ${params.scheme}. App launched: ${appPath}`,
        },
      ],
      isError: false,
    };
    return successResponse;
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error during macOS build & run logic: ${errorMessage}`);
    const errorResponse = createTextResponse(
      `Error during macOS build and run: ${errorMessage}`,
      true,
    );
    return errorResponse;
  }
}

export default {
  name: 'build_run_macos',
  description: 'Builds and runs a macOS app.',
  schema: publicSchemaObject.shape,
  handler: createSessionAwareTool<BuildRunMacOSParams>({
    internalSchema: buildRunMacOSSchema as unknown as z.ZodType<BuildRunMacOSParams>,
    logicFunction: buildRunMacOSLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { allOf: ['scheme'], message: 'scheme is required' },
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
    ],
    exclusivePairs: [['projectPath', 'workspacePath']],
  }),
};

```

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  return content;
}

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

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

  const content = `# XcodeBuildMCP Tools Reference

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

## Workflow Groups

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

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

---

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

  return content;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      return;
    }

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

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

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

// Run the updater
main();

```

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

```markdown
# XcodeBuildMCP Code Quality Guide

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

## Table of Contents

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

## Overview

XcodeBuildMCP enforces code quality through multiple layers:

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

## ESLint Configuration

### Current Configuration

The project uses a comprehensive ESLint setup that covers:

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

### ESLint Rules

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

### Running ESLint

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

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

## Architectural Rules

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

### 1. Dependency Injection Pattern

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

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

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

### 2. Handler Signature Compliance

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

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

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

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

### 3. Testing Architecture

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

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

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

### 4. Server Type Safety

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

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

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

## Development Scripts

### Core Scripts

```bash
# Build the project
npm run build

# Run type checking
npm run typecheck

# Run tests
npm run test

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

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

### Pattern Checker Usage

The pattern checker enforces XcodeBuildMCP-specific architectural rules:

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

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

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

### Tool Summary Scripts

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

# List all tools
npm run tools:list

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

## Code Pattern Violations

The pattern checker identifies the following violations:

### 1. Vitest Mocking Violations

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

### 2. ExecSync Violations

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

### 3. Handler Signature Violations

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

### 4. Handler Testing Violations

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

### 5. Improper Server Typing Violations

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

## Type Safety Migration

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

### Check Migration Status

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

# Show detailed analysis
npm run check-migration:verbose

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

### Migration Benefits

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

## Best Practices

### 1. Before Committing

Always run these checks before committing:

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

### 2. Adding New Tools

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

### 3. Writing Tests

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

### 4. Code Organization

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

## Automated Enforcement

The project uses multiple layers of automated enforcement:

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

## Troubleshooting

### ESLint False Positives

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

### Pattern Checker Issues

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

### Type Safety Migration

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

## Future Improvements

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

--------------------------------------------------------------------------------
/.github/workflows/droid-code-review.yml:
--------------------------------------------------------------------------------

```yaml
name: Droid Code Review

on:
  pull_request:
    types: [opened, synchronize, reopened, ready_for_review]

concurrency:
  group: droid-review-${{ github.event.pull_request.number }}
  cancel-in-progress: true

permissions:
  pull-requests: write
  contents: read
  issues: write

jobs:
  code-review:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    # Skip automated code review for draft PRs
    if: github.event.pull_request.draft == false

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          ref: ${{ github.event.pull_request.head.sha }}

      - name: Install Droid CLI
        run: |
          curl -fsSL https://app.factory.ai/cli | sh
          echo "$HOME/.local/bin" >> $GITHUB_PATH
          "$HOME/.local/bin/droid" --version

      - name: Configure git identity
        run: |
          git config user.name "Droid Agent"
          git config user.email "[email protected]"

      - name: Prepare review context
        run: |
          # Get the PR diff
          git fetch origin ${{ github.event.pull_request.base.ref }}
          git diff origin/${{ github.event.pull_request.base.ref }}...${{ github.event.pull_request.head.sha }} > diff.txt

          # Get existing comments using GitHub API
          curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
               -H "Accept: application/vnd.github.v3+json" \
               "https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \
               > existing_comments.json

          # Get changed files with patches for positioning
          curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
               -H "Accept: application/vnd.github.v3+json" \
               "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \
               | jq '[.[] | {filename: .filename, patch: .patch}]' > files.json

      - name: Perform automated code review
        env:
          FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          cat > prompt.txt << 'EOF'
          You are an automated code review system. Review the PR diff and identify clear issues that need to be fixed.

          Input files (already in current directory):
          - diff.txt: the code changes to review
          - files.json: file patches with line numbers for positioning comments
          - existing_comments.json: skip issues already mentioned here

          Task: Create a file called comments.json with this exact format:
          [{ "path": "path/to/file.js", "position": 42, "body": "Your comment here" }]

          Focus on these types of issues:
          - Dead/unreachable code (if (false), while (false), code after return/throw/break)
          - Broken control flow (missing break in switch, fallthrough bugs)
          - Async/await mistakes (missing await, .then without return, unhandled promise rejections)
          - Array/object mutations in React components or reducers
          - UseEffect dependency array problems (missing deps, incorrect deps)
          - Incorrect operator usage (== vs ===, && vs ||, = in conditions)
          - Off-by-one errors in loops or array indexing
          - Integer overflow/underflow in calculations
          - Regex catastrophic backtracking vulnerabilities
          - Missing base cases in recursive functions
          - Incorrect type coercion that changes behavior
          - Environment variable access without defaults or validation
          - Null/undefined dereferences
          - Resource leaks (unclosed files or connections)
          - SQL/XSS injection vulnerabilities
          - Concurrency/race conditions
          - Missing error handling for critical operations

          Comment format:
          - Clearly describe the issue: "This code block is unreachable due to the if (false) condition"
          - Provide a concrete fix: "Remove this entire if block as it will never execute"
          - When possible, suggest the exact code change:
            ```suggestion
            // Remove the unreachable code
            ```
          - Be specific about why it's a problem: "This will cause a TypeError if input is null"
          - No emojis, just clear technical language

          Skip commenting on:
          - Code style, formatting, or naming conventions
          - Minor performance optimizations
          - Architectural decisions or design patterns
          - Features or functionality (unless broken)
          - Test coverage (unless tests are clearly broken)

          Position calculation:
          - Use the "position" field from files.json patches
          - This is the line number in the diff, not the file
          - Comments must align with exact changed lines only

          Output: 
          - Empty array [] if no issues found
          - Otherwise array of comment objects with path, position, body
          - Each comment should be actionable and clear about what needs to be fixed
          - Maximum 10 comments total; prioritize the most critical issues
          EOF

          # Run droid exec with the prompt
          echo "Running code review analysis..."
          droid exec --auto high -f prompt.txt

          # Check if comments.json was created
          if [ ! -f comments.json ]; then
            echo "❌ ERROR: droid exec did not create comments.json"
            echo "This usually indicates the review run failed (e.g. missing FACTORY_API_KEY or runtime error)."
            exit 1
          fi

          echo "=== Review Results ==="
          cat comments.json

      - name: Submit inline review comments
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const prNumber = context.payload.pull_request.number;

            if (!fs.existsSync('comments.json')) {
              core.info('comments.json missing; skipping review submission');
              return;
            }

            const comments = JSON.parse(fs.readFileSync('comments.json', 'utf8'));

            if (!Array.isArray(comments) || comments.length === 0) {
              // Check if we already have a "no issues" comment
              const existing = await github.paginate(github.rest.issues.listComments, {
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: prNumber,
                per_page: 100
              });
              
              const hasNoIssuesComment = existing.some(c => 
                c.user.login.includes('[bot]') && 
                /no issues found|lgtm|✅/i.test(c.body || '')
              );
              
              if (!hasNoIssuesComment) {
                await github.rest.pulls.createReview({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  pull_number: prNumber,
                  event: 'COMMENT',
                  body: '✅ No issues found in the current changes.'
                });
              }
              return;
            }

            // Submit review with inline comments
            const summary = `Found ${comments.length} potential issue${comments.length === 1 ? '' : 's'} that should be addressed.`;

            await github.rest.pulls.createReview({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: prNumber,
              event: 'COMMENT',
              body: summary,
              comments: comments
            });

            core.info(`Submitted review with ${comments.length} inline comments`);

      - name: Upload debug artifacts on failure
        if: ${{ failure() }}
        uses: actions/upload-artifact@v4
        with:
          name: droid-review-debug-${{ github.run_id }}
          path: |
            diff.txt
            files.json
            existing_comments.json
            prompt.txt
            comments.json
            ${{ runner.home }}/.factory/logs/droid-log-single.log
            ${{ runner.home }}/.factory/logs/console.log
          if-no-files-found: ignore
          retention-days: 7

```

--------------------------------------------------------------------------------
/src/mcp/tools/device/__tests__/stop_app_device.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tests for stop_app_device plugin (device-shared)
 * Following CLAUDE.md testing standards with literal validation
 * Using dependency injection for deterministic testing
 */

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

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

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

    it('should have correct description', () => {
      expect(stopAppDevice.description).toBe('Stops a running app on a connected device.');
    });

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

    it('should require processId in public schema', () => {
      const schema = z.object(stopAppDevice.schema).strict();
      expect(schema.safeParse({ processId: 12345 }).success).toBe(true);
      expect(schema.safeParse({}).success).toBe(false);
      expect(schema.safeParse({ deviceId: 'test-device-123' }).success).toBe(false);

      expect(Object.keys(stopAppDevice.schema)).toEqual(['processId']);
    });
  });

  describe('Handler Requirements', () => {
    it('should require deviceId when not provided', async () => {
      const result = await stopAppDevice.handler({ processId: 12345 });

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

  describe('Command Generation', () => {
    it('should generate correct devicectl command with basic parameters', async () => {
      let capturedCommand: unknown[] = [];
      let capturedDescription: string = '';
      let capturedUseShell: boolean = false;
      let capturedEnv: unknown = undefined;

      const mockExecutor = createMockExecutor({
        success: true,
        output: 'App terminated successfully',
        process: { pid: 12345 },
      });

      const trackingExecutor = async (
        command: unknown[],
        description: string,
        useShell: boolean,
        env: unknown,
      ) => {
        capturedCommand = command;
        capturedDescription = description;
        capturedUseShell = useShell;
        capturedEnv = env;
        return mockExecutor(command, description, useShell, env);
      };

      await stop_app_deviceLogic(
        {
          deviceId: 'test-device-123',
          processId: 12345,
        },
        trackingExecutor,
      );

      expect(capturedCommand).toEqual([
        'xcrun',
        'devicectl',
        'device',
        'process',
        'terminate',
        '--device',
        'test-device-123',
        '--pid',
        '12345',
      ]);
      expect(capturedDescription).toBe('Stop app on device');
      expect(capturedUseShell).toBe(true);
      expect(capturedEnv).toBe(undefined);
    });

    it('should generate correct command with different device ID and process ID', async () => {
      let capturedCommand: unknown[] = [];

      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Process terminated',
        process: { pid: 12345 },
      });

      const trackingExecutor = async (command: unknown[]) => {
        capturedCommand = command;
        return mockExecutor(command);
      };

      await stop_app_deviceLogic(
        {
          deviceId: 'different-device-uuid',
          processId: 99999,
        },
        trackingExecutor,
      );

      expect(capturedCommand).toEqual([
        'xcrun',
        'devicectl',
        'device',
        'process',
        'terminate',
        '--device',
        'different-device-uuid',
        '--pid',
        '99999',
      ]);
    });

    it('should generate correct command with large process ID', async () => {
      let capturedCommand: unknown[] = [];

      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Process terminated',
        process: { pid: 12345 },
      });

      const trackingExecutor = async (command: unknown[]) => {
        capturedCommand = command;
        return mockExecutor(command);
      };

      await stop_app_deviceLogic(
        {
          deviceId: 'test-device-123',
          processId: 2147483647,
        },
        trackingExecutor,
      );

      expect(capturedCommand).toEqual([
        'xcrun',
        'devicectl',
        'device',
        'process',
        'terminate',
        '--device',
        'test-device-123',
        '--pid',
        '2147483647',
      ]);
    });
  });

  describe('Success Path Tests', () => {
    it('should return successful stop response', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'App terminated successfully',
      });

      const result = await stop_app_deviceLogic(
        {
          deviceId: 'test-device-123',
          processId: 12345,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '✅ App stopped successfully\n\nApp terminated successfully',
          },
        ],
      });
    });

    it('should return successful stop with detailed output', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Terminating process...\nProcess ID: 12345\nTermination completed successfully',
      });

      const result = await stop_app_deviceLogic(
        {
          deviceId: 'device-456',
          processId: 67890,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '✅ App stopped successfully\n\nTerminating process...\nProcess ID: 12345\nTermination completed successfully',
          },
        ],
      });
    });

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

      const result = await stop_app_deviceLogic(
        {
          deviceId: 'empty-output-device',
          processId: 54321,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '✅ App stopped successfully\n\n',
          },
        ],
      });
    });
  });

  describe('Error Handling', () => {
    it('should return stop failure response', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Terminate failed: Process not found',
      });

      const result = await stop_app_deviceLogic(
        {
          deviceId: 'test-device-123',
          processId: 99999,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Failed to stop app: Terminate failed: Process not found',
          },
        ],
        isError: true,
      });
    });

    it('should return exception handling response', async () => {
      const mockExecutor = createMockExecutor(new Error('Network error'));

      const result = await stop_app_deviceLogic(
        {
          deviceId: 'test-device-123',
          processId: 12345,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Failed to stop app on device: Network error',
          },
        ],
        isError: true,
      });
    });

    it('should return string error handling response', async () => {
      const mockExecutor = createMockExecutor('String error');

      const result = await stop_app_deviceLogic(
        {
          deviceId: 'test-device-123',
          processId: 12345,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Failed to stop app on device: String error',
          },
        ],
        isError: true,
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/src/utils/test-common.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Common Test Utilities - Shared logic for test tools
 *
 * This module provides shared functionality for all test-related tools across different platforms.
 * It includes common test execution logic, xcresult parsing, and utility functions used by
 * platform-specific test tools.
 *
 * Responsibilities:
 * - Parsing xcresult bundles into human-readable format
 * - Shared test execution logic with platform-specific handling
 * - Common error handling and cleanup for test operations
 * - Temporary directory management for xcresult files
 */

import { promisify } from 'util';
import { exec } from 'child_process';
import { mkdtemp, rm } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
import { log } from './logger.ts';
import { XcodePlatform } from './xcode.ts';
import { executeXcodeBuildCommand } from './build/index.ts';
import { createTextResponse, consolidateContentForClaudeCode } from './validation.ts';
import { normalizeTestRunnerEnv } from './environment.ts';
import { ToolResponse } from '../types/common.ts';
import { CommandExecutor, CommandExecOptions, getDefaultCommandExecutor } from './command.ts';

/**
 * Type definition for test summary structure from xcresulttool
 */
interface TestSummary {
  title?: string;
  result?: string;
  totalTestCount?: number;
  passedTests?: number;
  failedTests?: number;
  skippedTests?: number;
  expectedFailures?: number;
  environmentDescription?: string;
  devicesAndConfigurations?: Array<{
    device?: {
      deviceName?: string;
      platform?: string;
      osVersion?: string;
    };
  }>;
  testFailures?: Array<{
    testName?: string;
    targetName?: string;
    failureText?: string;
  }>;
  topInsights?: Array<{
    impact?: string;
    text?: string;
  }>;
}

/**
 * Parse xcresult bundle using xcrun xcresulttool
 */
export async function parseXcresultBundle(resultBundlePath: string): Promise<string> {
  try {
    const execAsync = promisify(exec);
    const { stdout } = await execAsync(
      `xcrun xcresulttool get test-results summary --path "${resultBundlePath}"`,
    );

    // Parse JSON response and format as human-readable
    const summary = JSON.parse(stdout) as TestSummary;
    return formatTestSummary(summary);
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error parsing xcresult bundle: ${errorMessage}`);
    throw error;
  }
}

/**
 * Format test summary JSON into human-readable text
 */
function formatTestSummary(summary: TestSummary): string {
  const lines: string[] = [];

  lines.push(`Test Summary: ${summary.title ?? 'Unknown'}`);
  lines.push(`Overall Result: ${summary.result ?? 'Unknown'}`);
  lines.push('');

  lines.push('Test Counts:');
  lines.push(`  Total: ${summary.totalTestCount ?? 0}`);
  lines.push(`  Passed: ${summary.passedTests ?? 0}`);
  lines.push(`  Failed: ${summary.failedTests ?? 0}`);
  lines.push(`  Skipped: ${summary.skippedTests ?? 0}`);
  lines.push(`  Expected Failures: ${summary.expectedFailures ?? 0}`);
  lines.push('');

  if (summary.environmentDescription) {
    lines.push(`Environment: ${summary.environmentDescription}`);
    lines.push('');
  }

  if (
    summary.devicesAndConfigurations &&
    Array.isArray(summary.devicesAndConfigurations) &&
    summary.devicesAndConfigurations.length > 0
  ) {
    const device = summary.devicesAndConfigurations[0].device;
    if (device) {
      lines.push(
        `Device: ${device.deviceName ?? 'Unknown'} (${device.platform ?? 'Unknown'} ${device.osVersion ?? 'Unknown'})`,
      );
      lines.push('');
    }
  }

  if (
    summary.testFailures &&
    Array.isArray(summary.testFailures) &&
    summary.testFailures.length > 0
  ) {
    lines.push('Test Failures:');
    summary.testFailures.forEach((failure, index: number) => {
      lines.push(
        `  ${index + 1}. ${failure.testName ?? 'Unknown Test'} (${failure.targetName ?? 'Unknown Target'})`,
      );
      if (failure.failureText) {
        lines.push(`     ${failure.failureText}`);
      }
    });
    lines.push('');
  }

  if (summary.topInsights && Array.isArray(summary.topInsights) && summary.topInsights.length > 0) {
    lines.push('Insights:');
    summary.topInsights.forEach((insight, index: number) => {
      lines.push(
        `  ${index + 1}. [${insight.impact ?? 'Unknown'}] ${insight.text ?? 'No description'}`,
      );
    });
  }

  return lines.join('\n');
}

/**
 * Internal logic for running tests with platform-specific handling
 */
export async function handleTestLogic(
  params: {
    workspacePath?: string;
    projectPath?: string;
    scheme: string;
    configuration: string;
    simulatorName?: string;
    simulatorId?: string;
    deviceId?: string;
    useLatestOS?: boolean;
    derivedDataPath?: string;
    extraArgs?: string[];
    preferXcodebuild?: boolean;
    platform: XcodePlatform;
    testRunnerEnv?: Record<string, string>;
  },
  executor?: CommandExecutor,
): Promise<ToolResponse> {
  log(
    'info',
    `Starting test run for scheme ${params.scheme} on platform ${params.platform} (internal)`,
  );

  try {
    // Create temporary directory for xcresult bundle
    const tempDir = await mkdtemp(join(tmpdir(), 'xcodebuild-test-'));
    const resultBundlePath = join(tempDir, 'TestResults.xcresult');

    // Add resultBundlePath to extraArgs
    const extraArgs = [...(params.extraArgs ?? []), `-resultBundlePath`, resultBundlePath];

    // Prepare execution options with TEST_RUNNER_ environment variables
    const execOpts: CommandExecOptions | undefined = params.testRunnerEnv
      ? { env: normalizeTestRunnerEnv(params.testRunnerEnv) }
      : undefined;

    // Run the test command
    const testResult = await executeXcodeBuildCommand(
      {
        ...params,
        extraArgs,
      },
      {
        platform: params.platform,
        simulatorName: params.simulatorName,
        simulatorId: params.simulatorId,
        deviceId: params.deviceId,
        useLatestOS: params.useLatestOS,
        logPrefix: 'Test Run',
      },
      params.preferXcodebuild,
      'test',
      executor ?? getDefaultCommandExecutor(),
      execOpts,
    );

    // Parse xcresult bundle if it exists, regardless of whether tests passed or failed
    // Test failures are expected and should not prevent xcresult parsing
    try {
      log('info', `Attempting to parse xcresult bundle at: ${resultBundlePath}`);

      // Check if the file exists
      try {
        const { stat } = await import('fs/promises');
        await stat(resultBundlePath);
        log('info', `xcresult bundle exists at: ${resultBundlePath}`);
      } catch {
        log('warn', `xcresult bundle does not exist at: ${resultBundlePath}`);
        throw new Error(`xcresult bundle not found at ${resultBundlePath}`);
      }

      const testSummary = await parseXcresultBundle(resultBundlePath);
      log('info', 'Successfully parsed xcresult bundle');

      // Clean up temporary directory
      await rm(tempDir, { recursive: true, force: true });

      // Return combined result - preserve isError from testResult (test failures should be marked as errors)
      const combinedResponse: ToolResponse = {
        content: [
          ...(testResult.content || []),
          {
            type: 'text',
            text: '\nTest Results Summary:\n' + testSummary,
          },
        ],
        isError: testResult.isError,
      };

      // Apply Claude Code workaround if enabled
      return consolidateContentForClaudeCode(combinedResponse);
    } catch (parseError) {
      // If parsing fails, return original test result
      log('warn', `Failed to parse xcresult bundle: ${parseError}`);

      // Clean up temporary directory even if parsing fails
      try {
        await rm(tempDir, { recursive: true, force: true });
      } catch (cleanupError) {
        log('warn', `Failed to clean up temporary directory: ${cleanupError}`);
      }

      return consolidateContentForClaudeCode(testResult);
    }
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error during test run: ${errorMessage}`);
    return consolidateContentForClaudeCode(
      createTextResponse(`Error during test run: ${errorMessage}`, true),
    );
  }
}

```
Page 4/11FirstPrevNextLast