#
tokens: 48988/50000 10/337 files (page 9/11)
lines: off (toggle) GitHub
raw markdown copy
This is page 9 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/logging/__tests__/start_device_log_cap.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tests for start_device_log_cap plugin
 * Following CLAUDE.md testing standards with pure dependency injection
 */
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { EventEmitter } from 'events';
import type { ChildProcess } from 'child_process';
import { z } from 'zod';
import {
  createMockExecutor,
  createMockFileSystemExecutor,
} from '../../../../test-utils/mock-executors.ts';
import plugin, {
  start_device_log_capLogic,
  activeDeviceLogSessions,
} from '../start_device_log_cap.ts';
import { sessionStore } from '../../../../utils/session-store.ts';

describe('start_device_log_cap plugin', () => {
  // Mock state tracking
  let commandCalls: Array<{
    command: string[];
    logPrefix?: string;
    useShell?: boolean;
    env?: Record<string, string>;
  }> = [];
  let mkdirCalls: string[] = [];
  let writeFileCalls: Array<{ path: string; content: string }> = [];

  // Reset state
  commandCalls = [];
  mkdirCalls = [];
  writeFileCalls = [];

  const originalJsonWaitEnv = process.env.XBMCP_LAUNCH_JSON_WAIT_MS;

  beforeEach(() => {
    sessionStore.clear();
    activeDeviceLogSessions.clear();
    process.env.XBMCP_LAUNCH_JSON_WAIT_MS = '25';
  });

  afterEach(() => {
    if (originalJsonWaitEnv === undefined) {
      delete process.env.XBMCP_LAUNCH_JSON_WAIT_MS;
    } else {
      process.env.XBMCP_LAUNCH_JSON_WAIT_MS = originalJsonWaitEnv;
    }
  });

  describe('Plugin Structure', () => {
    it('should export an object with required properties', () => {
      expect(plugin).toHaveProperty('name');
      expect(plugin).toHaveProperty('description');
      expect(plugin).toHaveProperty('schema');
      expect(plugin).toHaveProperty('handler');
    });

    it('should have correct tool name', () => {
      expect(plugin.name).toBe('start_device_log_cap');
    });

    it('should have correct description', () => {
      expect(plugin.description).toBe('Starts log capture on a connected device.');
    });

    it('should have correct schema structure', () => {
      // Schema should be a plain object for MCP protocol compliance
      expect(typeof plugin.schema).toBe('object');
      expect(Object.keys(plugin.schema)).toEqual(['bundleId']);

      // Validate that schema fields are Zod types that can be used for validation
      const schema = z.object(plugin.schema).strict();
      expect(schema.safeParse({ bundleId: 'com.test.app' }).success).toBe(true);
      expect(schema.safeParse({}).success).toBe(false);
    });

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

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

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

  describe('Handler Functionality', () => {
    it('should start log capture successfully', async () => {
      // Mock successful command execution
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'App launched successfully',
      });

      const mockFileSystemExecutor = createMockFileSystemExecutor({
        mkdir: async (path: string) => {
          mkdirCalls.push(path);
        },
        writeFile: async (path: string, content: string) => {
          writeFileCalls.push({ path, content });
        },
      });

      const result = await start_device_log_capLogic(
        {
          deviceId: '00008110-001A2C3D4E5F',
          bundleId: 'com.example.MyApp',
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result.content[0].text).toMatch(/✅ Device log capture started successfully/);
      expect(result.content[0].text).toMatch(/Session ID: [a-f0-9-]{36}/);
      expect(result.isError ?? false).toBe(false);
    });

    it('should include next steps in success response', async () => {
      // Mock successful command execution
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'App launched successfully',
      });

      const mockFileSystemExecutor = createMockFileSystemExecutor({
        mkdir: async (path: string) => {
          mkdirCalls.push(path);
        },
        writeFile: async (path: string, content: string) => {
          writeFileCalls.push({ path, content });
        },
      });

      const result = await start_device_log_capLogic(
        {
          deviceId: '00008110-001A2C3D4E5F',
          bundleId: 'com.example.MyApp',
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result.content[0].text).toContain('Next Steps:');
      expect(result.content[0].text).toContain('Use stop_device_log_cap');
    });

    it('should surface early launch failures when process exits immediately', async () => {
      const failingProcess = new EventEmitter() as unknown as ChildProcess & {
        exitCode: number | null;
        killed: boolean;
        kill(signal?: string): boolean;
        stdout: NodeJS.ReadableStream & { setEncoding?: (encoding: string) => void };
        stderr: NodeJS.ReadableStream & { setEncoding?: (encoding: string) => void };
      };

      const stubOutput = new EventEmitter() as NodeJS.ReadableStream & {
        setEncoding?: (encoding: string) => void;
      };
      stubOutput.setEncoding = () => {};
      const stubError = new EventEmitter() as NodeJS.ReadableStream & {
        setEncoding?: (encoding: string) => void;
      };
      stubError.setEncoding = () => {};

      failingProcess.stdout = stubOutput;
      failingProcess.stderr = stubError;
      failingProcess.exitCode = null;
      failingProcess.killed = false;
      failingProcess.kill = () => {
        failingProcess.killed = true;
        failingProcess.exitCode = 0;
        failingProcess.emit('close', 0, null);
        return true;
      };

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

      let createdLogPath = '';
      const mockFileSystemExecutor = createMockFileSystemExecutor({
        mkdir: async () => {},
        writeFile: async (path: string, content: string) => {
          createdLogPath = path;
          writeFileCalls.push({ path, content });
        },
      });

      const resultPromise = start_device_log_capLogic(
        {
          deviceId: '00008110-001A2C3D4E5F',
          bundleId: 'com.invalid.App',
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      setTimeout(() => {
        stubError.emit(
          'data',
          'ERROR: The application failed to launch. (com.apple.dt.CoreDeviceError error 10002)\nNSLocalizedRecoverySuggestion = Provide a valid bundle identifier.\n',
        );
        failingProcess.exitCode = 70;
        failingProcess.emit('close', 70, null);
      }, 10);

      const result = await resultPromise;

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Provide a valid bundle identifier');
      expect(activeDeviceLogSessions.size).toBe(0);
      expect(createdLogPath).not.toBe('');
    });

    it('should surface JSON-reported failures when launch cannot start', async () => {
      const jsonFailure = {
        error: {
          domain: 'com.apple.dt.CoreDeviceError',
          code: 10002,
          localizedDescription: 'The application failed to launch.',
          userInfo: {
            NSLocalizedRecoverySuggestion: 'Provide a valid bundle identifier.',
            NSLocalizedFailureReason: 'The requested application com.invalid.App is not installed.',
            BundleIdentifier: 'com.invalid.App',
          },
        },
      };

      const failingProcess = new EventEmitter() as unknown as ChildProcess & {
        exitCode: number | null;
        killed: boolean;
        kill(signal?: string): boolean;
        stdout: NodeJS.ReadableStream & { setEncoding?: (encoding: string) => void };
        stderr: NodeJS.ReadableStream & { setEncoding?: (encoding: string) => void };
      };

      const stubOutput = new EventEmitter() as NodeJS.ReadableStream & {
        setEncoding?: (encoding: string) => void;
      };
      stubOutput.setEncoding = () => {};
      const stubError = new EventEmitter() as NodeJS.ReadableStream & {
        setEncoding?: (encoding: string) => void;
      };
      stubError.setEncoding = () => {};

      failingProcess.stdout = stubOutput;
      failingProcess.stderr = stubError;
      failingProcess.exitCode = null;
      failingProcess.killed = false;
      failingProcess.kill = () => {
        failingProcess.killed = true;
        return true;
      };

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

      let jsonPathSeen = '';
      let removedJsonPath = '';

      const mockFileSystemExecutor = createMockFileSystemExecutor({
        mkdir: async () => {},
        writeFile: async () => {},
        existsSync: (filePath: string): boolean => {
          if (filePath.includes('devicectl-launch-')) {
            jsonPathSeen = filePath;
            return true;
          }
          return false;
        },
        readFile: async (filePath: string): Promise<string> => {
          if (filePath.includes('devicectl-launch-')) {
            jsonPathSeen = filePath;
            return JSON.stringify(jsonFailure);
          }
          return '';
        },
        rm: async (filePath: string) => {
          if (filePath.includes('devicectl-launch-')) {
            removedJsonPath = filePath;
          }
        },
      });

      setTimeout(() => {
        failingProcess.exitCode = 0;
        failingProcess.emit('close', 0, null);
      }, 5);

      const result = await start_device_log_capLogic(
        {
          deviceId: '00008110-001A2C3D4E5F',
          bundleId: 'com.invalid.App',
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Provide a valid bundle identifier');
      expect(jsonPathSeen).not.toBe('');
      expect(removedJsonPath).toBe(jsonPathSeen);
      expect(activeDeviceLogSessions.size).toBe(0);
      expect(failingProcess.killed).toBe(true);
    });

    it('should treat JSON success payload as confirmation of launch', async () => {
      const jsonSuccess = {
        result: {
          process: {
            processIdentifier: 4321,
          },
        },
      };

      const runningProcess = new EventEmitter() as unknown as ChildProcess & {
        exitCode: number | null;
        killed: boolean;
        kill(signal?: string): boolean;
        stdout: NodeJS.ReadableStream & { setEncoding?: (encoding: string) => void };
        stderr: NodeJS.ReadableStream & { setEncoding?: (encoding: string) => void };
      };

      const stubOutput = new EventEmitter() as NodeJS.ReadableStream & {
        setEncoding?: (encoding: string) => void;
      };
      stubOutput.setEncoding = () => {};
      const stubError = new EventEmitter() as NodeJS.ReadableStream & {
        setEncoding?: (encoding: string) => void;
      };
      stubError.setEncoding = () => {};

      runningProcess.stdout = stubOutput;
      runningProcess.stderr = stubError;
      runningProcess.exitCode = null;
      runningProcess.killed = false;
      runningProcess.kill = () => {
        runningProcess.killed = true;
        runningProcess.emit('close', 0, null);
        return true;
      };

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

      let jsonPathSeen = '';
      let removedJsonPath = '';
      let jsonRemoved = false;

      const mockFileSystemExecutor = createMockFileSystemExecutor({
        mkdir: async () => {},
        writeFile: async () => {},
        existsSync: (filePath: string): boolean => {
          if (filePath.includes('devicectl-launch-')) {
            jsonPathSeen = filePath;
            return !jsonRemoved;
          }
          return false;
        },
        readFile: async (filePath: string): Promise<string> => {
          if (filePath.includes('devicectl-launch-')) {
            jsonPathSeen = filePath;
            return JSON.stringify(jsonSuccess);
          }
          return '';
        },
        rm: async (filePath: string) => {
          if (filePath.includes('devicectl-launch-')) {
            jsonRemoved = true;
            removedJsonPath = filePath;
          }
        },
      });

      setTimeout(() => {
        runningProcess.emit('close', 0, null);
      }, 5);

      const result = await start_device_log_capLogic(
        {
          deviceId: '00008110-001A2C3D4E5F',
          bundleId: 'com.example.MyApp',
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result.content[0].text).toContain('Device log capture started successfully');
      expect(result.isError ?? false).toBe(false);
      expect(jsonPathSeen).not.toBe('');
      expect(removedJsonPath).toBe(jsonPathSeen);
      expect(activeDeviceLogSessions.size).toBe(1);
    });

    it('should handle directory creation failure', async () => {
      // Mock mkdir to fail
      const mockExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'Command failed',
      });

      const mockFileSystemExecutor = createMockFileSystemExecutor({
        mkdir: async (path: string) => {
          mkdirCalls.push(path);
          throw new Error('Permission denied');
        },
      });

      const result = await start_device_log_capLogic(
        {
          deviceId: '00008110-001A2C3D4E5F',
          bundleId: 'com.example.MyApp',
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Failed to start device log capture: Permission denied',
          },
        ],
        isError: true,
      });
    });

    it('should handle file write failure', async () => {
      // Mock writeFile to fail
      const mockExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'Command failed',
      });

      const mockFileSystemExecutor = createMockFileSystemExecutor({
        mkdir: async (path: string) => {
          mkdirCalls.push(path);
        },
        writeFile: async (path: string, content: string) => {
          writeFileCalls.push({ path, content });
          throw new Error('Disk full');
        },
      });

      const result = await start_device_log_capLogic(
        {
          deviceId: '00008110-001A2C3D4E5F',
          bundleId: 'com.example.MyApp',
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Failed to start device log capture: Disk full',
          },
        ],
        isError: true,
      });
    });

    it('should handle spawn process error', async () => {
      // Mock spawn to throw error
      const mockExecutor = createMockExecutor(new Error('Command not found'));

      const mockFileSystemExecutor = createMockFileSystemExecutor({
        mkdir: async (path: string) => {
          mkdirCalls.push(path);
        },
        writeFile: async (path: string, content: string) => {
          writeFileCalls.push({ path, content });
        },
      });

      const result = await start_device_log_capLogic(
        {
          deviceId: '00008110-001A2C3D4E5F',
          bundleId: 'com.example.MyApp',
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Failed to start device log capture: Command not found',
          },
        ],
        isError: true,
      });
    });

    it('should handle string error objects', async () => {
      // Mock mkdir to fail with string error
      const mockExecutor = createMockExecutor('String error message');

      const mockFileSystemExecutor = createMockFileSystemExecutor({
        mkdir: async (path: string) => {
          mkdirCalls.push(path);
        },
        writeFile: async (path: string, content: string) => {
          writeFileCalls.push({ path, content });
        },
      });

      const result = await start_device_log_capLogic(
        {
          deviceId: '00008110-001A2C3D4E5F',
          bundleId: 'com.example.MyApp',
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Failed to start device log capture: String error message',
          },
        ],
        isError: true,
      });
    });
  });
});

```

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

```typescript
/**
 * Utilities Plugin: Scaffold iOS Project
 *
 * Scaffold a new iOS project from templates.
 */

import { z } from 'zod';
import { join, dirname, basename } from 'path';
import { log } from '../../../utils/logging/index.ts';
import { ValidationError } from '../../../utils/responses/index.ts';
import { TemplateManager } from '../../../utils/template/index.ts';
import type { CommandExecutor, FileSystemExecutor } from '../../../utils/execution/index.ts';
import {
  getDefaultCommandExecutor,
  getDefaultFileSystemExecutor,
} from '../../../utils/execution/index.ts';
import { ToolResponse } from '../../../types/common.ts';

// Common base schema for both iOS and macOS
const BaseScaffoldSchema = z.object({
  projectName: z.string().min(1).describe('Name of the new project'),
  outputPath: z.string().describe('Path where the project should be created'),
  bundleIdentifier: z
    .string()
    .optional()
    .describe(
      'Bundle identifier (e.g., com.example.myapp). If not provided, will use com.example.projectname',
    ),
  displayName: z
    .string()
    .optional()
    .describe(
      'App display name (shown on home screen/dock). If not provided, will use projectName',
    ),
  marketingVersion: z
    .string()
    .optional()
    .describe('Marketing version (e.g., 1.0, 2.1.3). If not provided, will use 1.0'),
  currentProjectVersion: z
    .string()
    .optional()
    .describe('Build number (e.g., 1, 42, 100). If not provided, will use 1'),
  customizeNames: z
    .boolean()
    .default(true)
    .describe('Whether to customize project names and identifiers. Default is true.'),
});

// iOS-specific schema
const ScaffoldiOSProjectSchema = BaseScaffoldSchema.extend({
  deploymentTarget: z
    .string()
    .optional()
    .describe('iOS deployment target (e.g., 18.4, 17.0). If not provided, will use 18.4'),
  targetedDeviceFamily: z
    .array(z.enum(['iphone', 'ipad', 'universal']))
    .optional()
    .describe('Targeted device families'),
  supportedOrientations: z
    .array(z.enum(['portrait', 'landscape-left', 'landscape-right', 'portrait-upside-down']))
    .optional()
    .describe('Supported orientations for iPhone'),
  supportedOrientationsIpad: z
    .array(z.enum(['portrait', 'landscape-left', 'landscape-right', 'portrait-upside-down']))
    .optional()
    .describe('Supported orientations for iPad'),
});

/**
 * Convert orientation enum to iOS constant
 */
function orientationToIOSConstant(orientation: string): string {
  switch (orientation) {
    case 'Portrait':
      return 'UIInterfaceOrientationPortrait';
    case 'PortraitUpsideDown':
      return 'UIInterfaceOrientationPortraitUpsideDown';
    case 'LandscapeLeft':
      return 'UIInterfaceOrientationLandscapeLeft';
    case 'LandscapeRight':
      return 'UIInterfaceOrientationLandscapeRight';
    default:
      return orientation;
  }
}

/**
 * Convert device family enum to numeric value
 */
function deviceFamilyToNumeric(family: string): string {
  switch (family) {
    case 'iPhone':
      return '1';
    case 'iPad':
      return '2';
    case 'iPhone+iPad':
      return '1,2';
    default:
      return '1,2';
  }
}

/**
 * Update Package.swift file with deployment target
 */
function updatePackageSwiftFile(content: string, params: Record<string, unknown>): string {
  let result = content;

  const projectName = params.projectName as string;
  const platform = params.platform as string;
  const deploymentTarget = params.deploymentTarget as string | undefined;

  // Update ALL target name references in Package.swift
  const featureName = `${projectName}Feature`;
  const testName = `${projectName}FeatureTests`;

  // Replace ALL occurrences of MyProjectFeatureTests first (more specific)
  result = result.replace(/MyProjectFeatureTests/g, testName);
  // Then replace ALL occurrences of MyProjectFeature (less specific, so comes after)
  result = result.replace(/MyProjectFeature/g, featureName);

  // Update deployment targets based on platform
  if (platform === 'iOS') {
    if (deploymentTarget) {
      // Extract major version (e.g., "17.0" -> "17")
      const majorVersion = deploymentTarget.split('.')[0];
      result = result.replace(/\.iOS\(\.v\d+\)/, `.iOS(.v${majorVersion})`);
    }
  }

  return result;
}

/**
 * Update XCConfig file with scaffold parameters
 */
function updateXCConfigFile(content: string, params: Record<string, unknown>): string {
  let result = content;

  const projectName = params.projectName as string;
  const displayName = params.displayName as string | undefined;
  const bundleIdentifier = params.bundleIdentifier as string | undefined;
  const marketingVersion = params.marketingVersion as string | undefined;
  const currentProjectVersion = params.currentProjectVersion as string | undefined;
  const platform = params.platform as string;
  const deploymentTarget = params.deploymentTarget as string | undefined;
  const targetedDeviceFamily = params.targetedDeviceFamily as string | undefined;
  const supportedOrientations = params.supportedOrientations as string[] | undefined;
  const supportedOrientationsIpad = params.supportedOrientationsIpad as string[] | undefined;

  // Update project identity settings
  result = result.replace(/PRODUCT_NAME = .+/g, `PRODUCT_NAME = ${projectName}`);
  result = result.replace(
    /PRODUCT_DISPLAY_NAME = .+/g,
    `PRODUCT_DISPLAY_NAME = ${displayName ?? projectName}`,
  );
  result = result.replace(
    /PRODUCT_BUNDLE_IDENTIFIER = .+/g,
    `PRODUCT_BUNDLE_IDENTIFIER = ${bundleIdentifier ?? `com.example.${projectName.toLowerCase().replace(/[^a-z0-9]/g, '')}`}`,
  );
  result = result.replace(
    /MARKETING_VERSION = .+/g,
    `MARKETING_VERSION = ${marketingVersion ?? '1.0'}`,
  );
  result = result.replace(
    /CURRENT_PROJECT_VERSION = .+/g,
    `CURRENT_PROJECT_VERSION = ${currentProjectVersion ?? '1'}`,
  );

  // Platform-specific updates
  if (platform === 'iOS') {
    // iOS deployment target
    if (deploymentTarget) {
      result = result.replace(
        /IPHONEOS_DEPLOYMENT_TARGET = .+/g,
        `IPHONEOS_DEPLOYMENT_TARGET = ${deploymentTarget}`,
      );
    }

    // Device family
    if (targetedDeviceFamily) {
      const deviceFamilyValue = deviceFamilyToNumeric(targetedDeviceFamily);
      result = result.replace(
        /TARGETED_DEVICE_FAMILY = .+/g,
        `TARGETED_DEVICE_FAMILY = ${deviceFamilyValue}`,
      );
    }

    // iPhone orientations
    if (supportedOrientations && supportedOrientations.length > 0) {
      // Filter out any empty strings and validate
      const validOrientations = supportedOrientations.filter((o: string) => o && o.trim() !== '');
      if (validOrientations.length > 0) {
        const orientations = validOrientations.map(orientationToIOSConstant).join(' ');
        result = result.replace(
          /INFOPLIST_KEY_UISupportedInterfaceOrientations = .+/g,
          `INFOPLIST_KEY_UISupportedInterfaceOrientations = ${orientations}`,
        );
      }
    }

    // iPad orientations
    if (supportedOrientationsIpad && supportedOrientationsIpad.length > 0) {
      // Filter out any empty strings and validate
      const validOrientations = supportedOrientationsIpad.filter(
        (o: string) => o && o.trim() !== '',
      );
      if (validOrientations.length > 0) {
        const orientations = validOrientations.map(orientationToIOSConstant).join(' ');
        result = result.replace(
          /INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = .+/g,
          `INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = ${orientations}`,
        );
      }
    }

    // Update entitlements path for iOS
    result = result.replace(
      /CODE_SIGN_ENTITLEMENTS = .+/g,
      `CODE_SIGN_ENTITLEMENTS = Config/${projectName}.entitlements`,
    );
  }

  // Update test bundle identifier and target name
  result = result.replace(/TEST_TARGET_NAME = .+/g, `TEST_TARGET_NAME = ${projectName}`);

  // Update comments that reference MyProject in entitlements paths
  result = result.replace(/Config\/MyProject\.entitlements/g, `Config/${projectName}.entitlements`);

  return result;
}

/**
 * Replace placeholders in a string (for non-XCConfig files)
 */
function replacePlaceholders(
  content: string,
  projectName: string,
  bundleIdentifier: string,
): string {
  let result = content;

  // Replace project name
  result = result.replace(/MyProject/g, projectName);

  // Replace bundle identifier - check for both patterns used in templates
  if (bundleIdentifier) {
    result = result.replace(/com\.example\.MyProject/g, bundleIdentifier);
    result = result.replace(/com\.mycompany\.MyProject/g, bundleIdentifier);
  }

  return result;
}

/**
 * Process a single file, replacing placeholders if it's a text file
 */
async function processFile(
  sourcePath: string,
  destPath: string,
  params: Record<string, unknown>,
  fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(),
): Promise<void> {
  const projectName = params.projectName as string;
  const bundleIdentifierParam = params.bundleIdentifier as string | undefined;
  const customizeNames = params.customizeNames as boolean | undefined;

  // Determine the destination file path
  let finalDestPath = destPath;
  if (customizeNames) {
    // Replace MyProject in file/directory names
    const fileName = basename(destPath);
    const dirName = dirname(destPath);
    const newFileName = fileName.replace(/MyProject/g, projectName);
    finalDestPath = join(dirName, newFileName);
  }

  // Text file extensions that should be processed
  const textExtensions = [
    '.swift',
    '.h',
    '.m',
    '.mm',
    '.cpp',
    '.c',
    '.pbxproj',
    '.plist',
    '.xcscheme',
    '.xctestplan',
    '.xcworkspacedata',
    '.xcconfig',
    '.json',
    '.xml',
    '.entitlements',
    '.storyboard',
    '.xib',
    '.md',
  ];

  const ext = sourcePath.toLowerCase();
  const isTextFile = textExtensions.some((textExt) => ext.endsWith(textExt));
  const isXCConfig = sourcePath.endsWith('.xcconfig');
  const isPackageSwift = sourcePath.endsWith('Package.swift');

  if (isTextFile && customizeNames) {
    // Read the file content
    const content = await fileSystemExecutor.readFile(sourcePath, 'utf-8');

    let processedContent;

    if (isXCConfig) {
      // Use special XCConfig processing
      processedContent = updateXCConfigFile(content, params);
    } else if (isPackageSwift) {
      // Use special Package.swift processing
      processedContent = updatePackageSwiftFile(content, params);
    } else {
      // Use standard placeholder replacement
      const bundleIdentifier =
        bundleIdentifierParam ??
        `com.example.${projectName.toLowerCase().replace(/[^a-z0-9]/g, '')}`;
      processedContent = replacePlaceholders(content, projectName, bundleIdentifier);
    }

    await fileSystemExecutor.mkdir(dirname(finalDestPath), { recursive: true });
    await fileSystemExecutor.writeFile(finalDestPath, processedContent, 'utf-8');
  } else {
    // Copy binary files as-is
    await fileSystemExecutor.mkdir(dirname(finalDestPath), { recursive: true });
    await fileSystemExecutor.cp(sourcePath, finalDestPath);
  }
}

/**
 * Recursively process a directory
 */
async function processDirectory(
  sourceDir: string,
  destDir: string,
  params: Record<string, unknown>,
  fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(),
): Promise<void> {
  const entries = await fileSystemExecutor.readdir(sourceDir, { withFileTypes: true });

  for (const entry of entries) {
    const entryTyped = entry as { name: string; isDirectory: () => boolean; isFile: () => boolean };
    const sourcePath = join(sourceDir, entryTyped.name);
    let destName = entryTyped.name;

    if (params.customizeNames) {
      // Replace MyProject in directory names
      destName = destName.replace(/MyProject/g, params.projectName as string);
    }

    const destPath = join(destDir, destName);

    if (entryTyped.isDirectory()) {
      // Skip certain directories
      if (entryTyped.name === '.git' || entryTyped.name === 'xcuserdata') {
        continue;
      }
      await fileSystemExecutor.mkdir(destPath, { recursive: true });
      await processDirectory(sourcePath, destPath, params, fileSystemExecutor);
    } else if (entryTyped.isFile()) {
      // Skip certain files
      if (entryTyped.name === '.DS_Store' || entryTyped.name.endsWith('.xcuserstate')) {
        continue;
      }
      await processFile(sourcePath, destPath, params, fileSystemExecutor);
    }
  }
}

// Use z.infer for type safety
type ScaffoldIOSProjectParams = z.infer<typeof ScaffoldiOSProjectSchema>;

/**
 * Logic function for scaffolding iOS projects
 */
export async function scaffold_ios_projectLogic(
  params: ScaffoldIOSProjectParams,
  commandExecutor: CommandExecutor,
  fileSystemExecutor: FileSystemExecutor,
): Promise<ToolResponse> {
  try {
    const projectParams = { ...params, platform: 'iOS' };
    const projectPath = await scaffoldProject(projectParams, commandExecutor, fileSystemExecutor);

    const response = {
      success: true,
      projectPath,
      platform: 'iOS',
      message: `Successfully scaffolded iOS project "${params.projectName}" in ${projectPath}`,
      nextSteps: [
        `Important: Before working on the project make sure to read the README.md file in the workspace root directory.`,
        `Build for simulator: build_sim({ workspacePath: "${projectPath}/${params.customizeNames ? params.projectName : 'MyProject'}.xcworkspace", scheme: "${params.customizeNames ? params.projectName : 'MyProject'}", simulatorName: "iPhone 16" })`,
        `Build and run on simulator: build_run_sim({ workspacePath: "${projectPath}/${params.customizeNames ? params.projectName : 'MyProject'}.xcworkspace", scheme: "${params.customizeNames ? params.projectName : 'MyProject'}", simulatorName: "iPhone 16" })`,
      ],
    };

    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(response, null, 2),
        },
      ],
    };
  } catch (error) {
    log(
      'error',
      `Failed to scaffold iOS project: ${error instanceof Error ? error.message : String(error)}`,
    );

    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(
            {
              success: false,
              error: error instanceof Error ? error.message : 'Unknown error occurred',
            },
            null,
            2,
          ),
        },
      ],
      isError: true,
    };
  }
}

/**
 * Scaffold a new iOS or macOS project
 */
async function scaffoldProject(
  params: Record<string, unknown>,
  commandExecutor?: CommandExecutor,
  fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(),
): Promise<string> {
  const projectName = params.projectName as string;
  const outputPath = params.outputPath as string;
  const platform = params.platform as 'iOS' | 'macOS';
  const customizeNames = (params.customizeNames as boolean | undefined) ?? true;

  log('info', `Scaffolding project: ${projectName} (${platform}) at ${outputPath}`);

  // Validate project name
  if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(projectName)) {
    throw new ValidationError(
      'Project name must start with a letter and contain only letters, numbers, and underscores',
    );
  }

  // Get template path from TemplateManager
  let templatePath;
  try {
    // Use the default command executor if not provided
    commandExecutor ??= getDefaultCommandExecutor();

    templatePath = await TemplateManager.getTemplatePath(
      platform,
      commandExecutor,
      fileSystemExecutor,
    );
  } catch (error) {
    throw new ValidationError(
      `Failed to get template for ${platform}: ${error instanceof Error ? error.message : String(error)}`,
    );
  }

  // Use outputPath directly as the destination
  const projectPath = outputPath;

  // Check if the output directory already has Xcode project files
  const xcworkspaceExists = fileSystemExecutor.existsSync(
    join(projectPath, `${customizeNames ? projectName : 'MyProject'}.xcworkspace`),
  );
  const xcodeprojExists = fileSystemExecutor.existsSync(
    join(projectPath, `${customizeNames ? projectName : 'MyProject'}.xcodeproj`),
  );

  if (xcworkspaceExists || xcodeprojExists) {
    throw new ValidationError(`Xcode project files already exist in ${projectPath}`);
  }

  try {
    // Process the template directly into the output path
    await processDirectory(templatePath, projectPath, params, fileSystemExecutor);

    return projectPath;
  } finally {
    // Clean up downloaded template if needed
    await TemplateManager.cleanup(templatePath, fileSystemExecutor);
  }
}

export default {
  name: 'scaffold_ios_project',
  description:
    'Scaffold a new iOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper iOS configuration.',
  schema: ScaffoldiOSProjectSchema.shape,
  async handler(args: Record<string, unknown>): Promise<ToolResponse> {
    const params = ScaffoldiOSProjectSchema.parse(args);
    return scaffold_ios_projectLogic(
      params,
      getDefaultCommandExecutor(),
      getDefaultFileSystemExecutor(),
    );
  },
};

```

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

```typescript
/**
 * Tests for test_macos plugin (unified project/workspace)
 * 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 { sessionStore } from '../../../../utils/session-store.ts';
import testMacos, { testMacosLogic } from '../test_macos.ts';

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

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

    it('should have correct description', () => {
      expect(testMacos.description).toBe('Runs tests for a macOS target.');
    });

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

    it('should validate schema correctly', () => {
      const schema = z.object(testMacos.schema);

      expect(schema.safeParse({}).success).toBe(true);
      expect(
        schema.safeParse({
          derivedDataPath: '/path/to/derived-data',
          extraArgs: ['--arg1', '--arg2'],
          preferXcodebuild: true,
          testRunnerEnv: { FOO: 'BAR' },
        }).success,
      ).toBe(true);

      expect(schema.safeParse({ derivedDataPath: 123 }).success).toBe(false);
      expect(schema.safeParse({ extraArgs: ['--ok', 1] }).success).toBe(false);
      expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false);
      expect(schema.safeParse({ testRunnerEnv: { FOO: 123 } }).success).toBe(false);

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

  describe('Handler Requirements', () => {
    it('should require scheme before running', async () => {
      const result = await testMacos.handler({});

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

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

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

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

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

      const result = await testMacos.handler({
        projectPath: '/path/to/project.xcodeproj',
        workspacePath: '/path/to/workspace.xcworkspace',
      });

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

  describe('XOR Parameter Validation', () => {
    it('should validate that either projectPath or workspacePath is provided', async () => {
      // Should return error response when neither is provided
      const result = await testMacos.handler({
        scheme: 'MyScheme',
      });

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

    it('should validate that both projectPath and workspacePath cannot be provided', async () => {
      // Should return error response when both are provided
      const result = await testMacos.handler({
        projectPath: '/path/to/project.xcodeproj',
        workspacePath: '/path/to/workspace.xcworkspace',
        scheme: 'MyScheme',
      });

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

    it('should allow only projectPath', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Test Suite All Tests passed',
      });

      const mockFileSystemExecutor = {
        mkdtemp: async () => '/tmp/test-123',
        rm: async () => {},
        tmpdir: () => '/tmp',
        stat: async () => ({ isDirectory: () => true }),
      };

      const result = await testMacosLogic(
        {
          projectPath: '/path/to/project.xcodeproj',
          scheme: 'MyScheme',
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result.content).toBeDefined();
      expect(Array.isArray(result.content)).toBe(true);
      expect(result.isError).toBeUndefined();
    });

    it('should allow only workspacePath', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Test Suite All Tests passed',
      });

      const mockFileSystemExecutor = {
        mkdtemp: async () => '/tmp/test-123',
        rm: async () => {},
        tmpdir: () => '/tmp',
        stat: async () => ({ isDirectory: () => true }),
      };

      const result = await testMacosLogic(
        {
          workspacePath: '/path/to/workspace.xcworkspace',
          scheme: 'MyScheme',
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result.content).toBeDefined();
      expect(Array.isArray(result.content)).toBe(true);
      expect(result.isError).toBeUndefined();
    });
  });

  describe('Handler Behavior (Complete Literal Returns)', () => {
    it('should return successful test response with workspace when xcodebuild succeeds', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Test Suite All Tests passed',
      });

      // Mock file system dependencies
      const mockFileSystemExecutor = {
        mkdtemp: async () => '/tmp/test-123',
        rm: async () => {},
        tmpdir: () => '/tmp',
        stat: async () => ({ isDirectory: () => true }),
      };

      const result = await testMacosLogic(
        {
          workspacePath: '/path/to/workspace.xcworkspace',
          scheme: 'MyScheme',
          configuration: 'Debug',
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result.content).toBeDefined();
      expect(Array.isArray(result.content)).toBe(true);
      expect(result.isError).toBeUndefined();
    });

    it('should return successful test response with project when xcodebuild succeeds', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Test Suite All Tests passed',
      });

      // Mock file system dependencies
      const mockFileSystemExecutor = {
        mkdtemp: async () => '/tmp/test-123',
        rm: async () => {},
        tmpdir: () => '/tmp',
        stat: async () => ({ isDirectory: () => true }),
      };

      const result = await testMacosLogic(
        {
          projectPath: '/path/to/project.xcodeproj',
          scheme: 'MyScheme',
          configuration: 'Debug',
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result.content).toBeDefined();
      expect(Array.isArray(result.content)).toBe(true);
      expect(result.isError).toBeUndefined();
    });

    it('should use default configuration when not provided', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Test Suite All Tests passed',
      });

      // Mock file system dependencies
      const mockFileSystemExecutor = {
        mkdtemp: async () => '/tmp/test-123',
        rm: async () => {},
        tmpdir: () => '/tmp',
        stat: async () => ({ isDirectory: () => true }),
      };

      const result = await testMacosLogic(
        {
          workspacePath: '/path/to/workspace.xcworkspace',
          scheme: 'MyScheme',
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result.content).toBeDefined();
      expect(Array.isArray(result.content)).toBe(true);
      expect(result.isError).toBeUndefined();
    });

    it('should handle optional parameters correctly', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Test Suite All Tests passed',
      });

      // Mock file system dependencies
      const mockFileSystemExecutor = {
        mkdtemp: async () => '/tmp/test-123',
        rm: async () => {},
        tmpdir: () => '/tmp',
        stat: async () => ({ isDirectory: () => true }),
      };

      const result = await testMacosLogic(
        {
          workspacePath: '/path/to/workspace.xcworkspace',
          scheme: 'MyScheme',
          configuration: 'Release',
          derivedDataPath: '/custom/derived',
          extraArgs: ['--verbose'],
          preferXcodebuild: true,
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result.content).toBeDefined();
      expect(Array.isArray(result.content)).toBe(true);
      expect(result.isError).toBeUndefined();
    });

    it('should handle successful test execution with minimal parameters', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Test Suite All Tests passed',
      });

      // Mock file system dependencies
      const mockFileSystemExecutor = {
        mkdtemp: async () => '/tmp/test-123',
        rm: async () => {},
        tmpdir: () => '/tmp',
        stat: async () => ({ isDirectory: () => true }),
      };

      const result = await testMacosLogic(
        {
          workspacePath: '/path/to/MyProject.xcworkspace',
          scheme: 'MyApp',
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result.content).toBeDefined();
      expect(Array.isArray(result.content)).toBe(true);
      expect(result.isError).toBeUndefined();
    });

    it('should return exact successful test response', async () => {
      // Track command execution calls
      const commandCalls: any[] = [];

      // Mock executor for successful test
      const mockExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: Record<string, string>,
      ) => {
        commandCalls.push({ command, logPrefix, useShell, env });

        // Handle xcresulttool command
        if (command.includes('xcresulttool')) {
          return {
            success: true,
            output: JSON.stringify({
              title: 'Test Results',
              result: 'SUCCEEDED',
              totalTestCount: 5,
              passedTests: 5,
              failedTests: 0,
              skippedTests: 0,
              expectedFailures: 0,
            }),
            error: undefined,
          };
        }

        return {
          success: true,
          output: 'Test Succeeded',
          error: undefined,
          process: { pid: 12345 },
        };
      };

      // Mock file system dependencies using approved utility
      const mockFileSystemExecutor = {
        mkdtemp: async () => '/tmp/xcodebuild-test-abc123',
        rm: async () => {},
        tmpdir: () => '/tmp',
        stat: async () => ({ isDirectory: () => true }),
      };

      const result = await testMacosLogic(
        {
          workspacePath: '/path/to/MyProject.xcworkspace',
          scheme: 'MyScheme',
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      // Verify commands were called with correct parameters
      expect(commandCalls).toHaveLength(2); // xcodebuild test + xcresulttool
      expect(commandCalls[0].command).toEqual([
        'xcodebuild',
        '-workspace',
        '/path/to/MyProject.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=macOS',
        '-resultBundlePath',
        '/tmp/xcodebuild-test-abc123/TestResults.xcresult',
        'test',
      ]);
      expect(commandCalls[0].logPrefix).toBe('Test Run');
      expect(commandCalls[0].useShell).toBe(true);

      // Verify xcresulttool was called
      expect(commandCalls[1].command).toEqual([
        'xcrun',
        'xcresulttool',
        'get',
        'test-results',
        'summary',
        '--path',
        '/tmp/xcodebuild-test-abc123/TestResults.xcresult',
      ]);
      expect(commandCalls[1].logPrefix).toBe('Parse xcresult bundle');

      expect(result.content).toEqual(
        expect.arrayContaining([
          expect.objectContaining({
            type: 'text',
            text: '✅ Test Run test succeeded for scheme MyScheme.',
          }),
        ]),
      );
    });

    it('should return exact test failure response', async () => {
      // Track command execution calls
      let callCount = 0;
      const mockExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: Record<string, string>,
      ) => {
        callCount++;

        // First call is xcodebuild test - fails
        if (callCount === 1) {
          return {
            success: false,
            output: '',
            error: 'error: Test failed',
            process: { pid: 12345 },
          };
        }

        // Second call is xcresulttool
        if (command.includes('xcresulttool')) {
          return {
            success: true,
            output: JSON.stringify({
              title: 'Test Results',
              result: 'FAILED',
              totalTestCount: 5,
              passedTests: 3,
              failedTests: 2,
              skippedTests: 0,
              expectedFailures: 0,
            }),
            error: undefined,
          };
        }

        return { success: true, output: '', error: undefined };
      };

      // Mock file system dependencies
      const mockFileSystemExecutor = {
        mkdtemp: async () => '/tmp/xcodebuild-test-abc123',
        rm: async () => {},
        tmpdir: () => '/tmp',
        stat: async () => ({ isDirectory: () => true }),
      };

      const result = await testMacosLogic(
        {
          workspacePath: '/path/to/MyProject.xcworkspace',
          scheme: 'MyScheme',
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result.content).toEqual(
        expect.arrayContaining([
          expect.objectContaining({
            type: 'text',
            text: '❌ Test Run test failed for scheme MyScheme.',
          }),
        ]),
      );
      expect(result.isError).toBe(true);
    });

    it('should return exact successful test response with optional parameters', async () => {
      // Track command execution calls
      const commandCalls: any[] = [];

      // Mock executor for successful test with optional parameters
      const mockExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: Record<string, string>,
      ) => {
        commandCalls.push({ command, logPrefix, useShell, env });

        // Handle xcresulttool command
        if (command.includes('xcresulttool')) {
          return {
            success: true,
            output: JSON.stringify({
              title: 'Test Results',
              result: 'SUCCEEDED',
              totalTestCount: 5,
              passedTests: 5,
              failedTests: 0,
              skippedTests: 0,
              expectedFailures: 0,
            }),
            error: undefined,
          };
        }

        return {
          success: true,
          output: 'Test Succeeded',
          error: undefined,
          process: { pid: 12345 },
        };
      };

      // Mock file system dependencies
      const mockFileSystemExecutor = {
        mkdtemp: async () => '/tmp/xcodebuild-test-abc123',
        rm: async () => {},
        tmpdir: () => '/tmp',
        stat: async () => ({ isDirectory: () => true }),
      };

      const result = await testMacosLogic(
        {
          workspacePath: '/path/to/MyProject.xcworkspace',
          scheme: 'MyScheme',
          configuration: 'Release',
          derivedDataPath: '/path/to/derived-data',
          extraArgs: ['--verbose'],
          preferXcodebuild: true,
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result.content).toEqual(
        expect.arrayContaining([
          expect.objectContaining({
            type: 'text',
            text: '✅ Test Run test succeeded for scheme MyScheme.',
          }),
        ]),
      );
    });

    it('should return exact exception handling response', async () => {
      // Mock executor (won't be called due to mkdtemp failure)
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Test Succeeded',
      });

      // Mock file system dependencies - mkdtemp fails
      const mockFileSystemExecutor = {
        mkdtemp: async () => {
          throw new Error('Network error');
        },
        rm: async () => {},
        tmpdir: () => '/tmp',
        stat: async () => ({ isDirectory: () => true }),
      };

      const result = await testMacosLogic(
        {
          workspacePath: '/path/to/MyProject.xcworkspace',
          scheme: 'MyScheme',
        },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error during test run: Network error',
          },
        ],
        isError: true,
      });
    });
  });
});

```

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

```typescript
/**
 * Tests for build_run_sim plugin (unified)
 * Following CLAUDE.md testing standards with dependency injection and literal validation
 */

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 buildRunSim, { build_run_simLogic } from '../build_run_sim.ts';

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

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

    it('should have correct description', () => {
      expect(buildRunSim.description).toBe('Builds and runs an app on an iOS simulator.');
    });

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

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

      expect(schema.safeParse({}).success).toBe(true);

      expect(
        schema.safeParse({
          derivedDataPath: '/path/to/derived',
          extraArgs: ['--verbose'],
          preferXcodebuild: false,
        }).success,
      ).toBe(true);

      expect(schema.safeParse({ derivedDataPath: 123 }).success).toBe(false);
      expect(schema.safeParse({ extraArgs: [123] }).success).toBe(false);
      expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false);

      const schemaKeys = Object.keys(buildRunSim.schema).sort();
      expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs', 'preferXcodebuild'].sort());
      expect(schemaKeys).not.toContain('scheme');
      expect(schemaKeys).not.toContain('simulatorName');
      expect(schemaKeys).not.toContain('projectPath');
    });
  });

  describe('Handler Behavior (Complete Literal Returns)', () => {
    // Note: Parameter validation is now handled by createTypedTool wrapper with Zod schema
    // The logic function receives validated parameters, so these tests focus on business logic

    it('should handle simulator not found', async () => {
      let callCount = 0;
      const mockExecutor = async (command: string[]) => {
        callCount++;
        if (callCount === 1) {
          // First call: build succeeds
          return {
            success: true,
            output: 'BUILD SUCCEEDED',
            process: { pid: 12345 },
          };
        } else if (callCount === 2) {
          // Second call: showBuildSettings fails to get app path
          return {
            success: false,
            error: 'Could not get build settings',
            process: { pid: 12345 },
          };
        }
        return {
          success: false,
          error: 'Unexpected call',
          process: { pid: 12345 },
        };
      };

      const result = await build_run_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Build succeeded, but failed to get app path: Could not get build settings',
          },
        ],
        isError: true,
      });
    });

    it('should handle build failure', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Build failed with error',
      });

      const result = await build_run_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content).toBeDefined();
      expect(Array.isArray(result.content)).toBe(true);
    });

    it('should handle successful build and run', async () => {
      // Create a mock executor that simulates full successful flow
      let callCount = 0;
      const mockExecutor = async (command: string[], logPrefix?: string) => {
        callCount++;

        if (command.includes('xcodebuild') && command.includes('build')) {
          // First call: build succeeds
          return {
            success: true,
            output: 'BUILD SUCCEEDED',
            process: { pid: 12345 },
          };
        } else if (command.includes('xcodebuild') && command.includes('-showBuildSettings')) {
          // Second call: build settings to get app path
          return {
            success: true,
            output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app\n',
            process: { pid: 12345 },
          };
        } else if (command.includes('simctl') && command.includes('list')) {
          // Find simulator calls
          return {
            success: true,
            output: JSON.stringify({
              devices: {
                'iOS 16.0': [
                  {
                    udid: 'test-uuid-123',
                    name: 'iPhone 16',
                    state: 'Booted',
                    isAvailable: true,
                  },
                ],
              },
            }),
            process: { pid: 12345 },
          };
        } else if (
          command.includes('plutil') ||
          command.includes('PlistBuddy') ||
          command.includes('defaults')
        ) {
          // Bundle ID extraction
          return {
            success: true,
            output: 'com.example.MyApp',
            process: { pid: 12345 },
          };
        } else {
          // All other commands (boot, open, install, launch) succeed
          return {
            success: true,
            output: 'Success',
            process: { pid: 12345 },
          };
        }
      };

      const result = await build_run_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result.content).toBeDefined();
      expect(Array.isArray(result.content)).toBe(true);
      expect(result.isError).toBe(false);
    });

    it('should handle exception with Error object', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Command failed',
      });

      const result = await build_run_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content).toBeDefined();
      expect(Array.isArray(result.content)).toBe(true);
    });

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

      const result = await build_run_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content).toBeDefined();
      expect(Array.isArray(result.content)).toBe(true);
    });
  });

  describe('Command Generation', () => {
    it('should generate correct simctl list command with minimal parameters', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        env?: any;
      }> = [];

      // Create tracking executor
      const trackingExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: Record<string, string>,
      ) => {
        callHistory.push({ command, logPrefix, useShell, env });
        return {
          success: false,
          output: '',
          error: 'Test error to stop execution early',
          process: { pid: 12345 },
        };
      };

      const result = await build_run_simLogic(
        {
          workspacePath: '/path/to/MyProject.xcworkspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        trackingExecutor,
      );

      // Should generate the initial build command
      expect(callHistory).toHaveLength(1);
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-workspace',
        '/path/to/MyProject.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16,OS=latest',
        'build',
      ]);
      expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');
    });

    it('should generate correct build command after finding simulator', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        env?: any;
      }> = [];

      let callCount = 0;
      // Create tracking executor that succeeds on first call (list) and fails on second
      const trackingExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: Record<string, string>,
      ) => {
        callHistory.push({ command, logPrefix, useShell, env });
        callCount++;

        if (callCount === 1) {
          // First call: simulator list succeeds
          return {
            success: true,
            output: JSON.stringify({
              devices: {
                'iOS 16.0': [
                  {
                    udid: 'test-uuid-123',
                    name: 'iPhone 16',
                    state: 'Booted',
                  },
                ],
              },
            }),
            error: undefined,
            process: { pid: 12345 },
          };
        } else {
          // Second call: build command fails to stop execution
          return {
            success: false,
            output: '',
            error: 'Test error to stop execution',
            process: { pid: 12345 },
          };
        }
      };

      const result = await build_run_simLogic(
        {
          workspacePath: '/path/to/MyProject.xcworkspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        trackingExecutor,
      );

      // Should generate build command and then build settings command
      expect(callHistory).toHaveLength(2);

      // First call: build command
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-workspace',
        '/path/to/MyProject.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16,OS=latest',
        'build',
      ]);
      expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');

      // Second call: build settings command to get app path
      expect(callHistory[1].command).toEqual([
        'xcodebuild',
        '-showBuildSettings',
        '-workspace',
        '/path/to/MyProject.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16,OS=latest',
      ]);
      expect(callHistory[1].logPrefix).toBe('Get App Path');
    });

    it('should generate correct build settings command after successful build', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        env?: any;
      }> = [];

      let callCount = 0;
      // Create tracking executor that succeeds on first two calls and fails on third
      const trackingExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: Record<string, string>,
      ) => {
        callHistory.push({ command, logPrefix, useShell, env });
        callCount++;

        if (callCount === 1) {
          // First call: simulator list succeeds
          return {
            success: true,
            output: JSON.stringify({
              devices: {
                'iOS 16.0': [
                  {
                    udid: 'test-uuid-123',
                    name: 'iPhone 16',
                    state: 'Booted',
                  },
                ],
              },
            }),
            error: undefined,
            process: { pid: 12345 },
          };
        } else if (callCount === 2) {
          // Second call: build command succeeds
          return {
            success: true,
            output: 'BUILD SUCCEEDED',
            error: undefined,
            process: { pid: 12345 },
          };
        } else {
          // Third call: build settings command fails to stop execution
          return {
            success: false,
            output: '',
            error: 'Test error to stop execution',
            process: { pid: 12345 },
          };
        }
      };

      const result = await build_run_simLogic(
        {
          workspacePath: '/path/to/MyProject.xcworkspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
          configuration: 'Release',
          useLatestOS: false,
        },
        trackingExecutor,
      );

      // Should generate build command and build settings command
      expect(callHistory).toHaveLength(2);

      // First call: build command
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-workspace',
        '/path/to/MyProject.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Release',
        '-skipMacroValidation',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16',
        'build',
      ]);
      expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');

      // Second call: build settings command
      expect(callHistory[1].command).toEqual([
        'xcodebuild',
        '-showBuildSettings',
        '-workspace',
        '/path/to/MyProject.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Release',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16',
      ]);
      expect(callHistory[1].logPrefix).toBe('Get App Path');
    });

    it('should handle paths with spaces in command generation', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        env?: any;
      }> = [];

      // Create tracking executor
      const trackingExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: Record<string, string>,
      ) => {
        callHistory.push({ command, logPrefix, useShell, env });
        return {
          success: false,
          output: '',
          error: 'Test error to stop execution early',
          process: { pid: 12345 },
        };
      };

      const result = await build_run_simLogic(
        {
          workspacePath: '/Users/dev/My Project/MyProject.xcworkspace',
          scheme: 'My Scheme',
          simulatorName: 'iPhone 16 Pro',
        },
        trackingExecutor,
      );

      // Should generate build command first
      expect(callHistory).toHaveLength(1);
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-workspace',
        '/Users/dev/My Project/MyProject.xcworkspace',
        '-scheme',
        'My Scheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16 Pro,OS=latest',
        'build',
      ]);
      expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');
    });
  });

  describe('XOR Validation', () => {
    it('should error when neither projectPath nor workspacePath provided', async () => {
      const result = await buildRunSim.handler({
        scheme: 'MyScheme',
        simulatorName: 'iPhone 16',
      });
      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 error when both projectPath and workspacePath provided', async () => {
      const result = await buildRunSim.handler({
        projectPath: '/path/project.xcodeproj',
        workspacePath: '/path/workspace.xcworkspace',
        scheme: 'MyScheme',
        simulatorName: 'iPhone 16',
      });
      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Parameter validation failed');
      expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
      expect(result.content[0].text).toContain('projectPath');
      expect(result.content[0].text).toContain('workspacePath');
    });

    it('should succeed with only projectPath', async () => {
      // This test fails early due to build failure, which is expected behavior
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Build failed',
      });

      const result = await build_run_simLogic(
        {
          projectPath: '/path/project.xcodeproj',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );
      // The test succeeds if the logic function accepts the parameters and attempts to build
      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Build failed');
    });

    it('should succeed with only workspacePath', async () => {
      // This test fails early due to build failure, which is expected behavior
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Build failed',
      });

      const result = await build_run_simLogic(
        {
          workspacePath: '/path/workspace.xcworkspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );
      // The test succeeds if the logic function accepts the parameters and attempts to build
      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Build failed');
    });
  });
});

```

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

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

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

// Unified schema: XOR between projectPath and workspacePath, and XOR between simulatorId and simulatorName
const baseOptions = {
  scheme: z.string().describe('The scheme to use (Required)'),
  simulatorId: z
    .string()
    .optional()
    .describe(
      'UUID of the simulator (from list_sims). Provide EITHER this OR simulatorName, not both',
    ),
  simulatorName: z
    .string()
    .optional()
    .describe(
      "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both",
    ),
  configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
  derivedDataPath: z
    .string()
    .optional()
    .describe('Path where build products and other derived data will go'),
  extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'),
  useLatestOS: z
    .boolean()
    .optional()
    .describe('Whether to use the latest OS version for the named simulator'),
  preferXcodebuild: z
    .boolean()
    .optional()
    .describe(
      'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.',
    ),
};

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

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

const buildRunSimulatorSchema = 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.',
  })
  .refine((val) => val.simulatorId !== undefined || val.simulatorName !== undefined, {
    message: 'Either simulatorId or simulatorName is required.',
  })
  .refine((val) => !(val.simulatorId !== undefined && val.simulatorName !== undefined), {
    message: 'simulatorId and simulatorName are mutually exclusive. Provide only one.',
  });

export type BuildRunSimulatorParams = z.infer<typeof buildRunSimulatorSchema>;

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

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

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

  // Create SharedBuildParams object with required configuration property
  const sharedBuildParams: SharedBuildParams = {
    workspacePath: params.workspacePath,
    projectPath: params.projectPath,
    scheme: params.scheme,
    configuration: params.configuration ?? 'Debug',
    derivedDataPath: params.derivedDataPath,
    extraArgs: params.extraArgs,
  };

  return executeXcodeBuildCommandFn(
    sharedBuildParams,
    {
      platform: XcodePlatform.iOSSimulator,
      simulatorId: params.simulatorId,
      simulatorName: params.simulatorName,
      useLatestOS: params.simulatorId ? false : params.useLatestOS,
      logPrefix: 'iOS Simulator Build',
    },
    params.preferXcodebuild as boolean,
    'build',
    executor,
  );
}

// Exported business logic function for building and running iOS Simulator apps.
export async function build_run_simLogic(
  params: BuildRunSimulatorParams,
  executor: CommandExecutor,
  executeXcodeBuildCommandFn: typeof executeXcodeBuildCommand = executeXcodeBuildCommand,
): Promise<ToolResponse> {
  const projectType = params.projectPath ? 'project' : 'workspace';
  const filePath = params.projectPath ?? params.workspacePath;

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

  try {
    // --- Build Step ---
    const buildResult = await _handleSimulatorBuildLogic(
      params,
      executor,
      executeXcodeBuildCommandFn,
    );

    if (buildResult.isError) {
      return buildResult; // Return the build error
    }

    // --- Get App Path Step ---
    // Create the command array for xcodebuild with -showBuildSettings option
    const command = ['xcodebuild', '-showBuildSettings'];

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

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

    // Handle destination for simulator
    let destinationString: string;
    if (params.simulatorId) {
      destinationString = `platform=iOS Simulator,id=${params.simulatorId}`;
    } else if (params.simulatorName) {
      destinationString = `platform=iOS Simulator,name=${params.simulatorName}${(params.useLatestOS ?? true) ? ',OS=latest' : ''}`;
    } else {
      // This shouldn't happen due to validation, but handle it
      destinationString = 'platform=iOS Simulator';
    }
    command.push('-destination', destinationString);

    // 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 App Path', true, undefined);

    // If there was an error with the command execution, return it
    if (!result.success) {
      return createTextResponse(
        `Build succeeded, but failed to get app path: ${result.error ?? 'Unknown error'}`,
        true,
      );
    }

    // Parse the output to extract the app path
    const buildSettingsOutput = result.output;

    // Try both approaches to get app path - first the project approach (CODESIGNING_FOLDER_PATH)
    let appBundlePath: string | null = null;

    // Project approach: Extract CODESIGNING_FOLDER_PATH from build settings to get app path
    const appPathMatch = buildSettingsOutput.match(/CODESIGNING_FOLDER_PATH = (.+\.app)/);
    if (appPathMatch?.[1]) {
      appBundlePath = appPathMatch[1].trim();
    } else {
      // Workspace approach: Extract BUILT_PRODUCTS_DIR and FULL_PRODUCT_NAME
      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) {
        const builtProductsDir = builtProductsDirMatch[1].trim();
        const fullProductName = fullProductNameMatch[1].trim();
        appBundlePath = `${builtProductsDir}/${fullProductName}`;
      }
    }

    if (!appBundlePath) {
      return createTextResponse(
        `Build succeeded, but could not find app path in build settings.`,
        true,
      );
    }

    log('info', `App bundle path for run: ${appBundlePath}`);

    // --- Find/Boot Simulator Step ---
    // Use our helper to determine the simulator UUID
    const uuidResult = await determineSimulatorUuid(
      { simulatorUuid: params.simulatorId, simulatorName: params.simulatorName },
      executor,
    );

    if (uuidResult.error) {
      return createTextResponse(`Build succeeded, but ${uuidResult.error.content[0].text}`, true);
    }

    if (uuidResult.warning) {
      log('warning', uuidResult.warning);
    }

    const simulatorUuid = uuidResult.uuid;

    if (!simulatorUuid) {
      return createTextResponse(
        'Build succeeded, but no simulator specified and failed to find a suitable one.',
        true,
      );
    }

    // Check simulator state and boot if needed
    try {
      log('info', `Checking simulator state for UUID: ${simulatorUuid}`);
      const simulatorListResult = await executor(
        ['xcrun', 'simctl', 'list', 'devices', 'available', '--json'],
        'List Simulators',
      );
      if (!simulatorListResult.success) {
        throw new Error(simulatorListResult.error ?? 'Failed to list simulators');
      }

      const simulatorsData = JSON.parse(simulatorListResult.output) as {
        devices: Record<string, unknown[]>;
      };
      let targetSimulator: { udid: string; name: string; state: string } | null = null;

      // Find the target simulator
      for (const runtime in simulatorsData.devices) {
        const devices = simulatorsData.devices[runtime];
        if (Array.isArray(devices)) {
          for (const device of devices) {
            if (
              typeof device === 'object' &&
              device !== null &&
              'udid' in device &&
              'name' in device &&
              'state' in device &&
              typeof device.udid === 'string' &&
              typeof device.name === 'string' &&
              typeof device.state === 'string' &&
              device.udid === simulatorUuid
            ) {
              targetSimulator = {
                udid: device.udid,
                name: device.name,
                state: device.state,
              };
              break;
            }
          }
          if (targetSimulator) break;
        }
      }

      if (!targetSimulator) {
        return createTextResponse(
          `Build succeeded, but could not find simulator with UUID: ${simulatorUuid}`,
          true,
        );
      }

      // Boot if needed
      if (targetSimulator.state !== 'Booted') {
        log('info', `Booting simulator ${targetSimulator.name}...`);
        const bootResult = await executor(
          ['xcrun', 'simctl', 'boot', simulatorUuid],
          'Boot Simulator',
        );
        if (!bootResult.success) {
          throw new Error(bootResult.error ?? 'Failed to boot simulator');
        }
      } else {
        log('info', `Simulator ${simulatorUuid} is already booted`);
      }
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      log('error', `Error checking/booting simulator: ${errorMessage}`);
      return createTextResponse(
        `Build succeeded, but error checking/booting simulator: ${errorMessage}`,
        true,
      );
    }

    // --- Open Simulator UI Step ---
    try {
      log('info', 'Opening Simulator app');
      const openResult = await executor(['open', '-a', 'Simulator'], 'Open Simulator App');
      if (!openResult.success) {
        throw new Error(openResult.error ?? 'Failed to open Simulator app');
      }
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      log('warning', `Warning: Could not open Simulator app: ${errorMessage}`);
      // Don't fail the whole operation for this
    }

    // --- Install App Step ---
    try {
      log('info', `Installing app at path: ${appBundlePath} to simulator: ${simulatorUuid}`);
      const installResult = await executor(
        ['xcrun', 'simctl', 'install', simulatorUuid, appBundlePath],
        'Install App',
      );
      if (!installResult.success) {
        throw new Error(installResult.error ?? 'Failed to install app');
      }
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      log('error', `Error installing app: ${errorMessage}`);
      return createTextResponse(
        `Build succeeded, but error installing app on simulator: ${errorMessage}`,
        true,
      );
    }

    // --- Get Bundle ID Step ---
    let bundleId;
    try {
      log('info', `Extracting bundle ID from app: ${appBundlePath}`);

      // Try multiple methods to get bundle ID - first PlistBuddy, then plutil, then defaults
      let bundleIdResult = null;

      // Method 1: PlistBuddy (most reliable)
      try {
        bundleIdResult = await executor(
          [
            '/usr/libexec/PlistBuddy',
            '-c',
            'Print :CFBundleIdentifier',
            `${appBundlePath}/Info.plist`,
          ],
          'Get Bundle ID with PlistBuddy',
        );
        if (bundleIdResult.success) {
          bundleId = bundleIdResult.output.trim();
        }
      } catch {
        // Continue to next method
      }

      // Method 2: plutil (workspace approach)
      if (!bundleId) {
        try {
          bundleIdResult = await executor(
            ['plutil', '-extract', 'CFBundleIdentifier', 'raw', `${appBundlePath}/Info.plist`],
            'Get Bundle ID with plutil',
          );
          if (bundleIdResult?.success) {
            bundleId = bundleIdResult.output?.trim();
          }
        } catch {
          // Continue to next method
        }
      }

      // Method 3: defaults (fallback)
      if (!bundleId) {
        try {
          bundleIdResult = await executor(
            ['defaults', 'read', `${appBundlePath}/Info`, 'CFBundleIdentifier'],
            'Get Bundle ID with defaults',
          );
          if (bundleIdResult?.success) {
            bundleId = bundleIdResult.output?.trim();
          }
        } catch {
          // All methods failed
        }
      }

      if (!bundleId) {
        throw new Error('Could not extract bundle ID from Info.plist using any method');
      }

      log('info', `Bundle ID for run: ${bundleId}`);
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      log('error', `Error getting bundle ID: ${errorMessage}`);
      return createTextResponse(
        `Build and install succeeded, but error getting bundle ID: ${errorMessage}`,
        true,
      );
    }

    // --- Launch App Step ---
    try {
      log('info', `Launching app with bundle ID: ${bundleId} on simulator: ${simulatorUuid}`);
      const launchResult = await executor(
        ['xcrun', 'simctl', 'launch', simulatorUuid, bundleId],
        'Launch App',
      );
      if (!launchResult.success) {
        throw new Error(launchResult.error ?? 'Failed to launch app');
      }
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      log('error', `Error launching app: ${errorMessage}`);
      return createTextResponse(
        `Build and install succeeded, but error launching app on simulator: ${errorMessage}`,
        true,
      );
    }

    // --- Success ---
    log('info', '✅ iOS simulator build & run succeeded.');

    const target = params.simulatorId
      ? `simulator UUID '${params.simulatorId}'`
      : `simulator name '${params.simulatorName}'`;
    const sourceType = params.projectPath ? 'project' : 'workspace';
    const sourcePath = params.projectPath ?? params.workspacePath;

    return {
      content: [
        {
          type: 'text',
          text: `✅ iOS simulator build and run succeeded for scheme ${params.scheme} from ${sourceType} ${sourcePath} targeting ${target}.
          
The app (${bundleId}) is now running in the iOS Simulator. 
If you don't see the simulator window, it may be hidden behind other windows. The Simulator app should be open.

Next Steps:
- Option 1: Capture structured logs only (app continues running):
  start_simulator_log_capture({ simulatorUuid: '${simulatorUuid}', bundleId: '${bundleId}' })
- Option 2: Capture both console and structured logs (app will restart):
  start_simulator_log_capture({ simulatorUuid: '${simulatorUuid}', bundleId: '${bundleId}', captureConsole: true })
- Option 3: Launch app with logs in one step (for a fresh start):
  launch_app_with_logs_in_simulator({ simulatorUuid: '${simulatorUuid}', bundleId: '${bundleId}' })

When done with any option, use: stop_sim_log_cap({ logSessionId: 'SESSION_ID' })`,
        },
      ],
      isError: false,
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error in iOS Simulator build and run: ${errorMessage}`);
    return createTextResponse(`Error in iOS Simulator build and run: ${errorMessage}`, true);
  }
}

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

export default {
  name: 'build_run_sim',
  description: 'Builds and runs an app on an iOS simulator.',
  schema: publicSchemaObject.shape,
  handler: createSessionAwareTool<BuildRunSimulatorParams>({
    internalSchema: buildRunSimulatorSchema as unknown as z.ZodType<BuildRunSimulatorParams>,
    logicFunction: build_run_simLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { allOf: ['scheme'], message: 'scheme is required' },
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
      { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
    ],
    exclusivePairs: [
      ['projectPath', 'workspacePath'],
      ['simulatorId', 'simulatorName'],
    ],
  }),
};

```

--------------------------------------------------------------------------------
/docs/session_management_plan.md:
--------------------------------------------------------------------------------

```markdown
# Stateful Session Defaults for MCP Tools — Design, Middleware, and Plan

Below is a concise architecture and implementation plan to introduce a session-aware defaults layer that removes repeated tool parameters from public schemas, while keeping all tool logic and tests unchanged.

## Architecture Overview

- **Core idea**: keep logic functions and tests untouched; move argument consolidation into a session-aware interop layer and expose minimal public schemas.
- **Data flow**:
  - Client calls a tool with zero or few args → session middleware merges session defaults → validates with the internal schema → calls the existing logic function.
- **Components**:
  - `SessionStore` (singleton, in-memory): set/get/clear/show defaults.
  - Session-aware tool factory: merges defaults, performs preflight requirement checks (allOf/oneOf), then validates with the tool's internal zod schema.
  - Public vs internal schema: plugins register a minimal "public" input schema; handlers validate with the unchanged "internal" schema.

## Core Types

```typescript
// src/utils/session-store.ts
export type SessionDefaults = {
  projectPath?: string;
  workspacePath?: string;
  scheme?: string;
  configuration?: string;
  simulatorName?: string;
  simulatorId?: string;
  deviceId?: string;
  useLatestOS?: boolean;
  arch?: 'arm64' | 'x86_64';
};
```

## Session Store (singleton)

```typescript
// src/utils/session-store.ts
import { log } from './logger.ts';

class SessionStore {
  private defaults: SessionDefaults = {};

  setDefaults(partial: Partial<SessionDefaults>): void {
    this.defaults = { ...this.defaults, ...partial };
    log('info', '[Session] Defaults set', { keys: Object.keys(partial) });
  }

  clear(keys?: (keyof SessionDefaults)[]): void {
    if (!keys || keys.length === 0) {
      this.defaults = {};
      log('info', '[Session] All defaults cleared');
      return;
    }
    for (const k of keys) delete this.defaults[k];
    log('info', '[Session] Defaults cleared', { keys });
  }

  get<K extends keyof SessionDefaults>(key: K): SessionDefaults[K] {
    return this.defaults[key];
  }

  getAll(): SessionDefaults {
    return { ...this.defaults };
  }
}

export const sessionStore = new SessionStore();
```

## Session-Aware Tool Factory

```typescript
// src/utils/typed-tool-factory.ts (add new helper, keep createTypedTool as-is)
import { z } from 'zod';
import { sessionStore, type SessionDefaults } from './session-store.ts';
import type { CommandExecutor } from './execution/index.ts';
import { createErrorResponse } from './responses/index.ts';
import type { ToolResponse } from '../types/common.ts';

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

function missingFromArgsAndSession(
  keys: (keyof SessionDefaults)[],
  args: Record<string, unknown>,
): string[] {
  return keys.filter((k) => args[k] == null && sessionStore.get(k) == null);
}

export function createSessionAwareTool<TParams>(opts: {
  internalSchema: z.ZodType<TParams>;
  logicFunction: (params: TParams, executor: CommandExecutor) => Promise<ToolResponse>;
  getExecutor: () => CommandExecutor;
  // Optional extras to improve UX and ergonomics
  sessionKeys?: (keyof SessionDefaults)[];
  requirements?: SessionRequirement[]; // preflight, friendlier than raw zod errors
}) {
  const { internalSchema, logicFunction, getExecutor, sessionKeys = [], requirements = [] } = opts;

  return async (rawArgs: Record<string, unknown>): Promise<ToolResponse> => {
    try {
      // Merge: explicit args take precedence over session defaults
      const merged: Record<string, unknown> = { ...sessionStore.getAll(), ...rawArgs };

      // Preflight requirement checks (clear message how to fix)
      for (const req of requirements) {
        if ('allOf' in req) {
          const missing = missingFromArgsAndSession(req.allOf, rawArgs);
          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 missing = missingFromArgsAndSession(req.oneOf, rawArgs);
          // oneOf satisfied if at least one is present in merged
          const satisfied = req.oneOf.some((k) => merged[k] != null);
          if (!satisfied) {
            return createErrorResponse(
              'Missing required session defaults',
              `${req.message ?? `Provide one of: ${req.oneOf.join(', ')}`}\n` +
                `Set with: session-set-defaults { "${req.oneOf[0]}": "..." }`,
            );
          }
        }
      }

      // Validate against unchanged internal schema (logic/api untouched)
      const validated = internalSchema.parse(merged);
      return await logicFunction(validated, getExecutor());
    } catch (error) {
      if (error instanceof z.ZodError) {
        const msgs = error.errors.map((e) => `${e.path.join('.') || 'root'}: ${e.message}`);
        return createErrorResponse(
          'Parameter validation failed',
          `Invalid parameters:\n${msgs.join('\n')}\n` +
            `Tip: set session defaults via session-set-defaults`,
        );
      }
      throw error;
    }
  };
}
```

## Plugin Migration Pattern (Example: build_sim)

Public schema hides session fields; handler uses session-aware factory with internal schema and requirements; logic function unchanged.

```typescript
// src/mcp/tools/simulator/build_sim.ts (key parts only)
import { z } from 'zod';
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';

// Existing internal schema (unchanged)…
const baseOptions = { /* as-is (scheme, simulatorId, simulatorName, configuration, …) */ };
const baseSchemaObject = z.object({
  projectPath: z.string().optional(),
  workspacePath: z.string().optional(),
  ...baseOptions,
});
const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);
const buildSimulatorSchema = baseSchema
  .refine(/* as-is: projectPath XOR workspacePath */)
  .refine(/* as-is: simulatorId XOR simulatorName */);

export type BuildSimulatorParams = z.infer<typeof buildSimulatorSchema>;

// Public schema = internal minus session-managed fields
const sessionManaged = [
  'projectPath',
  'workspacePath',
  'scheme',
  'configuration',
  'simulatorId',
  'simulatorName',
  'useLatestOS',
] as const;

const publicSchemaObject = baseSchemaObject.omit(
  Object.fromEntries(sessionManaged.map((k) => [k, true])) as Record<string, true>,
);

export default {
  name: 'build_sim',
  description: 'Builds an app for an iOS simulator.',
  schema: publicSchemaObject.shape, // what the MCP client sees
  handler: createSessionAwareTool<BuildSimulatorParams>({
    internalSchema: buildSimulatorSchema,
    logicFunction: build_simLogic,
    getExecutor: getDefaultCommandExecutor,
    sessionKeys: sessionManaged,
    requirements: [
      { allOf: ['scheme'], message: 'scheme is required' },
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
      { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
    ],
  }),
};
```

This same pattern applies to `build_run_sim`, `test_sim`, device/macos tools, etc. Public schemas become minimal, while internal schemas and logic remain unchanged.

## New Tool Group: session-management

### session_set_defaults.ts

```typescript
// src/mcp/tools/session-management/session_set_defaults.ts
import { z } from 'zod';
import { sessionStore, type SessionDefaults } from '../../../utils/session-store.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';

const schemaObj = z.object({
  projectPath: z.string().optional(),
  workspacePath: z.string().optional(),
  scheme: z.string().optional(),
  configuration: z.string().optional(),
  simulatorName: z.string().optional(),
  simulatorId: z.string().optional(),
  deviceId: z.string().optional(),
  useLatestOS: z.boolean().optional(),
  arch: z.enum(['arm64', 'x86_64']).optional(),
});
type Params = z.infer<typeof schemaObj>;

async function logic(params: Params): Promise<import('../../../types/common.ts').ToolResponse> {
  sessionStore.setDefaults(params as Partial<SessionDefaults>);
  const current = sessionStore.getAll();
  return { content: [{ type: 'text', text: `Defaults updated:\n${JSON.stringify(current, null, 2)}` }] };
}

export default {
  name: 'session-set-defaults',
  description: 'Set session defaults used by other tools.',
  schema: schemaObj.shape,
  handler: createTypedTool(schemaObj, logic, getDefaultCommandExecutor),
};
```

### session_clear_defaults.ts

```typescript
// src/mcp/tools/session-management/session_clear_defaults.ts
import { z } from 'zod';
import { sessionStore } from '../../../utils/session-store.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';

const keys = [
  'projectPath','workspacePath','scheme','configuration',
  'simulatorName','simulatorId','deviceId','useLatestOS','arch',
] as const;
const schemaObj = z.object({
  keys: z.array(z.enum(keys)).optional(),
  all: z.boolean().optional(),
});

async function logic(params: z.infer<typeof schemaObj>) {
  if (params.all || !params.keys) sessionStore.clear();
  else sessionStore.clear(params.keys);
  return { content: [{ type: 'text', text: 'Session defaults cleared' }] };
}

export default {
  name: 'session-clear-defaults',
  description: 'Clear selected or all session defaults.',
  schema: schemaObj.shape,
  handler: createTypedTool(schemaObj, logic, getDefaultCommandExecutor),
};
```

### session_show_defaults.ts

```typescript
// src/mcp/tools/session-management/session_show_defaults.ts
import { sessionStore } from '../../../utils/session-store.ts';

export default {
  name: 'session-show-defaults',
  description: 'Show current session defaults.',
  schema: {}, // no args
  handler: async () => {
    const current = sessionStore.getAll();
    return { content: [{ type: 'text', text: JSON.stringify(current, null, 2) }] };
  },
};
```

## Step-by-Step Implementation Plan (Incremental, buildable at each step)

1. **Add SessionStore** ✅ **DONE**
   - New file: `src/utils/session-store.ts`.
   - No existing code changes; run: `npm run build`, `lint`, `test`.
   - Commit checkpoint (after review): see Commit & Review Protocol below.

2. **Add session-management tools** ✅ **DONE**
   - New folder: `src/mcp/tools/session-management` with the three tools above.
   - Register via existing plugin discovery (same pattern as others).
   - Build and test.
   - Commit checkpoint (after review).

3. **Add session-aware tool factory** ✅ **DONE**
   - Add `createSessionAwareTool` to `src/utils/typed-tool-factory.ts` (keep `createTypedTool` intact).
   - Unit tests for requirement preflight and merge precedence.
   - Commit checkpoint (after review).

4. **Migrate 2-3 representative tools**
   - Example: `simulator/build_sim`, `macos/build_macos`, `device/build_device`.
   - Create `publicSchemaObject` (omit session fields), switch handler to `createSessionAwareTool` with requirements.
   - Keep internal schema and logic unchanged. Build and test.
   - Commit checkpoint (after review).

5. **Migrate remaining tools in small batches**
   - Apply the same pattern across simulator/device/macos/test utilities.
   - After each batch: `npm run typecheck`, `lint`, `test`.
   - Commit checkpoint (after review).

6. **Final polish**
   - Add tests for session tools and session-aware preflight error messages.
   - Ensure public schemas no longer expose session parameters globally.
   - Commit checkpoint (after review).

## Standard Testing & DI Checklist (Mandatory)

- Handlers must use dependency injection; tests must never call real executors.
- For validation-only tests, calling the handler is acceptable because Zod validation occurs before executor acquisition.
- For logic tests that would otherwise trigger `getDefaultCommandExecutor`, export the logic function and test it directly (no executor needed if logic doesn’t use one):

```ts
// Example: src/mcp/tools/session-management/session_clear_defaults.ts
export async function sessionClearDefaultsLogic(params: Params): Promise<ToolResponse> { /* ... */ }
export default {
  name: 'session-clear-defaults',
  handler: createTypedTool(schemaObj, sessionClearDefaultsLogic, getDefaultCommandExecutor),
};

// Test: import logic and call directly to avoid real executor
import plugin, { sessionClearDefaultsLogic } from '../session_clear_defaults.ts';
```

- Add tests for the new group and tools:
  - Group metadata test: `src/mcp/tools/session-management/__tests__/index.test.ts`
  - Tool tests: `session_set_defaults.test.ts`, `session_clear_defaults.test.ts`, `session_show_defaults.test.ts`
  - Utils tests: `src/utils/__tests__/session-store.test.ts`
  - Factory tests: `src/utils/__tests__/session-aware-tool-factory.test.ts` covering:
    - Preflight requirements (allOf/oneOf)
    - Merge precedence (explicit args override session defaults)
    - Zod error reporting with helpful tips

- Always run locally before requesting review:
  - `npm run typecheck`
  - `npm run lint`
  - `npm run format:check`
  - `npm run build`
  - `npm run test`
  - Perform a quick manual CLI check (mcpli or reloaderoo) per the Manual Testing section

### Minimal Changes Policy for Tests (Enforced)

- Only make material, essential edits to tests required by the code change (e.g., new preflight error messages or added/removed fields).
- Do not change sample input values or defaults in tests (e.g., flipping a boolean like `preferXcodebuild`) unless strictly necessary to validate behavior.
- Preserve the original intent and coverage of logic-function tests; keep handler vs logic boundaries intact.
- When session-awareness is added, prefer setting/clearing session defaults around tests rather than altering existing assertions or sample inputs.

### Tool Description Policy (Enforced)

- Keep tool descriptions concise (maximum one short sentence).
- Do not mention session defaults, setup steps, examples, or parameter relationships in descriptions.
- Use clear, imperative phrasing (e.g., "Builds an app for an iOS simulator.").
- Apply consistently across all migrated tools; update any tests that assert `description` to match the concise string only.

## Commit & Review Protocol (Enforced)

At the end of each numbered step above:

1. Ensure all checks pass: `typecheck`, `lint`, `format:check`, `build`, `test`; then perform a quick manual CLI test (mcpli or reloaderoo) per the Manual Testing section.
   - Verify tool descriptions comply with the Tool Description Policy (concise, no session-defaults mention).
2. Stage only the files for that step.
3. Prepare a concise commit message focused on the “why”.
4. Request manual review and approval before committing. Do not push.

Example messages per step:

- Step 1 (SessionStore)
  - `chore(utils): add in-memory SessionStore for session defaults`
  - Body: “Introduces singleton SessionStore with set/get/clear/show for session defaults; no behavior changes.”

- Step 2 (session-management tools)
  - `feat(session-management): add set/clear/show session defaults tools and workflow metadata`
  - Body: “Adds tools to manage session defaults and exposes workflow metadata; minimal schemas via typed factory.”

- Step 3 (middleware)
  - `feat(utils): add createSessionAwareTool with preflight requirements and args>session merge`
  - Body: “Session-aware interop layer performing requirements checks and Zod validation against internal schema.”

- Step 6 (tests/final polish)
  - `test(session-management): add tool, store, and middleware tests; export logic for DI`
  - Body: “Covers group metadata, tools, SessionStore, and factory (requirements/merge/errors). No production behavior changes.”

Approval flow:
- After preparing messages and confirming checks, request maintainer approval.
- On approval: commit locally (no push).
- On rejection: revise and re-run checks.

Note on commit hooks and selective commits:
- The pre-commit hook runs format/lint/build and can auto-add or modify files, causing additional files to be included in the commit. If you must commit a minimal subset, skip hooks with: `git commit --no-verify` (use sparingly and run `npm run typecheck && npm run lint && npm run test` manually first).

## Safety, Buildability, Testability

- Logic functions and their types remain unchanged; existing unit tests that import logic directly continue to pass.
- Public schemas shrink; MCP clients see smaller input schemas without session fields.
- Handlers validate with internal schemas after session-defaults merge, preserving runtime guarantees.
- Preflight requirement checks return clear guidance, e.g., "Provide one of: projectPath or workspacePath" + "Set with: session-set-defaults { "projectPath": "..." }".

## Developer Usage

- **Set defaults once**:
  - `session-set-defaults { "workspacePath": "...", "scheme": "App", "simulatorName": "iPhone 16" }`
- **Run tools without args**:
  - `build_sim {}`
- **Inspect/reset**:
  - `session-show-defaults {}`
  - `session-clear-defaults { "all": true }`

## Manual Testing with mcpli (CLI)

The following commands exercise the session workflow end‑to‑end using the built server.

1) Build the server (required after code changes):

```bash
npm run build
```

2) Discover a scheme (optional helper):

```bash
mcpli --raw list-schemes --projectPath "/Volumes/Developer/XcodeBuildMCP/example_projects/iOS/MCPTest.xcodeproj" -- node build/index.js
```

3) Set the session defaults (project/workspace, scheme, and simulator):

```bash
mcpli --raw session-set-defaults \
  --projectPath "/Volumes/Developer/XcodeBuildMCP/example_projects/iOS/MCPTest.xcodeproj" \
  --scheme MCPTest \
  --simulatorName "iPhone 16" \
  -- node build/index.js
```

4) Verify defaults are stored:

```bash
mcpli --raw session-show-defaults -- node build/index.js
```

5) Run a session‑aware tool with zero or minimal args (defaults are merged automatically):

```bash
# Optionally provide a scratch derived data path and a short timeout
mcpli --tool-timeout=60 --raw build-sim --derivedDataPath "/tmp/XBMCP_DD" -- node build/index.js
```

Troubleshooting:

- If you see validation errors like “Missing required session defaults …”, (re)run step 3 with the missing keys.
- If you see connect ECONNREFUSED or the daemon appears flaky:
  - Check logs: `mcpli daemon log --since=10m -- node build/index.js`
  - Restart daemon: `mcpli daemon restart -- node build/index.js`
  - Clean daemon state: `mcpli daemon clean -- node build/index.js` then `mcpli daemon start -- node build/index.js`
  - After code changes, always: `npm run build` then `mcpli daemon restart -- node build/index.js`

Notes:

- Public schemas for session‑aware tools intentionally omit session fields (e.g., `scheme`, `projectPath`, `simulatorName`). Provide them once via `session-set-defaults` and then call the tool with zero/minimal flags.
- Use `--tool-timeout=<seconds>` to cap long‑running builds during manual testing.
- mcpli CLI normalizes tool names: tools exported with underscores (e.g., `build_sim`) can be invoked with hyphens (e.g., `build-sim`). Copy/paste samples using hyphens are valid because mcpli converts underscores to dashes.

## Next Steps

Would you like me to proceed with Phase 1–3 implementation (store + session tools + middleware), then migrate a first tool (build_sim) and run the test suite?
```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/__tests__/build_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 the plugin and logic function
import buildSim, { build_simLogic } from '../build_sim.ts';

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

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

    it('should have correct description', () => {
      expect(buildSim.description).toBe('Builds an app for an iOS simulator.');
    });

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

    it('should have correct public schema (only non-session fields)', () => {
      const schema = z.object(buildSim.schema);

      // Public schema should allow empty input
      expect(schema.safeParse({}).success).toBe(true);

      // Valid public inputs
      expect(
        schema.safeParse({
          derivedDataPath: '/path/to/derived',
          extraArgs: ['--verbose'],
          preferXcodebuild: false,
        }).success,
      ).toBe(true);

      // Invalid types on public inputs
      expect(schema.safeParse({ derivedDataPath: 123 }).success).toBe(false);
      expect(schema.safeParse({ extraArgs: [123] }).success).toBe(false);
      expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false);
    });
  });

  describe('Parameter Validation', () => {
    it('should handle missing both projectPath and workspacePath', async () => {
      const result = await buildSim.handler({
        scheme: 'MyScheme',
        simulatorName: 'iPhone 16',
      });

      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 handle both projectPath and workspacePath provided', async () => {
      const result = await buildSim.handler({
        projectPath: '/path/to/project.xcodeproj',
        workspacePath: '/path/to/workspace',
        scheme: 'MyScheme',
        simulatorName: 'iPhone 16',
      });

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

    it('should handle empty workspacePath parameter', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });

      const result = await build_simLogic(
        {
          workspacePath: '',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      // Empty string passes validation but may cause build issues
      expect(result.content).toEqual([
        {
          type: 'text',
          text: '✅ iOS Simulator Build build succeeded for scheme MyScheme.',
        },
        {
          type: 'text',
          text: expect.stringContaining('Next Steps:'),
        },
      ]);
    });

    it('should handle missing scheme parameter', async () => {
      const result = await buildSim.handler({
        workspacePath: '/path/to/workspace',
        simulatorName: 'iPhone 16',
      });

      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 handle empty scheme parameter', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: '',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      // Empty string passes validation but may cause build issues
      expect(result.content).toEqual([
        {
          type: 'text',
          text: '✅ iOS Simulator Build build succeeded for scheme .',
        },
        {
          type: 'text',
          text: expect.stringContaining('Next Steps:'),
        },
      ]);
    });

    it('should handle missing both simulatorId and simulatorName', async () => {
      const result = await buildSim.handler({
        workspacePath: '/path/to/workspace',
        scheme: 'MyScheme',
      });

      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');
    });

    it('should handle both simulatorId and simulatorName provided', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'Build succeeded' });

      // Should fail with XOR validation
      const result = await buildSim.handler({
        workspacePath: '/path/to/workspace',
        scheme: 'MyScheme',
        simulatorId: 'ABC-123',
        simulatorName: 'iPhone 16',
      });

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

    it('should handle empty simulatorName parameter', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'For iOS Simulator platform, either simulatorId or simulatorName must be provided',
      });

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: '',
        },
        mockExecutor,
      );

      // Empty simulatorName passes validation but causes early failure in destination construction
      expect(result.isError).toBe(true);
      expect(result.content[0].text).toBe(
        'For iOS Simulator platform, either simulatorId or simulatorName must be provided',
      );
    });
  });

  describe('Command Generation', () => {
    it('should generate correct build command with minimal parameters (workspace)', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        env?: any;
      }> = [];

      // Create tracking executor
      const trackingExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: Record<string, string>,
      ) => {
        callHistory.push({ command, logPrefix, useShell, env });
        return {
          success: false,
          output: '',
          error: 'Test error to stop execution early',
          process: { pid: 12345 },
        };
      };

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/MyProject.xcworkspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        trackingExecutor,
      );

      // Should generate one build command
      expect(callHistory).toHaveLength(1);
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-workspace',
        '/path/to/MyProject.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16,OS=latest',
        'build',
      ]);
      expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');
    });

    it('should generate correct build command with minimal parameters (project)', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        env?: any;
      }> = [];

      // Create tracking executor
      const trackingExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: Record<string, string>,
      ) => {
        callHistory.push({ command, logPrefix, useShell, env });
        return {
          success: false,
          output: '',
          error: 'Test error to stop execution early',
          process: { pid: 12345 },
        };
      };

      const result = await build_simLogic(
        {
          projectPath: '/path/to/MyProject.xcodeproj',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        trackingExecutor,
      );

      // Should generate one build command
      expect(callHistory).toHaveLength(1);
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-project',
        '/path/to/MyProject.xcodeproj',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16,OS=latest',
        'build',
      ]);
      expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');
    });

    it('should generate correct build command with all optional parameters', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        env?: any;
      }> = [];

      // Create tracking executor
      const trackingExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: Record<string, string>,
      ) => {
        callHistory.push({ command, logPrefix, useShell, env });
        return {
          success: false,
          output: '',
          error: 'Test error to stop execution early',
          process: { pid: 12345 },
        };
      };

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/MyProject.xcworkspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
          configuration: 'Release',
          derivedDataPath: '/custom/derived/path',
          extraArgs: ['--verbose'],
          useLatestOS: false,
        },
        trackingExecutor,
      );

      // Should generate one build command with all parameters
      expect(callHistory).toHaveLength(1);
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-workspace',
        '/path/to/MyProject.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Release',
        '-skipMacroValidation',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16',
        '-derivedDataPath',
        '/custom/derived/path',
        '--verbose',
        'build',
      ]);
      expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');
    });

    it('should handle paths with spaces in command generation', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        env?: any;
      }> = [];

      // Create tracking executor
      const trackingExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: Record<string, string>,
      ) => {
        callHistory.push({ command, logPrefix, useShell, env });
        return {
          success: false,
          output: '',
          error: 'Test error to stop execution early',
          process: { pid: 12345 },
        };
      };

      const result = await build_simLogic(
        {
          workspacePath: '/Users/dev/My Project/MyProject.xcworkspace',
          scheme: 'My Scheme',
          simulatorName: 'iPhone 16 Pro',
        },
        trackingExecutor,
      );

      // Should generate one build command with paths containing spaces
      expect(callHistory).toHaveLength(1);
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-workspace',
        '/Users/dev/My Project/MyProject.xcworkspace',
        '-scheme',
        'My Scheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16 Pro,OS=latest',
        'build',
      ]);
      expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');
    });

    it('should generate correct build command with useLatestOS set to true', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        env?: any;
      }> = [];

      // Create tracking executor
      const trackingExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: Record<string, string>,
      ) => {
        callHistory.push({ command, logPrefix, useShell, env });
        return {
          success: false,
          output: '',
          error: 'Test error to stop execution early',
          process: { pid: 12345 },
        };
      };

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/MyProject.xcworkspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
          useLatestOS: true,
        },
        trackingExecutor,
      );

      // Should generate one build command with OS=latest
      expect(callHistory).toHaveLength(1);
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-workspace',
        '/path/to/MyProject.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16,OS=latest',
        'build',
      ]);
      expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');
    });
  });

  describe('Response Processing', () => {
    it('should handle successful build', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result.content).toEqual([
        {
          type: 'text',
          text: '✅ iOS Simulator Build build succeeded for scheme MyScheme.',
        },
        {
          type: 'text',
          text: expect.stringContaining('Next Steps:'),
        },
      ]);
    });

    it('should handle successful build with all optional parameters', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
          configuration: 'Release',
          derivedDataPath: '/path/to/derived',
          extraArgs: ['--verbose'],
          useLatestOS: false,
          preferXcodebuild: true,
        },
        mockExecutor,
      );

      expect(result.content).toEqual([
        {
          type: 'text',
          text: '✅ iOS Simulator Build build succeeded for scheme MyScheme.',
        },
        {
          type: 'text',
          text: expect.stringContaining('Next Steps:'),
        },
      ]);
    });

    it('should handle build failure', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'Build failed: Compilation error',
      });

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '❌ [stderr] Build failed: Compilation error',
          },
          {
            type: 'text',
            text: '❌ iOS Simulator Build build failed for scheme MyScheme.',
          },
        ],
        isError: true,
      });
    });

    it('should handle build warnings', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'warning: deprecated method used\nBUILD SUCCEEDED',
      });

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result.content).toEqual(
        expect.arrayContaining([
          {
            type: 'text',
            text: expect.stringContaining('⚠️'),
          },
          {
            type: 'text',
            text: '✅ iOS Simulator Build build succeeded for scheme MyScheme.',
          },
          {
            type: 'text',
            text: expect.stringContaining('Next Steps:'),
          },
        ]),
      );
    });

    it('should handle command executor errors', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'spawn xcodebuild ENOENT',
      });

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toBe('❌ [stderr] spawn xcodebuild ENOENT');
    });

    it('should handle mixed warning and error output', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        output: 'warning: deprecated method\nerror: undefined symbol',
        error: 'Build failed',
      });

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content).toEqual([
        {
          type: 'text',
          text: '⚠️ Warning: warning: deprecated method',
        },
        {
          type: 'text',
          text: '❌ Error: error: undefined symbol',
        },
        {
          type: 'text',
          text: '❌ [stderr] Build failed',
        },
        {
          type: 'text',
          text: '❌ iOS Simulator Build build failed for scheme MyScheme.',
        },
      ]);
    });

    it('should use default configuration when not provided', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
          // configuration intentionally omitted - should default to Debug
        },
        mockExecutor,
      );

      expect(result.content).toEqual([
        {
          type: 'text',
          text: '✅ iOS Simulator Build build succeeded for scheme MyScheme.',
        },
        {
          type: 'text',
          text: expect.stringContaining('Next Steps:'),
        },
      ]);
    });
  });

  describe('Error Handling', () => {
    it('should handle catch block exceptions', async () => {
      // Create a mock that throws an error when called
      const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });

      // Mock the handler to throw an error by passing invalid parameters to internal functions
      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      // Should handle the build successfully
      expect(result.content).toEqual([
        {
          type: 'text',
          text: '✅ iOS Simulator Build build succeeded for scheme MyScheme.',
        },
        {
          type: 'text',
          text: expect.stringContaining('Next Steps:'),
        },
      ]);
    });
  });
});

```

--------------------------------------------------------------------------------
/src/mcp/tools/logging/start_device_log_cap.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Logging Plugin: Start Device Log Capture
 *
 * Starts capturing logs from a specified Apple device by launching the app with console output.
 */

import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import type { ChildProcess } from 'child_process';
import { v4 as uuidv4 } from 'uuid';
import { z } from 'zod';
import { log } from '../../../utils/logging/index.ts';
import type { CommandExecutor, FileSystemExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { ToolResponse } from '../../../types/common.ts';
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';

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

// Note: Device and simulator logging use different approaches due to platform constraints:
// - Simulators use 'xcrun simctl' with console-pty and OSLog stream capabilities
// - Devices use 'xcrun devicectl' with console output only (no OSLog streaming)
// The different command structures and output formats make sharing infrastructure complex.
// However, both follow similar patterns for session management and log retention.
export interface DeviceLogSession {
  process: ChildProcess;
  logFilePath: string;
  deviceUuid: string;
  bundleId: string;
  logStream?: fs.WriteStream;
  hasEnded: boolean;
}

export const activeDeviceLogSessions = new Map<string, DeviceLogSession>();

const EARLY_FAILURE_WINDOW_MS = 5000;
const INITIAL_OUTPUT_LIMIT = 8_192;
const DEFAULT_JSON_RESULT_WAIT_MS = 8000;

const FAILURE_PATTERNS = [
  /The application failed to launch/i,
  /Provide a valid bundle identifier/i,
  /The requested application .* is not installed/i,
  /NSOSStatusErrorDomain/i,
  /NSLocalizedFailureReason/i,
  /ERROR:/i,
];

type JsonOutcome = {
  errorMessage?: string;
  pid?: number;
};

type DevicectlLaunchJson = {
  result?: {
    process?: {
      processIdentifier?: unknown;
    };
  };
  error?: {
    code?: unknown;
    domain?: unknown;
    localizedDescription?: unknown;
    userInfo?: Record<string, unknown> | undefined;
  };
};

function getJsonResultWaitMs(): number {
  const raw = process.env.XBMCP_LAUNCH_JSON_WAIT_MS;
  if (raw === undefined) {
    return DEFAULT_JSON_RESULT_WAIT_MS;
  }

  const parsed = Number(raw);
  if (!Number.isFinite(parsed) || parsed < 0) {
    return DEFAULT_JSON_RESULT_WAIT_MS;
  }

  return parsed;
}

function safeParseJson(text: string): DevicectlLaunchJson | null {
  try {
    const parsed = JSON.parse(text) as unknown;
    if (!parsed || typeof parsed !== 'object') {
      return null;
    }
    return parsed as DevicectlLaunchJson;
  } catch {
    return null;
  }
}

function extractJsonOutcome(json: DevicectlLaunchJson | null): JsonOutcome | null {
  if (!json) {
    return null;
  }

  const resultProcess = json.result?.process;
  const pidValue = resultProcess?.processIdentifier;
  if (typeof pidValue === 'number' && Number.isFinite(pidValue)) {
    return { pid: pidValue };
  }

  const error = json.error;
  if (!error) {
    return null;
  }

  const parts: string[] = [];

  if (typeof error.localizedDescription === 'string' && error.localizedDescription.length > 0) {
    parts.push(error.localizedDescription);
  }

  const userInfo = error.userInfo ?? {};
  const recovery = userInfo?.NSLocalizedRecoverySuggestion;
  const failureReason = userInfo?.NSLocalizedFailureReason;
  const bundleIdentifier = userInfo?.BundleIdentifier;

  if (typeof failureReason === 'string' && failureReason.length > 0) {
    parts.push(failureReason);
  }

  if (typeof recovery === 'string' && recovery.length > 0) {
    parts.push(recovery);
  }

  if (typeof bundleIdentifier === 'string' && bundleIdentifier.length > 0) {
    parts.push(`BundleIdentifier = ${bundleIdentifier}`);
  }

  const domain = error.domain;
  const code = error.code;
  const domainPart = typeof domain === 'string' && domain.length > 0 ? domain : undefined;
  const codePart = typeof code === 'number' && Number.isFinite(code) ? code : undefined;

  if (domainPart || codePart !== undefined) {
    parts.push(`(${domainPart ?? 'UnknownDomain'} code ${codePart ?? 'unknown'})`);
  }

  if (parts.length === 0) {
    return { errorMessage: 'Launch failed' };
  }

  return { errorMessage: parts.join('\n') };
}

async function removeFileIfExists(
  targetPath: string,
  fileExecutor?: FileSystemExecutor,
): Promise<void> {
  try {
    if (fileExecutor) {
      if (fileExecutor.existsSync(targetPath)) {
        await fileExecutor.rm(targetPath, { force: true });
      }
      return;
    }

    if (fs.existsSync(targetPath)) {
      await fs.promises.rm(targetPath, { force: true });
    }
  } catch {
    // Best-effort cleanup only
  }
}

async function pollJsonOutcome(
  jsonPath: string,
  fileExecutor: FileSystemExecutor | undefined,
  timeoutMs: number,
): Promise<JsonOutcome | null> {
  const start = Date.now();

  const readOnce = async (): Promise<JsonOutcome | null> => {
    try {
      const exists = fileExecutor?.existsSync(jsonPath) ?? fs.existsSync(jsonPath);

      if (!exists) {
        return null;
      }

      const content = fileExecutor
        ? await fileExecutor.readFile(jsonPath, 'utf8')
        : await fs.promises.readFile(jsonPath, 'utf8');

      const outcome = extractJsonOutcome(safeParseJson(content));
      if (outcome) {
        await removeFileIfExists(jsonPath, fileExecutor);
        return outcome;
      }
    } catch {
      // File may still be written; try again later
    }

    return null;
  };

  const immediate = await readOnce();
  if (immediate) {
    return immediate;
  }

  if (timeoutMs <= 0) {
    return null;
  }

  let delay = Math.min(100, Math.max(10, Math.floor(timeoutMs / 4) || 10));

  while (Date.now() - start < timeoutMs) {
    await new Promise((resolve) => setTimeout(resolve, delay));
    const result = await readOnce();
    if (result) {
      return result;
    }
    delay = Math.min(400, delay + 50);
  }

  return null;
}

type WriteStreamWithClosed = fs.WriteStream & { closed?: boolean };

/**
 * Start a log capture session for an iOS device by launching the app with console output.
 * Uses the devicectl command to launch the app and capture console logs.
 * Returns { sessionId, error? }
 */
export async function startDeviceLogCapture(
  params: {
    deviceUuid: string;
    bundleId: string;
  },
  executor: CommandExecutor = getDefaultCommandExecutor(),
  fileSystemExecutor?: FileSystemExecutor,
): Promise<{ sessionId: string; error?: string }> {
  // Clean up old logs before starting a new session
  await cleanOldDeviceLogs();

  const { deviceUuid, bundleId } = params;
  const logSessionId = uuidv4();
  const logFileName = `${DEVICE_LOG_FILE_PREFIX}${logSessionId}.log`;
  const tempDir = fileSystemExecutor ? fileSystemExecutor.tmpdir() : os.tmpdir();
  const logFilePath = path.join(tempDir, logFileName);
  const launchJsonPath = path.join(tempDir, `devicectl-launch-${logSessionId}.json`);

  let logStream: fs.WriteStream | undefined;

  try {
    // Use injected file system executor or default
    if (fileSystemExecutor) {
      await fileSystemExecutor.mkdir(tempDir, { recursive: true });
      await fileSystemExecutor.writeFile(logFilePath, '');
    } else {
      await fs.promises.mkdir(tempDir, { recursive: true });
      await fs.promises.writeFile(logFilePath, '');
    }

    logStream = fs.createWriteStream(logFilePath, { flags: 'a' });

    logStream.write(
      `\n--- Device log capture for bundle ID: ${bundleId} on device: ${deviceUuid} ---\n`,
    );

    // Use executor with dependency injection instead of spawn directly
    const result = await executor(
      [
        'xcrun',
        'devicectl',
        'device',
        'process',
        'launch',
        '--console',
        '--terminate-existing',
        '--device',
        deviceUuid,
        '--json-output',
        launchJsonPath,
        bundleId,
      ],
      'Device Log Capture',
      true,
      undefined,
      true,
    );

    if (!result.success) {
      log(
        'error',
        `Device log capture process reported failure: ${result.error ?? 'unknown error'}`,
      );
      if (logStream && !logStream.destroyed) {
        logStream.write(
          `\n--- Device log capture failed to start ---\n${result.error ?? 'Unknown error'}\n`,
        );
        logStream.end();
      }
      return {
        sessionId: '',
        error: result.error ?? 'Failed to start device log capture',
      };
    }

    const childProcess = result.process;
    if (!childProcess) {
      throw new Error('Device log capture process handle was not returned');
    }

    const session: DeviceLogSession = {
      process: childProcess,
      logFilePath,
      deviceUuid,
      bundleId,
      logStream,
      hasEnded: false,
    };

    let bufferedOutput = '';
    const appendBufferedOutput = (text: string): void => {
      bufferedOutput += text;
      if (bufferedOutput.length > INITIAL_OUTPUT_LIMIT) {
        bufferedOutput = bufferedOutput.slice(bufferedOutput.length - INITIAL_OUTPUT_LIMIT);
      }
    };

    let triggerImmediateFailure: ((message: string) => void) | undefined;

    const handleOutput = (chunk: unknown): void => {
      if (!logStream || logStream.destroyed) return;
      const text =
        typeof chunk === 'string'
          ? chunk
          : chunk instanceof Buffer
            ? chunk.toString('utf8')
            : String(chunk ?? '');
      if (text.length > 0) {
        appendBufferedOutput(text);
        const extracted = extractFailureMessage(bufferedOutput);
        if (extracted) {
          triggerImmediateFailure?.(extracted);
        }
        logStream.write(text);
      }
    };

    childProcess.stdout?.setEncoding?.('utf8');
    childProcess.stdout?.on?.('data', handleOutput);
    childProcess.stderr?.setEncoding?.('utf8');
    childProcess.stderr?.on?.('data', handleOutput);

    const cleanupStreams = (): void => {
      childProcess.stdout?.off?.('data', handleOutput);
      childProcess.stderr?.off?.('data', handleOutput);
    };

    const earlyFailure = await detectEarlyLaunchFailure(
      childProcess,
      EARLY_FAILURE_WINDOW_MS,
      () => bufferedOutput,
      (handler) => {
        triggerImmediateFailure = handler;
      },
    );

    if (earlyFailure) {
      cleanupStreams();
      session.hasEnded = true;

      const failureMessage =
        earlyFailure.errorMessage && earlyFailure.errorMessage.length > 0
          ? earlyFailure.errorMessage
          : `Device log capture process exited immediately (exit code: ${
              earlyFailure.exitCode ?? 'unknown'
            })`;

      log('error', `Device log capture failed to start: ${failureMessage}`);
      if (logStream && !logStream.destroyed) {
        try {
          logStream.write(`\n--- Device log capture failed to start ---\n${failureMessage}\n`);
        } catch {
          // best-effort logging
        }
        logStream.end();
      }

      await removeFileIfExists(launchJsonPath, fileSystemExecutor);

      childProcess.kill?.('SIGTERM');
      return { sessionId: '', error: failureMessage };
    }

    const jsonOutcome = await pollJsonOutcome(
      launchJsonPath,
      fileSystemExecutor,
      getJsonResultWaitMs(),
    );

    if (jsonOutcome?.errorMessage) {
      cleanupStreams();
      session.hasEnded = true;

      const failureMessage = jsonOutcome.errorMessage;

      log('error', `Device log capture failed to start (JSON): ${failureMessage}`);

      if (logStream && !logStream.destroyed) {
        try {
          logStream.write(`\n--- Device log capture failed to start ---\n${failureMessage}\n`);
        } catch {
          // ignore secondary logging failures
        }
        logStream.end();
      }

      childProcess.kill?.('SIGTERM');
      return { sessionId: '', error: failureMessage };
    }

    if (jsonOutcome?.pid && logStream && !logStream.destroyed) {
      try {
        logStream.write(`Process ID: ${jsonOutcome.pid}\n`);
      } catch {
        // best-effort logging only
      }
    }

    childProcess.once?.('error', (err) => {
      log(
        'error',
        `Device log capture process error (session ${logSessionId}): ${
          err instanceof Error ? err.message : String(err)
        }`,
      );
    });

    childProcess.once?.('close', (code) => {
      cleanupStreams();
      session.hasEnded = true;
      if (logStream && !logStream.destroyed && !(logStream as WriteStreamWithClosed).closed) {
        logStream.write(`\n--- Device log capture ended (exit code: ${code ?? 'unknown'}) ---\n`);
        logStream.end();
      }
      void removeFileIfExists(launchJsonPath, fileSystemExecutor);
    });

    // For testing purposes, we'll simulate process management
    // In actual usage, the process would be managed by the executor result
    activeDeviceLogSessions.set(logSessionId, session);

    log('info', `Device log capture started with session ID: ${logSessionId}`);
    return { sessionId: logSessionId };
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    log('error', `Failed to start device log capture: ${message}`);
    if (logStream && !logStream.destroyed && !(logStream as WriteStreamWithClosed).closed) {
      try {
        logStream.write(`\n--- Device log capture failed: ${message} ---\n`);
      } catch {
        // ignore secondary stream write failures
      }
      logStream.end();
    }
    await removeFileIfExists(launchJsonPath, fileSystemExecutor);
    return { sessionId: '', error: message };
  }
}

type EarlyFailureResult = {
  exitCode: number | null;
  errorMessage?: string;
};

function detectEarlyLaunchFailure(
  process: ChildProcess,
  timeoutMs: number,
  getBufferedOutput?: () => string,
  registerImmediateFailure?: (handler: (message: string) => void) => void,
): Promise<EarlyFailureResult | null> {
  if (process.exitCode != null) {
    if (process.exitCode === 0) {
      const failureFromOutput = extractFailureMessage(getBufferedOutput?.());
      return Promise.resolve(
        failureFromOutput ? { exitCode: process.exitCode, errorMessage: failureFromOutput } : null,
      );
    }
    const failureFromOutput = extractFailureMessage(getBufferedOutput?.());
    return Promise.resolve({ exitCode: process.exitCode, errorMessage: failureFromOutput });
  }

  return new Promise<EarlyFailureResult | null>((resolve) => {
    let settled = false;

    const finalize = (result: EarlyFailureResult | null): void => {
      if (settled) return;
      settled = true;
      process.removeListener('close', onClose);
      process.removeListener('error', onError);
      clearTimeout(timer);
      resolve(result);
    };

    registerImmediateFailure?.((message) => {
      finalize({ exitCode: process.exitCode ?? null, errorMessage: message });
    });

    const onClose = (code: number | null): void => {
      const failureFromOutput = extractFailureMessage(getBufferedOutput?.());
      if (code === 0 && failureFromOutput) {
        finalize({ exitCode: code ?? null, errorMessage: failureFromOutput });
        return;
      }
      if (code === 0) {
        finalize(null);
      } else {
        finalize({ exitCode: code ?? null, errorMessage: failureFromOutput });
      }
    };

    const onError = (error: Error): void => {
      finalize({ exitCode: null, errorMessage: error.message });
    };

    const timer = setTimeout(() => {
      const failureFromOutput = extractFailureMessage(getBufferedOutput?.());
      if (failureFromOutput) {
        process.kill?.('SIGTERM');
        finalize({ exitCode: process.exitCode ?? null, errorMessage: failureFromOutput });
        return;
      }
      finalize(null);
    }, timeoutMs);

    process.once('close', onClose);
    process.once('error', onError);
  });
}

function extractFailureMessage(output?: string): string | undefined {
  if (!output) {
    return undefined;
  }
  const normalized = output.replace(/\r/g, '');
  const lines = normalized
    .split('\n')
    .map((line) => line.trim())
    .filter(Boolean);

  const shouldInclude = (line?: string): boolean => {
    if (!line) return false;
    return (
      line.startsWith('NS') ||
      line.startsWith('BundleIdentifier') ||
      line.startsWith('Provide ') ||
      line.startsWith('The application') ||
      line.startsWith('ERROR:')
    );
  };

  for (const pattern of FAILURE_PATTERNS) {
    const matchIndex = lines.findIndex((line) => pattern.test(line));
    if (matchIndex === -1) {
      continue;
    }

    const snippet: string[] = [lines[matchIndex]];
    const nextLine = lines[matchIndex + 1];
    const thirdLine = lines[matchIndex + 2];
    if (shouldInclude(nextLine)) snippet.push(nextLine);
    if (shouldInclude(thirdLine)) snippet.push(thirdLine);
    const message = snippet.join('\n').trim();
    if (message.length > 0) {
      return message;
    }
    return lines[matchIndex];
  }

  return undefined;
}

/**
 * Deletes device log files older than LOG_RETENTION_DAYS from the temp directory.
 * Runs quietly; errors are logged but do not throw.
 */
// Device logs follow the same retention policy as simulator logs but use a different prefix
// to avoid conflicts. Both clean up logs older than LOG_RETENTION_DAYS automatically.
async function cleanOldDeviceLogs(): Promise<void> {
  const tempDir = os.tmpdir();
  let files;
  try {
    files = await fs.promises.readdir(tempDir);
  } catch (err) {
    log(
      'warn',
      `Could not read temp dir for device log cleanup: ${err instanceof Error ? err.message : String(err)}`,
    );
    return;
  }
  const now = Date.now();
  const retentionMs = LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
  await Promise.all(
    files
      .filter((f) => f.startsWith(DEVICE_LOG_FILE_PREFIX) && f.endsWith('.log'))
      .map(async (f) => {
        const filePath = path.join(tempDir, f);
        try {
          const stat = await fs.promises.stat(filePath);
          if (now - stat.mtimeMs > retentionMs) {
            await fs.promises.unlink(filePath);
            log('info', `Deleted old device log file: ${filePath}`);
          }
        } catch (err) {
          log(
            'warn',
            `Error during device log cleanup for ${filePath}: ${err instanceof Error ? err.message : String(err)}`,
          );
        }
      }),
  );
}

// Define schema as ZodObject
const startDeviceLogCapSchema = z.object({
  deviceId: z.string().describe('UDID of the device (obtained from list_devices)'),
  bundleId: z.string().describe('Bundle identifier of the app to launch and capture logs for.'),
});

// Use z.infer for type safety
type StartDeviceLogCapParams = z.infer<typeof startDeviceLogCapSchema>;

/**
 * Core business logic for starting device log capture.
 */
export async function start_device_log_capLogic(
  params: StartDeviceLogCapParams,
  executor: CommandExecutor,
  fileSystemExecutor?: FileSystemExecutor,
): Promise<ToolResponse> {
  const { deviceId, bundleId } = params;

  const { sessionId, error } = await startDeviceLogCapture(
    {
      deviceUuid: deviceId,
      bundleId: bundleId,
    },
    executor,
    fileSystemExecutor,
  );

  if (error) {
    return {
      content: [
        {
          type: 'text',
          text: `Failed to start device log capture: ${error}`,
        },
      ],
      isError: true,
    };
  }

  return {
    content: [
      {
        type: 'text',
        text: `✅ Device log capture started successfully\n\nSession ID: ${sessionId}\n\nNote: The app has been launched on the device with console output capture enabled.\n\nNext Steps:\n1. Interact with your app on the device\n2. Use stop_device_log_cap({ logSessionId: '${sessionId}' }) to stop capture and retrieve logs`,
      },
    ],
  };
}

export default {
  name: 'start_device_log_cap',
  description: 'Starts log capture on a connected device.',
  schema: startDeviceLogCapSchema.omit({ deviceId: true } as const).shape,
  handler: createSessionAwareTool<StartDeviceLogCapParams>({
    internalSchema: startDeviceLogCapSchema as unknown as z.ZodType<StartDeviceLogCapParams>,
    logicFunction: start_device_log_capLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [{ allOf: ['deviceId'], message: 'deviceId is required' }],
  }),
};

```

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Shows tool and workflow counts using runtime or static analysis.

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

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

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

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

Lists tools and resources with optional details.

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

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

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

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

Performs detailed static analysis of source files using AST parsing.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    if (runtimeData.dynamicMode) {
      console.log(
        `   ${colors.yellow}ℹ️  Dynamic mode: Only enabled workflow tools shown${colors.reset}`,
      );
    }
    console.log();
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// Run the CLI
main();

```

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

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

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

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

// Helper function to create mock axe helpers
function createMockAxeHelpers(): AxeHelpers {
  return {
    getAxePath: () => '/mocked/axe/path',
    getBundledAxeEnvironment: () => ({ SOME_ENV: 'value' }),
    createAxeNotAvailableResponse: () => ({
      content: [
        {
          type: 'text',
          text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
        },
      ],
      isError: true,
    }),
  };
}

// Helper function to create mock axe helpers with null path (for dependency error tests)
function createMockAxeHelpersWithNullPath(): AxeHelpers {
  return {
    getAxePath: () => null,
    getBundledAxeEnvironment: () => ({ SOME_ENV: 'value' }),
    createAxeNotAvailableResponse: () => ({
      content: [
        {
          type: 'text',
          text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
        },
      ],
      isError: true,
    }),
  };
}

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

    it('should have correct description', () => {
      expect(tapPlugin.description).toBe(
        "Tap at specific coordinates. Use describe_ui to get precise element coordinates (don't guess from screenshots). Supports optional timing delays.",
      );
    });

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

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

      // Valid case
      expect(
        schema.safeParse({
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          x: 100,
          y: 200,
        }).success,
      ).toBe(true);

      // Invalid simulatorUuid
      expect(
        schema.safeParse({
          simulatorUuid: 'invalid-uuid',
          x: 100,
          y: 200,
        }).success,
      ).toBe(false);

      // Invalid x coordinate - non-integer
      expect(
        schema.safeParse({
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          x: 3.14,
          y: 200,
        }).success,
      ).toBe(false);

      // Invalid y coordinate - non-integer
      expect(
        schema.safeParse({
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          x: 100,
          y: 3.14,
        }).success,
      ).toBe(false);

      // Invalid preDelay - negative
      expect(
        schema.safeParse({
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          x: 100,
          y: 200,
          preDelay: -1,
        }).success,
      ).toBe(false);

      // Invalid postDelay - negative
      expect(
        schema.safeParse({
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          x: 100,
          y: 200,
          postDelay: -1,
        }).success,
      ).toBe(false);

      // Valid with optional delays
      expect(
        schema.safeParse({
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          x: 100,
          y: 200,
          preDelay: 0.5,
          postDelay: 1.0,
        }).success,
      ).toBe(true);

      // Missing required fields
      expect(schema.safeParse({}).success).toBe(false);
    });
  });

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

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

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

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

      const mockAxeHelpers = createMockAxeHelpers();

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

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

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

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

      const mockAxeHelpers = createMockAxeHelpers();

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

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

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

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

      const mockAxeHelpers = createMockAxeHelpers();

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

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

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

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

      const mockAxeHelpers = createMockAxeHelpers();

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

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

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

      const mockAxeHelpers = createMockAxeHelpers();

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

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

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

      const mockAxeHelpers = createMockAxeHelpers();

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

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

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

      const mockAxeHelpers = createMockAxeHelpers();

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

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

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

      const mockAxeHelpers = createMockAxeHelpers();

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

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

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

      const mockAxeHelpers = createMockAxeHelpers();

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

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

  describe('Plugin Handler Validation', () => {
    it('should return Zod validation error for missing simulatorUuid', async () => {
      const result = await tapPlugin.handler({
        x: 100,
        y: 200,
      });

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorUuid: Required',
          },
        ],
        isError: true,
      });
    });

    it('should return Zod validation error for missing x coordinate', async () => {
      const result = await tapPlugin.handler({
        simulatorUuid: '12345678-1234-1234-1234-123456789012',
        y: 200,
      });

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nx: Required',
          },
        ],
        isError: true,
      });
    });

    it('should return Zod validation error for missing y coordinate', async () => {
      const result = await tapPlugin.handler({
        simulatorUuid: '12345678-1234-1234-1234-123456789012',
        x: 100,
      });

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\ny: Required',
          },
        ],
        isError: true,
      });
    });

    it('should return Zod validation error for invalid UUID format', async () => {
      const result = await tapPlugin.handler({
        simulatorUuid: 'invalid-uuid',
        x: 100,
        y: 200,
      });

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorUuid: Invalid Simulator UUID format',
          },
        ],
        isError: true,
      });
    });

    it('should return Zod validation error for non-integer x coordinate', async () => {
      const result = await tapPlugin.handler({
        simulatorUuid: '12345678-1234-1234-1234-123456789012',
        x: 3.14,
        y: 200,
      });

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nx: X coordinate must be an integer',
          },
        ],
        isError: true,
      });
    });

    it('should return Zod validation error for non-integer y coordinate', async () => {
      const result = await tapPlugin.handler({
        simulatorUuid: '12345678-1234-1234-1234-123456789012',
        x: 100,
        y: 3.14,
      });

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\ny: Y coordinate must be an integer',
          },
        ],
        isError: true,
      });
    });

    it('should return Zod validation error for negative preDelay', async () => {
      const result = await tapPlugin.handler({
        simulatorUuid: '12345678-1234-1234-1234-123456789012',
        x: 100,
        y: 200,
        preDelay: -1,
      });

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\npreDelay: Pre-delay must be non-negative',
          },
        ],
        isError: true,
      });
    });

    it('should return Zod validation error for negative postDelay', async () => {
      const result = await tapPlugin.handler({
        simulatorUuid: '12345678-1234-1234-1234-123456789012',
        x: 100,
        y: 200,
        postDelay: -1,
      });

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\npostDelay: Post-delay must be non-negative',
          },
        ],
        isError: true,
      });
    });
  });

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

      const mockAxeHelpers = createMockAxeHelpersWithNullPath();

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

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
          },
        ],
        isError: true,
      });
    });

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

      const mockAxeHelpers = createMockAxeHelpersWithNullPath();

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

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
          },
        ],
        isError: true,
      });
    });

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

      const mockAxeHelpers = createMockAxeHelpersWithNullPath();

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

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
          },
        ],
        isError: true,
      });
    });

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

      const mockAxeHelpers = createMockAxeHelpersWithNullPath();

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

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
          },
        ],
        isError: true,
      });
    });

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

      const mockAxeHelpers = createMockAxeHelpersWithNullPath();

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

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
          },
        ],
        isError: true,
      });
    });

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

      const mockAxeHelpers = createMockAxeHelpersWithNullPath();

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

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
          },
        ],
        isError: true,
      });
    });
  });
});

```
Page 9/11FirstPrevNextLast