#
tokens: 49531/50000 17/337 files (page 6/14)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 6 of 14. Use http://codebase.md/cameroncooke/xcodebuildmcp?lines=true&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/doctor/lib/doctor.deps.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as os from 'os';
  2 | import type { CommandExecutor } from '../../../../utils/execution/index.ts';
  3 | import {
  4 |   loadWorkflowGroups,
  5 |   loadPlugins,
  6 |   getEnabledWorkflows,
  7 | } from '../../../../utils/plugin-registry/index.ts';
  8 | import { areAxeToolsAvailable } from '../../../../utils/axe/index.ts';
  9 | import {
 10 |   isXcodemakeEnabled,
 11 |   isXcodemakeAvailable,
 12 |   doesMakefileExist,
 13 | } from '../../../../utils/xcodemake/index.ts';
 14 | import { getTrackedToolNames } from '../../../../utils/tool-registry.ts';
 15 | 
 16 | export interface BinaryChecker {
 17 |   checkBinaryAvailability(binary: string): Promise<{ available: boolean; version?: string }>;
 18 | }
 19 | 
 20 | export interface XcodeInfoProvider {
 21 |   getXcodeInfo(): Promise<
 22 |     | { version: string; path: string; selectedXcode: string; xcrunVersion: string }
 23 |     | { error: string }
 24 |   >;
 25 | }
 26 | 
 27 | export interface EnvironmentInfoProvider {
 28 |   getEnvironmentVariables(): Record<string, string | undefined>;
 29 |   getSystemInfo(): {
 30 |     platform: string;
 31 |     release: string;
 32 |     arch: string;
 33 |     cpus: string;
 34 |     memory: string;
 35 |     hostname: string;
 36 |     username: string;
 37 |     homedir: string;
 38 |     tmpdir: string;
 39 |   };
 40 |   getNodeInfo(): {
 41 |     version: string;
 42 |     execPath: string;
 43 |     pid: string;
 44 |     ppid: string;
 45 |     platform: string;
 46 |     arch: string;
 47 |     cwd: string;
 48 |     argv: string;
 49 |   };
 50 | }
 51 | 
 52 | export interface PluginInfoProvider {
 53 |   getPluginSystemInfo(): Promise<
 54 |     | {
 55 |         totalPlugins: number;
 56 |         pluginDirectories: number;
 57 |         pluginsByDirectory: Record<string, string[]>;
 58 |         systemMode: string;
 59 |       }
 60 |     | { error: string; systemMode: string }
 61 |   >;
 62 | }
 63 | 
 64 | export interface RuntimeInfoProvider {
 65 |   getRuntimeToolInfo(): Promise<
 66 |     | {
 67 |         mode: 'dynamic';
 68 |         enabledWorkflows: string[];
 69 |         enabledTools: string[];
 70 |         totalRegistered: number;
 71 |       }
 72 |     | {
 73 |         mode: 'static';
 74 |         enabledWorkflows: string[];
 75 |         enabledTools: string[];
 76 |         totalRegistered: number;
 77 |       }
 78 |   >;
 79 | }
 80 | 
 81 | export interface FeatureDetector {
 82 |   areAxeToolsAvailable(): boolean;
 83 |   isXcodemakeEnabled(): boolean;
 84 |   isXcodemakeAvailable(): Promise<boolean>;
 85 |   doesMakefileExist(path: string): boolean;
 86 | }
 87 | 
 88 | export interface DoctorDependencies {
 89 |   binaryChecker: BinaryChecker;
 90 |   xcode: XcodeInfoProvider;
 91 |   env: EnvironmentInfoProvider;
 92 |   plugins: PluginInfoProvider;
 93 |   runtime: RuntimeInfoProvider;
 94 |   features: FeatureDetector;
 95 | }
 96 | 
 97 | export function createDoctorDependencies(executor: CommandExecutor): DoctorDependencies {
 98 |   const binaryChecker: BinaryChecker = {
 99 |     async checkBinaryAvailability(binary: string) {
100 |       // If bundled axe is available, reflect that in dependencies even if not on PATH
101 |       if (binary === 'axe' && areAxeToolsAvailable()) {
102 |         return { available: true, version: 'Bundled' };
103 |       }
104 |       try {
105 |         const which = await executor(['which', binary], 'Check Binary Availability');
106 |         if (!which.success) {
107 |           return { available: false };
108 |         }
109 |       } catch {
110 |         return { available: false };
111 |       }
112 | 
113 |       let version: string | undefined;
114 |       const versionCommands: Record<string, string> = {
115 |         axe: 'axe --version',
116 |         mise: 'mise --version',
117 |       };
118 | 
119 |       if (binary in versionCommands) {
120 |         try {
121 |           const res = await executor(versionCommands[binary]!.split(' '), 'Get Binary Version');
122 |           if (res.success && res.output) {
123 |             version = res.output.trim();
124 |           }
125 |         } catch {
126 |           // ignore
127 |         }
128 |       }
129 | 
130 |       return { available: true, version: version ?? 'Available (version info not available)' };
131 |     },
132 |   };
133 | 
134 |   const xcode: XcodeInfoProvider = {
135 |     async getXcodeInfo() {
136 |       try {
137 |         const xcodebuild = await executor(['xcodebuild', '-version'], 'Get Xcode Version');
138 |         if (!xcodebuild.success) throw new Error('xcodebuild command failed');
139 |         const version = xcodebuild.output.trim().split('\n').slice(0, 2).join(' - ');
140 | 
141 |         const pathRes = await executor(['xcode-select', '-p'], 'Get Xcode Path');
142 |         if (!pathRes.success) throw new Error('xcode-select command failed');
143 |         const path = pathRes.output.trim();
144 | 
145 |         const selected = await executor(['xcrun', '--find', 'xcodebuild'], 'Find Xcodebuild');
146 |         if (!selected.success) throw new Error('xcrun --find command failed');
147 |         const selectedXcode = selected.output.trim();
148 | 
149 |         const xcrun = await executor(['xcrun', '--version'], 'Get Xcrun Version');
150 |         if (!xcrun.success) throw new Error('xcrun --version command failed');
151 |         const xcrunVersion = xcrun.output.trim();
152 | 
153 |         return { version, path, selectedXcode, xcrunVersion };
154 |       } catch (error) {
155 |         return { error: error instanceof Error ? error.message : String(error) };
156 |       }
157 |     },
158 |   };
159 | 
160 |   const env: EnvironmentInfoProvider = {
161 |     getEnvironmentVariables() {
162 |       const relevantVars = [
163 |         'INCREMENTAL_BUILDS_ENABLED',
164 |         'PATH',
165 |         'DEVELOPER_DIR',
166 |         'HOME',
167 |         'USER',
168 |         'TMPDIR',
169 |         'NODE_ENV',
170 |         'SENTRY_DISABLED',
171 |       ];
172 | 
173 |       const envVars: Record<string, string | undefined> = {};
174 |       for (const varName of relevantVars) {
175 |         envVars[varName] = process.env[varName];
176 |       }
177 | 
178 |       Object.keys(process.env).forEach((key) => {
179 |         if (key.startsWith('XCODEBUILDMCP_')) {
180 |           envVars[key] = process.env[key];
181 |         }
182 |       });
183 | 
184 |       return envVars;
185 |     },
186 | 
187 |     getSystemInfo() {
188 |       return {
189 |         platform: os.platform(),
190 |         release: os.release(),
191 |         arch: os.arch(),
192 |         cpus: `${os.cpus().length} x ${os.cpus()[0]?.model ?? 'Unknown'}`,
193 |         memory: `${Math.round(os.totalmem() / (1024 * 1024 * 1024))} GB`,
194 |         hostname: os.hostname(),
195 |         username: os.userInfo().username,
196 |         homedir: os.homedir(),
197 |         tmpdir: os.tmpdir(),
198 |       };
199 |     },
200 | 
201 |     getNodeInfo() {
202 |       return {
203 |         version: process.version,
204 |         execPath: process.execPath,
205 |         pid: process.pid.toString(),
206 |         ppid: process.ppid.toString(),
207 |         platform: process.platform,
208 |         arch: process.arch,
209 |         cwd: process.cwd(),
210 |         argv: process.argv.join(' '),
211 |       };
212 |     },
213 |   };
214 | 
215 |   const plugins: PluginInfoProvider = {
216 |     async getPluginSystemInfo() {
217 |       try {
218 |         const workflows = await loadWorkflowGroups();
219 |         const pluginsByDirectory: Record<string, string[]> = {};
220 |         let totalPlugins = 0;
221 | 
222 |         for (const [dirName, wf] of workflows.entries()) {
223 |           const toolNames = wf.tools.map((t) => t.name).filter(Boolean) as string[];
224 |           totalPlugins += toolNames.length;
225 |           pluginsByDirectory[dirName] = toolNames;
226 |         }
227 | 
228 |         return {
229 |           totalPlugins,
230 |           pluginDirectories: workflows.size,
231 |           pluginsByDirectory,
232 |           systemMode: 'plugin-based',
233 |         };
234 |       } catch (error) {
235 |         return {
236 |           error: `Failed to load plugins: ${error instanceof Error ? error.message : 'Unknown error'}`,
237 |           systemMode: 'error',
238 |         };
239 |       }
240 |     },
241 |   };
242 | 
243 |   const runtime: RuntimeInfoProvider = {
244 |     async getRuntimeToolInfo() {
245 |       const dynamic = process.env.XCODEBUILDMCP_DYNAMIC_TOOLS === 'true';
246 | 
247 |       if (dynamic) {
248 |         const enabledWf = getEnabledWorkflows();
249 |         const enabledTools = getTrackedToolNames();
250 |         return {
251 |           mode: 'dynamic',
252 |           enabledWorkflows: enabledWf,
253 |           enabledTools,
254 |           totalRegistered: enabledTools.length,
255 |         };
256 |       }
257 | 
258 |       // Static mode: all tools are registered
259 |       const workflows = await loadWorkflowGroups();
260 |       const enabledWorkflows = Array.from(workflows.keys());
261 |       const plugins = await loadPlugins();
262 |       const enabledTools = Array.from(plugins.keys());
263 |       return {
264 |         mode: 'static',
265 |         enabledWorkflows,
266 |         enabledTools,
267 |         totalRegistered: enabledTools.length,
268 |       };
269 |     },
270 |   };
271 | 
272 |   const features: FeatureDetector = {
273 |     areAxeToolsAvailable,
274 |     isXcodemakeEnabled,
275 |     isXcodemakeAvailable,
276 |     doesMakefileExist,
277 |   };
278 | 
279 |   return { binaryChecker, xcode, env, plugins, runtime, features };
280 | }
281 | 
282 | export type { CommandExecutor };
283 | 
284 | export default {} as const;
285 | 
```

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

```typescript
  1 | /**
  2 |  * macOS Shared Plugin: Build and Run macOS (Unified)
  3 |  *
  4 |  * Builds and runs a macOS app from a project or workspace in one step.
  5 |  * Accepts mutually exclusive `projectPath` or `workspacePath`.
  6 |  */
  7 | 
  8 | import { z } from 'zod';
  9 | import { log } from '../../../utils/logging/index.ts';
 10 | import { createTextResponse } from '../../../utils/responses/index.ts';
 11 | import { executeXcodeBuildCommand } from '../../../utils/build/index.ts';
 12 | import { ToolResponse, XcodePlatform } from '../../../types/common.ts';
 13 | import type { CommandExecutor } from '../../../utils/execution/index.ts';
 14 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
 15 | import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
 16 | import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
 17 | 
 18 | // Unified schema: XOR between projectPath and workspacePath
 19 | const baseSchemaObject = z.object({
 20 |   projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
 21 |   workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
 22 |   scheme: z.string().describe('The scheme to use'),
 23 |   configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
 24 |   derivedDataPath: z
 25 |     .string()
 26 |     .optional()
 27 |     .describe('Path where build products and other derived data will go'),
 28 |   arch: z
 29 |     .enum(['arm64', 'x86_64'])
 30 |     .optional()
 31 |     .describe('Architecture to build for (arm64 or x86_64). For macOS only.'),
 32 |   extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'),
 33 |   preferXcodebuild: z
 34 |     .boolean()
 35 |     .optional()
 36 |     .describe('If true, prefers xcodebuild over the experimental incremental build system'),
 37 | });
 38 | 
 39 | const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);
 40 | 
 41 | const publicSchemaObject = baseSchemaObject.omit({
 42 |   projectPath: true,
 43 |   workspacePath: true,
 44 |   scheme: true,
 45 |   configuration: true,
 46 |   arch: true,
 47 | } as const);
 48 | 
 49 | const buildRunMacOSSchema = baseSchema
 50 |   .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
 51 |     message: 'Either projectPath or workspacePath is required.',
 52 |   })
 53 |   .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
 54 |     message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
 55 |   });
 56 | 
 57 | export type BuildRunMacOSParams = z.infer<typeof buildRunMacOSSchema>;
 58 | 
 59 | /**
 60 |  * Internal logic for building macOS apps.
 61 |  */
 62 | async function _handleMacOSBuildLogic(
 63 |   params: BuildRunMacOSParams,
 64 |   executor: CommandExecutor,
 65 | ): Promise<ToolResponse> {
 66 |   log('info', `Starting macOS build for scheme ${params.scheme} (internal)`);
 67 | 
 68 |   return executeXcodeBuildCommand(
 69 |     {
 70 |       ...params,
 71 |       configuration: params.configuration ?? 'Debug',
 72 |     },
 73 |     {
 74 |       platform: XcodePlatform.macOS,
 75 |       arch: params.arch,
 76 |       logPrefix: 'macOS Build',
 77 |     },
 78 |     params.preferXcodebuild ?? false,
 79 |     'build',
 80 |     executor,
 81 |   );
 82 | }
 83 | 
 84 | async function _getAppPathFromBuildSettings(
 85 |   params: BuildRunMacOSParams,
 86 |   executor: CommandExecutor,
 87 | ): Promise<{ success: true; appPath: string } | { success: false; error: string }> {
 88 |   try {
 89 |     // Create the command array for xcodebuild
 90 |     const command = ['xcodebuild', '-showBuildSettings'];
 91 | 
 92 |     // Add the project or workspace
 93 |     if (params.projectPath) {
 94 |       command.push('-project', params.projectPath);
 95 |     } else if (params.workspacePath) {
 96 |       command.push('-workspace', params.workspacePath);
 97 |     }
 98 | 
 99 |     // Add the scheme and configuration
100 |     command.push('-scheme', params.scheme);
101 |     command.push('-configuration', params.configuration ?? 'Debug');
102 | 
103 |     // Add derived data path if provided
104 |     if (params.derivedDataPath) {
105 |       command.push('-derivedDataPath', params.derivedDataPath);
106 |     }
107 | 
108 |     // Add extra args if provided
109 |     if (params.extraArgs && params.extraArgs.length > 0) {
110 |       command.push(...params.extraArgs);
111 |     }
112 | 
113 |     // Execute the command directly
114 |     const result = await executor(command, 'Get Build Settings for Launch', true, undefined);
115 | 
116 |     if (!result.success) {
117 |       return {
118 |         success: false,
119 |         error: result.error ?? 'Failed to get build settings',
120 |       };
121 |     }
122 | 
123 |     // Parse the output to extract the app path
124 |     const buildSettingsOutput = result.output;
125 |     const builtProductsDirMatch = buildSettingsOutput.match(/^\s*BUILT_PRODUCTS_DIR\s*=\s*(.+)$/m);
126 |     const fullProductNameMatch = buildSettingsOutput.match(/^\s*FULL_PRODUCT_NAME\s*=\s*(.+)$/m);
127 | 
128 |     if (!builtProductsDirMatch || !fullProductNameMatch) {
129 |       return { success: false, error: 'Could not extract app path from build settings' };
130 |     }
131 | 
132 |     const appPath = `${builtProductsDirMatch[1].trim()}/${fullProductNameMatch[1].trim()}`;
133 |     return { success: true, appPath };
134 |   } catch (error) {
135 |     const errorMessage = error instanceof Error ? error.message : String(error);
136 |     return { success: false, error: errorMessage };
137 |   }
138 | }
139 | 
140 | /**
141 |  * Business logic for building and running macOS apps.
142 |  */
143 | export async function buildRunMacOSLogic(
144 |   params: BuildRunMacOSParams,
145 |   executor: CommandExecutor,
146 | ): Promise<ToolResponse> {
147 |   log('info', 'Handling macOS build & run logic...');
148 | 
149 |   try {
150 |     // First, build the app
151 |     const buildResult = await _handleMacOSBuildLogic(params, executor);
152 | 
153 |     // 1. Check if the build itself failed
154 |     if (buildResult.isError) {
155 |       return buildResult; // Return build failure directly
156 |     }
157 |     const buildWarningMessages = buildResult.content?.filter((c) => c.type === 'text') ?? [];
158 | 
159 |     // 2. Build succeeded, now get the app path using the helper
160 |     const appPathResult = await _getAppPathFromBuildSettings(params, executor);
161 | 
162 |     // 3. Check if getting the app path failed
163 |     if (!appPathResult.success) {
164 |       log('error', 'Build succeeded, but failed to get app path to launch.');
165 |       const response = createTextResponse(
166 |         `✅ Build succeeded, but failed to get app path to launch: ${appPathResult.error}`,
167 |         false, // Build succeeded, so not a full error
168 |       );
169 |       if (response.content) {
170 |         response.content.unshift(...buildWarningMessages);
171 |       }
172 |       return response;
173 |     }
174 | 
175 |     const appPath = appPathResult.appPath; // success === true narrows to string
176 |     log('info', `App path determined as: ${appPath}`);
177 | 
178 |     // 4. Launch the app using CommandExecutor
179 |     const launchResult = await executor(['open', appPath], 'Launch macOS App', true);
180 | 
181 |     if (!launchResult.success) {
182 |       log('error', `Build succeeded, but failed to launch app ${appPath}: ${launchResult.error}`);
183 |       const errorResponse = createTextResponse(
184 |         `✅ Build succeeded, but failed to launch app ${appPath}. Error: ${launchResult.error}`,
185 |         false, // Build succeeded
186 |       );
187 |       if (errorResponse.content) {
188 |         errorResponse.content.unshift(...buildWarningMessages);
189 |       }
190 |       return errorResponse;
191 |     }
192 | 
193 |     log('info', `✅ macOS app launched successfully: ${appPath}`);
194 |     const successResponse: ToolResponse = {
195 |       content: [
196 |         ...buildWarningMessages,
197 |         {
198 |           type: 'text',
199 |           text: `✅ macOS build and run succeeded for scheme ${params.scheme}. App launched: ${appPath}`,
200 |         },
201 |       ],
202 |       isError: false,
203 |     };
204 |     return successResponse;
205 |   } catch (error) {
206 |     const errorMessage = error instanceof Error ? error.message : String(error);
207 |     log('error', `Error during macOS build & run logic: ${errorMessage}`);
208 |     const errorResponse = createTextResponse(
209 |       `Error during macOS build and run: ${errorMessage}`,
210 |       true,
211 |     );
212 |     return errorResponse;
213 |   }
214 | }
215 | 
216 | export default {
217 |   name: 'build_run_macos',
218 |   description: 'Builds and runs a macOS app.',
219 |   schema: publicSchemaObject.shape,
220 |   handler: createSessionAwareTool<BuildRunMacOSParams>({
221 |     internalSchema: buildRunMacOSSchema as unknown as z.ZodType<BuildRunMacOSParams>,
222 |     logicFunction: buildRunMacOSLogic,
223 |     getExecutor: getDefaultCommandExecutor,
224 |     requirements: [
225 |       { allOf: ['scheme'], message: 'scheme is required' },
226 |       { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
227 |     ],
228 |     exclusivePairs: [['projectPath', 'workspacePath']],
229 |   }),
230 | };
231 | 
```

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

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | /**
  4 |  * XcodeBuildMCP Tools Documentation Updater
  5 |  *
  6 |  * Automatically updates docs/TOOLS.md with current tool and workflow information
  7 |  * using static AST analysis. Ensures documentation always reflects the actual codebase.
  8 |  *
  9 |  * Usage:
 10 |  *   npx tsx scripts/update-tools-docs.ts [--dry-run] [--verbose]
 11 |  *
 12 |  * Options:
 13 |  *   --dry-run, -d       Show what would be updated without making changes
 14 |  *   --verbose, -v       Show detailed information about the update process
 15 |  *   --help, -h         Show this help message
 16 |  */
 17 | 
 18 | import * as fs from 'fs';
 19 | import * as path from 'path';
 20 | import { fileURLToPath } from 'url';
 21 | import {
 22 |   getStaticToolAnalysis,
 23 |   type StaticAnalysisResult,
 24 |   type WorkflowInfo,
 25 | } from './analysis/tools-analysis.js';
 26 | 
 27 | // Get project paths
 28 | const __filename = fileURLToPath(import.meta.url);
 29 | const __dirname = path.dirname(__filename);
 30 | const projectRoot = path.resolve(__dirname, '..');
 31 | const docsPath = path.join(projectRoot, 'docs', 'TOOLS.md');
 32 | 
 33 | // CLI options
 34 | const args = process.argv.slice(2);
 35 | const options = {
 36 |   dryRun: args.includes('--dry-run') || args.includes('-d'),
 37 |   verbose: args.includes('--verbose') || args.includes('-v'),
 38 |   help: args.includes('--help') || args.includes('-h'),
 39 | };
 40 | 
 41 | const colors = {
 42 |   reset: '\x1b[0m',
 43 |   bright: '\x1b[1m',
 44 |   red: '\x1b[31m',
 45 |   green: '\x1b[32m',
 46 |   yellow: '\x1b[33m',
 47 |   blue: '\x1b[34m',
 48 |   cyan: '\x1b[36m',
 49 |   magenta: '\x1b[35m',
 50 | } as const;
 51 | 
 52 | if (options.help) {
 53 |   console.log(`
 54 | ${colors.bright}${colors.blue}XcodeBuildMCP Tools Documentation Updater${colors.reset}
 55 | 
 56 | Automatically updates docs/TOOLS.md with current tool and workflow information.
 57 | 
 58 | ${colors.bright}Usage:${colors.reset}
 59 |   npx tsx scripts/update-tools-docs.ts [options]
 60 | 
 61 | ${colors.bright}Options:${colors.reset}
 62 |   --dry-run, -d       Show what would be updated without making changes
 63 |   --verbose, -v       Show detailed information about the update process
 64 |   --help, -h         Show this help message
 65 | 
 66 | ${colors.bright}Examples:${colors.reset}
 67 |   ${colors.cyan}npx tsx scripts/update-tools-docs.ts${colors.reset}                    # Update docs/TOOLS.md
 68 |   ${colors.cyan}npx tsx scripts/update-tools-docs.ts --dry-run${colors.reset}          # Preview changes
 69 |   ${colors.cyan}npx tsx scripts/update-tools-docs.ts --verbose${colors.reset}          # Show detailed progress
 70 | `);
 71 |   process.exit(0);
 72 | }
 73 | 
 74 | /**
 75 |  * Generate the workflow section content
 76 |  */
 77 | function generateWorkflowSection(workflow: WorkflowInfo): string {
 78 |   const canonicalTools = workflow.tools.filter((tool) => tool.isCanonical);
 79 |   const toolCount = canonicalTools.length;
 80 | 
 81 |   let content = `### ${workflow.displayName} (\`${workflow.name}\`)\n`;
 82 |   content += `**Purpose**: ${workflow.description} (${toolCount} tools)\n\n`;
 83 | 
 84 |   // List each tool with its description
 85 |   for (const tool of canonicalTools.sort((a, b) => a.name.localeCompare(b.name))) {
 86 |     // Clean up the description for documentation
 87 |     const cleanDescription = tool.description
 88 |       .replace(/IMPORTANT:.*?Example:.*?\)/g, '') // Remove IMPORTANT sections
 89 |       .replace(/\s+/g, ' ') // Normalize whitespace
 90 |       .trim();
 91 | 
 92 |     content += `- \`${tool.name}\` - ${cleanDescription}\n`;
 93 |   }
 94 | 
 95 |   return content;
 96 | }
 97 | 
 98 | /**
 99 |  * Generate the complete TOOLS.md content
100 |  */
101 | function generateToolsDocumentation(analysis: StaticAnalysisResult): string {
102 |   const { workflows, stats } = analysis;
103 | 
104 |   // Sort workflows by display name for consistent ordering
105 |   const sortedWorkflows = workflows.sort((a, b) => a.displayName.localeCompare(b.displayName));
106 | 
107 |   const content = `# XcodeBuildMCP Tools Reference
108 | 
109 | XcodeBuildMCP provides ${stats.canonicalTools} tools organized into ${stats.workflowCount} workflow groups for comprehensive Apple development workflows.
110 | 
111 | ## Workflow Groups
112 | 
113 | ${sortedWorkflows.map((workflow) => generateWorkflowSection(workflow)).join('')}
114 | ## Summary Statistics
115 | 
116 | - **Total Tools**: ${stats.canonicalTools} canonical tools + ${stats.reExportTools} re-exports = ${stats.totalTools} total
117 | - **Workflow Groups**: ${stats.workflowCount}
118 | 
119 | ---
120 | 
121 | *This documentation is automatically generated by \`scripts/update-tools-docs.ts\` using static analysis. Last updated: ${new Date().toISOString().split('T')[0]}*
122 | `;
123 | 
124 |   return content;
125 | }
126 | 
127 | /**
128 |  * Compare old and new content to show what changed
129 |  */
130 | function showDiff(oldContent: string, newContent: string): void {
131 |   if (!options.verbose) return;
132 | 
133 |   console.log(`${colors.bright}${colors.cyan}📄 Content Comparison:${colors.reset}`);
134 |   console.log('─'.repeat(50));
135 | 
136 |   const oldLines = oldContent.split('\n');
137 |   const newLines = newContent.split('\n');
138 | 
139 |   const maxLength = Math.max(oldLines.length, newLines.length);
140 |   let changes = 0;
141 | 
142 |   for (let i = 0; i < maxLength; i++) {
143 |     const oldLine = oldLines[i] || '';
144 |     const newLine = newLines[i] || '';
145 | 
146 |     if (oldLine !== newLine) {
147 |       changes++;
148 |       if (changes <= 10) {
149 |         // Show first 10 changes
150 |         console.log(`${colors.red}- Line ${i + 1}: ${oldLine}${colors.reset}`);
151 |         console.log(`${colors.green}+ Line ${i + 1}: ${newLine}${colors.reset}`);
152 |       }
153 |     }
154 |   }
155 | 
156 |   if (changes > 10) {
157 |     console.log(`${colors.yellow}... and ${changes - 10} more changes${colors.reset}`);
158 |   }
159 | 
160 |   console.log(`${colors.blue}Total changes: ${changes} lines${colors.reset}\n`);
161 | }
162 | 
163 | /**
164 |  * Main execution function
165 |  */
166 | async function main(): Promise<void> {
167 |   try {
168 |     console.log(
169 |       `${colors.bright}${colors.blue}🔧 XcodeBuildMCP Tools Documentation Updater${colors.reset}`,
170 |     );
171 | 
172 |     if (options.dryRun) {
173 |       console.log(
174 |         `${colors.yellow}🔍 Running in dry-run mode - no files will be modified${colors.reset}`,
175 |       );
176 |     }
177 | 
178 |     console.log(`${colors.cyan}📊 Analyzing tools...${colors.reset}`);
179 | 
180 |     // Get current tool analysis
181 |     const analysis = await getStaticToolAnalysis();
182 | 
183 |     if (options.verbose) {
184 |       console.log(
185 |         `${colors.green}✓ Found ${analysis.stats.canonicalTools} canonical tools in ${analysis.stats.workflowCount} workflows${colors.reset}`,
186 |       );
187 |       console.log(
188 |         `${colors.green}✓ Found ${analysis.stats.reExportTools} re-export files${colors.reset}`,
189 |       );
190 |     }
191 | 
192 |     // Generate new documentation content
193 |     console.log(`${colors.cyan}📝 Generating documentation...${colors.reset}`);
194 |     const newContent = generateToolsDocumentation(analysis);
195 | 
196 |     // Read current content for comparison
197 |     let oldContent = '';
198 |     if (fs.existsSync(docsPath)) {
199 |       oldContent = fs.readFileSync(docsPath, 'utf-8');
200 |     }
201 | 
202 |     // Check if content has changed
203 |     if (oldContent === newContent) {
204 |       console.log(`${colors.green}✅ Documentation is already up to date!${colors.reset}`);
205 |       return;
206 |     }
207 | 
208 |     // Show differences if verbose
209 |     if (oldContent && options.verbose) {
210 |       showDiff(oldContent, newContent);
211 |     }
212 | 
213 |     if (options.dryRun) {
214 |       console.log(
215 |         `${colors.yellow}📋 Dry run completed. Documentation would be updated with:${colors.reset}`,
216 |       );
217 |       console.log(`   - ${analysis.stats.canonicalTools} canonical tools`);
218 |       console.log(`   - ${analysis.stats.workflowCount} workflow groups`);
219 |       console.log(`   - ${newContent.split('\n').length} lines total`);
220 | 
221 |       if (!options.verbose) {
222 |         console.log(`\n${colors.cyan}💡 Use --verbose to see detailed changes${colors.reset}`);
223 |       }
224 | 
225 |       return;
226 |     }
227 | 
228 |     // Write new content
229 |     console.log(`${colors.cyan}✏️  Writing updated documentation...${colors.reset}`);
230 |     fs.writeFileSync(docsPath, newContent, 'utf-8');
231 | 
232 |     console.log(
233 |       `${colors.green}✅ Successfully updated ${path.relative(projectRoot, docsPath)}!${colors.reset}`,
234 |     );
235 | 
236 |     if (options.verbose) {
237 |       console.log(`\n${colors.bright}📈 Update Summary:${colors.reset}`);
238 |       console.log(
239 |         `   Tools: ${analysis.stats.canonicalTools} canonical + ${analysis.stats.reExportTools} re-exports = ${analysis.stats.totalTools} total`,
240 |       );
241 |       console.log(`   Workflows: ${analysis.stats.workflowCount}`);
242 |       console.log(`   File size: ${(newContent.length / 1024).toFixed(1)}KB`);
243 |       console.log(`   Lines: ${newContent.split('\n').length}`);
244 |     }
245 |   } catch (error) {
246 |     console.error(`${colors.red}❌ Error: ${(error as Error).message}${colors.reset}`);
247 |     process.exit(1);
248 |   }
249 | }
250 | 
251 | // Run the updater
252 | main();
253 | 
```

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

```markdown
  1 | # XcodeBuildMCP Code Quality Guide
  2 | 
  3 | This guide consolidates all code quality, linting, and architectural compliance information for the XcodeBuildMCP project.
  4 | 
  5 | ## Table of Contents
  6 | 
  7 | 1. [Overview](#overview)
  8 | 2. [ESLint Configuration](#eslint-configuration)
  9 | 3. [Architectural Rules](#architectural-rules)
 10 | 4. [Development Scripts](#development-scripts)
 11 | 5. [Code Pattern Violations](#code-pattern-violations)
 12 | 6. [Type Safety Migration](#type-safety-migration)
 13 | 7. [Best Practices](#best-practices)
 14 | 
 15 | ## Overview
 16 | 
 17 | XcodeBuildMCP enforces code quality through multiple layers:
 18 | 
 19 | 1. **ESLint**: Handles general code quality, TypeScript rules, and stylistic consistency
 20 | 2. **TypeScript**: Enforces type safety with strict mode
 21 | 3. **Pattern Checker**: Enforces XcodeBuildMCP-specific architectural rules
 22 | 4. **Migration Scripts**: Track progress on type safety improvements
 23 | 
 24 | ## ESLint Configuration
 25 | 
 26 | ### Current Configuration
 27 | 
 28 | The project uses a comprehensive ESLint setup that covers:
 29 | 
 30 | - TypeScript type safety rules
 31 | - Code style consistency
 32 | - Import ordering
 33 | - Unused variable detection
 34 | - Testing best practices
 35 | 
 36 | ### ESLint Rules
 37 | 
 38 | For detailed ESLint rules and rationale, see [ESLINT_RULES.md](./ESLINT_RULES.md).
 39 | 
 40 | ### Running ESLint
 41 | 
 42 | ```bash
 43 | # Check for linting issues
 44 | npm run lint
 45 | 
 46 | # Auto-fix linting issues
 47 | npm run lint:fix
 48 | ```
 49 | 
 50 | ## Architectural Rules
 51 | 
 52 | XcodeBuildMCP enforces several architectural patterns that cannot be expressed through ESLint:
 53 | 
 54 | ### 1. Dependency Injection Pattern
 55 | 
 56 | **Rule**: All tools must use dependency injection for external interactions.
 57 | 
 58 | ✅ **Allowed**:
 59 | - `createMockExecutor()` for command execution mocking
 60 | - `createMockFileSystemExecutor()` for file system mocking
 61 | - Logic functions accepting `executor?: CommandExecutor` parameter
 62 | 
 63 | ❌ **Forbidden**:
 64 | - Direct use of `vi.mock()`, `vi.fn()`, or any Vitest mocking
 65 | - Direct calls to `execSync`, `spawn`, or `exec` in production code
 66 | - Testing handler functions directly
 67 | 
 68 | ### 2. Handler Signature Compliance
 69 | 
 70 | **Rule**: MCP handlers must have exact signatures as required by the SDK.
 71 | 
 72 | ✅ **Tool Handler Signature**:
 73 | ```typescript
 74 | async handler(args: Record<string, unknown>): Promise<ToolResponse>
 75 | ```
 76 | 
 77 | ✅ **Resource Handler Signature**:
 78 | ```typescript
 79 | async handler(uri: URL): Promise<{ contents: Array<{ text: string }> }>
 80 | ```
 81 | 
 82 | ❌ **Forbidden**:
 83 | - Multiple parameters in handlers
 84 | - Optional parameters
 85 | - Dependency injection parameters in handlers
 86 | 
 87 | ### 3. Testing Architecture
 88 | 
 89 | **Rule**: Tests must only call logic functions, never handlers directly.
 90 | 
 91 | ✅ **Correct Pattern**:
 92 | ```typescript
 93 | const result = await myToolLogic(params, mockExecutor);
 94 | ```
 95 | 
 96 | ❌ **Forbidden Pattern**:
 97 | ```typescript
 98 | const result = await myTool.handler(params);
 99 | ```
100 | 
101 | ### 4. Server Type Safety
102 | 
103 | **Rule**: MCP server instances must use proper SDK types, not generic casts.
104 | 
105 | ✅ **Correct Pattern**:
106 | ```typescript
107 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
108 | const server = (globalThis as { mcpServer?: McpServer }).mcpServer;
109 | server.server.createMessage({...});
110 | ```
111 | 
112 | ❌ **Forbidden Pattern**:
113 | ```typescript
114 | const server = (globalThis as { mcpServer?: Record<string, unknown> }).mcpServer;
115 | const serverInstance = (server.server ?? server) as Record<string, unknown> & {...};
116 | ```
117 | 
118 | ## Development Scripts
119 | 
120 | ### Core Scripts
121 | 
122 | ```bash
123 | # Build the project
124 | npm run build
125 | 
126 | # Run type checking
127 | npm run typecheck
128 | 
129 | # Run tests
130 | npm run test
131 | 
132 | # Check code patterns (architectural compliance)
133 | node scripts/check-code-patterns.js
134 | 
135 | # Check type safety migration progress
136 | npm run check-migration
137 | ```
138 | 
139 | ### Pattern Checker Usage
140 | 
141 | The pattern checker enforces XcodeBuildMCP-specific architectural rules:
142 | 
143 | ```bash
144 | # Check all patterns
145 | node scripts/check-code-patterns.js
146 | 
147 | # Check specific pattern type
148 | node scripts/check-code-patterns.js --pattern=vitest
149 | node scripts/check-code-patterns.js --pattern=execsync
150 | node scripts/check-code-patterns.js --pattern=handler
151 | node scripts/check-code-patterns.js --pattern=handler-testing
152 | node scripts/check-code-patterns.js --pattern=server-typing
153 | 
154 | # Get help
155 | node scripts/check-code-patterns.js --help
156 | ```
157 | 
158 | ### Tool Summary Scripts
159 | 
160 | ```bash
161 | # Show tool and resource summary
162 | npm run tools
163 | 
164 | # List all tools
165 | npm run tools:list
166 | 
167 | # List both tools and resources
168 | npm run tools:all
169 | ```
170 | 
171 | ## Code Pattern Violations
172 | 
173 | The pattern checker identifies the following violations:
174 | 
175 | ### 1. Vitest Mocking Violations
176 | 
177 | **What**: Any use of Vitest mocking functions
178 | **Why**: Breaks dependency injection architecture
179 | **Fix**: Use `createMockExecutor()` instead
180 | 
181 | ### 2. ExecSync Violations
182 | 
183 | **What**: Direct use of Node.js child_process functions in production code
184 | **Why**: Bypasses CommandExecutor dependency injection
185 | **Fix**: Accept `CommandExecutor` parameter and use it
186 | 
187 | ### 3. Handler Signature Violations
188 | 
189 | **What**: Handlers with incorrect parameter signatures
190 | **Why**: MCP SDK requires exact signatures
191 | **Fix**: Move dependencies inside handler body
192 | 
193 | ### 4. Handler Testing Violations
194 | 
195 | **What**: Tests calling `.handler()` directly
196 | **Why**: Violates dependency injection principle
197 | **Fix**: Test logic functions instead
198 | 
199 | ### 5. Improper Server Typing Violations
200 | 
201 | **What**: Casting MCP server instances to `Record<string, unknown>` or using custom interfaces instead of SDK types
202 | **Why**: Breaks type safety and prevents proper API usage
203 | **Fix**: Import `McpServer` from SDK and use proper typing instead of generic casts
204 | 
205 | ## Type Safety Migration
206 | 
207 | The project is migrating to improved type safety using the `createTypedTool` factory:
208 | 
209 | ### Check Migration Status
210 | 
211 | ```bash
212 | # Show summary
213 | npm run check-migration
214 | 
215 | # Show detailed analysis
216 | npm run check-migration:verbose
217 | 
218 | # Show only unmigrated tools
219 | npm run check-migration:unfixed
220 | ```
221 | 
222 | ### Migration Benefits
223 | 
224 | 1. **Compile-time type safety** for tool parameters
225 | 2. **Automatic Zod schema validation**
226 | 3. **Better IDE support** and autocomplete
227 | 4. **Consistent error handling**
228 | 
229 | ## Best Practices
230 | 
231 | ### 1. Before Committing
232 | 
233 | Always run these checks before committing:
234 | 
235 | ```bash
236 | npm run build          # Ensure code compiles
237 | npm run typecheck      # Check TypeScript types
238 | npm run lint           # Check linting rules
239 | npm run test           # Run tests
240 | node scripts/check-code-patterns.js  # Check architectural compliance
241 | ```
242 | 
243 | ### 2. Adding New Tools
244 | 
245 | 1. Use dependency injection pattern
246 | 2. Follow handler signature requirements
247 | 3. Create comprehensive tests (test logic, not handlers)
248 | 4. Use `createTypedTool` factory for type safety
249 | 5. Document parameter schemas clearly
250 | 
251 | ### 3. Writing Tests
252 | 
253 | 1. Import the logic function, not the default export
254 | 2. Use `createMockExecutor()` for mocking
255 | 3. Test three dimensions: validation, command generation, output processing
256 | 4. Never test handlers directly
257 | 
258 | ### 4. Code Organization
259 | 
260 | 1. Keep tools in appropriate workflow directories
261 | 2. Share common tools via `-shared` directories
262 | 3. Re-export shared tools, don't duplicate
263 | 4. Follow naming conventions for tools
264 | 
265 | ## Automated Enforcement
266 | 
267 | The project uses multiple layers of automated enforcement:
268 | 
269 | 1. **Pre-commit**: ESLint and TypeScript checks (if configured)
270 | 2. **CI Pipeline**: All checks run on every PR
271 | 3. **PR Blocking**: Checks must pass before merge
272 | 4. **Code Review**: Automated and manual review processes
273 | 
274 | ## Troubleshooting
275 | 
276 | ### ESLint False Positives
277 | 
278 | If ESLint reports false positives in test files, check that:
279 | 1. Test files are properly configured in `.eslintrc.json`
280 | 2. Test-specific rules are applied correctly
281 | 3. File patterns match your test file locations
282 | 
283 | ### Pattern Checker Issues
284 | 
285 | If the pattern checker reports unexpected violations:
286 | 1. Check if it's a legitimate architectural violation
287 | 2. Verify the file is in the correct directory
288 | 3. Ensure you're using the latest pattern definitions
289 | 
290 | ### Type Safety Migration
291 | 
292 | If migration tooling reports incorrect status:
293 | 1. Ensure the tool exports follow standard patterns
294 | 2. Check that schema definitions are properly typed
295 | 3. Verify the handler uses the schema correctly
296 | 
297 | ## Future Improvements
298 | 
299 | 1. **Automated Fixes**: Add auto-fix capability to pattern checker
300 | 2. **IDE Integration**: Create VS Code extension for real-time checking
301 | 3. **Performance Metrics**: Add build and test performance tracking
302 | 4. **Complexity Analysis**: Add code complexity metrics
303 | 5. **Documentation Linting**: Add documentation quality checks
```

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

```yaml
  1 | name: Droid Code Review
  2 | 
  3 | on:
  4 |   pull_request:
  5 |     types: [opened, synchronize, reopened, ready_for_review]
  6 | 
  7 | concurrency:
  8 |   group: droid-review-${{ github.event.pull_request.number }}
  9 |   cancel-in-progress: true
 10 | 
 11 | permissions:
 12 |   pull-requests: write
 13 |   contents: read
 14 |   issues: write
 15 | 
 16 | jobs:
 17 |   code-review:
 18 |     runs-on: ubuntu-latest
 19 |     timeout-minutes: 15
 20 |     # Skip automated code review for draft PRs
 21 |     if: github.event.pull_request.draft == false
 22 | 
 23 |     steps:
 24 |       - name: Checkout repository
 25 |         uses: actions/checkout@v4
 26 |         with:
 27 |           fetch-depth: 0
 28 |           ref: ${{ github.event.pull_request.head.sha }}
 29 | 
 30 |       - name: Install Droid CLI
 31 |         run: |
 32 |           curl -fsSL https://app.factory.ai/cli | sh
 33 |           echo "$HOME/.local/bin" >> $GITHUB_PATH
 34 |           "$HOME/.local/bin/droid" --version
 35 | 
 36 |       - name: Configure git identity
 37 |         run: |
 38 |           git config user.name "Droid Agent"
 39 |           git config user.email "[email protected]"
 40 | 
 41 |       - name: Prepare review context
 42 |         run: |
 43 |           # Get the PR diff
 44 |           git fetch origin ${{ github.event.pull_request.base.ref }}
 45 |           git diff origin/${{ github.event.pull_request.base.ref }}...${{ github.event.pull_request.head.sha }} > diff.txt
 46 | 
 47 |           # Get existing comments using GitHub API
 48 |           curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
 49 |                -H "Accept: application/vnd.github.v3+json" \
 50 |                "https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \
 51 |                > existing_comments.json
 52 | 
 53 |           # Get changed files with patches for positioning
 54 |           curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
 55 |                -H "Accept: application/vnd.github.v3+json" \
 56 |                "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \
 57 |                | jq '[.[] | {filename: .filename, patch: .patch}]' > files.json
 58 | 
 59 |       - name: Perform automated code review
 60 |         env:
 61 |           FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
 62 |           GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 63 |         run: |
 64 |           cat > prompt.txt << 'EOF'
 65 |           You are an automated code review system. Review the PR diff and identify clear issues that need to be fixed.
 66 | 
 67 |           Input files (already in current directory):
 68 |           - diff.txt: the code changes to review
 69 |           - files.json: file patches with line numbers for positioning comments
 70 |           - existing_comments.json: skip issues already mentioned here
 71 | 
 72 |           Task: Create a file called comments.json with this exact format:
 73 |           [{ "path": "path/to/file.js", "position": 42, "body": "Your comment here" }]
 74 | 
 75 |           Focus on these types of issues:
 76 |           - Dead/unreachable code (if (false), while (false), code after return/throw/break)
 77 |           - Broken control flow (missing break in switch, fallthrough bugs)
 78 |           - Async/await mistakes (missing await, .then without return, unhandled promise rejections)
 79 |           - Array/object mutations in React components or reducers
 80 |           - UseEffect dependency array problems (missing deps, incorrect deps)
 81 |           - Incorrect operator usage (== vs ===, && vs ||, = in conditions)
 82 |           - Off-by-one errors in loops or array indexing
 83 |           - Integer overflow/underflow in calculations
 84 |           - Regex catastrophic backtracking vulnerabilities
 85 |           - Missing base cases in recursive functions
 86 |           - Incorrect type coercion that changes behavior
 87 |           - Environment variable access without defaults or validation
 88 |           - Null/undefined dereferences
 89 |           - Resource leaks (unclosed files or connections)
 90 |           - SQL/XSS injection vulnerabilities
 91 |           - Concurrency/race conditions
 92 |           - Missing error handling for critical operations
 93 | 
 94 |           Comment format:
 95 |           - Clearly describe the issue: "This code block is unreachable due to the if (false) condition"
 96 |           - Provide a concrete fix: "Remove this entire if block as it will never execute"
 97 |           - When possible, suggest the exact code change:
 98 |             ```suggestion
 99 |             // Remove the unreachable code
100 |             ```
101 |           - Be specific about why it's a problem: "This will cause a TypeError if input is null"
102 |           - No emojis, just clear technical language
103 | 
104 |           Skip commenting on:
105 |           - Code style, formatting, or naming conventions
106 |           - Minor performance optimizations
107 |           - Architectural decisions or design patterns
108 |           - Features or functionality (unless broken)
109 |           - Test coverage (unless tests are clearly broken)
110 | 
111 |           Position calculation:
112 |           - Use the "position" field from files.json patches
113 |           - This is the line number in the diff, not the file
114 |           - Comments must align with exact changed lines only
115 | 
116 |           Output: 
117 |           - Empty array [] if no issues found
118 |           - Otherwise array of comment objects with path, position, body
119 |           - Each comment should be actionable and clear about what needs to be fixed
120 |           - Maximum 10 comments total; prioritize the most critical issues
121 |           EOF
122 | 
123 |           # Run droid exec with the prompt
124 |           echo "Running code review analysis..."
125 |           droid exec --auto high -f prompt.txt
126 | 
127 |           # Check if comments.json was created
128 |           if [ ! -f comments.json ]; then
129 |             echo "❌ ERROR: droid exec did not create comments.json"
130 |             echo "This usually indicates the review run failed (e.g. missing FACTORY_API_KEY or runtime error)."
131 |             exit 1
132 |           fi
133 | 
134 |           echo "=== Review Results ==="
135 |           cat comments.json
136 | 
137 |       - name: Submit inline review comments
138 |         uses: actions/github-script@v7
139 |         with:
140 |           script: |
141 |             const fs = require('fs');
142 |             const prNumber = context.payload.pull_request.number;
143 | 
144 |             if (!fs.existsSync('comments.json')) {
145 |               core.info('comments.json missing; skipping review submission');
146 |               return;
147 |             }
148 | 
149 |             const comments = JSON.parse(fs.readFileSync('comments.json', 'utf8'));
150 | 
151 |             if (!Array.isArray(comments) || comments.length === 0) {
152 |               // Check if we already have a "no issues" comment
153 |               const existing = await github.paginate(github.rest.issues.listComments, {
154 |                 owner: context.repo.owner,
155 |                 repo: context.repo.repo,
156 |                 issue_number: prNumber,
157 |                 per_page: 100
158 |               });
159 |               
160 |               const hasNoIssuesComment = existing.some(c => 
161 |                 c.user.login.includes('[bot]') && 
162 |                 /no issues found|lgtm|✅/i.test(c.body || '')
163 |               );
164 |               
165 |               if (!hasNoIssuesComment) {
166 |                 await github.rest.pulls.createReview({
167 |                   owner: context.repo.owner,
168 |                   repo: context.repo.repo,
169 |                   pull_number: prNumber,
170 |                   event: 'COMMENT',
171 |                   body: '✅ No issues found in the current changes.'
172 |                 });
173 |               }
174 |               return;
175 |             }
176 | 
177 |             // Submit review with inline comments
178 |             const summary = `Found ${comments.length} potential issue${comments.length === 1 ? '' : 's'} that should be addressed.`;
179 | 
180 |             await github.rest.pulls.createReview({
181 |               owner: context.repo.owner,
182 |               repo: context.repo.repo,
183 |               pull_number: prNumber,
184 |               event: 'COMMENT',
185 |               body: summary,
186 |               comments: comments
187 |             });
188 | 
189 |             core.info(`Submitted review with ${comments.length} inline comments`);
190 | 
191 |       - name: Upload debug artifacts on failure
192 |         if: ${{ failure() }}
193 |         uses: actions/upload-artifact@v4
194 |         with:
195 |           name: droid-review-debug-${{ github.run_id }}
196 |           path: |
197 |             diff.txt
198 |             files.json
199 |             existing_comments.json
200 |             prompt.txt
201 |             comments.json
202 |             ${{ runner.home }}/.factory/logs/droid-log-single.log
203 |             ${{ runner.home }}/.factory/logs/console.log
204 |           if-no-files-found: ignore
205 |           retention-days: 7
206 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for stop_app_device plugin (device-shared)
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using dependency injection for deterministic testing
  5 |  */
  6 | 
  7 | import { describe, it, expect, beforeEach } from 'vitest';
  8 | import { z } from 'zod';
  9 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
 10 | import stopAppDevice, { stop_app_deviceLogic } from '../stop_app_device.ts';
 11 | import { sessionStore } from '../../../../utils/session-store.ts';
 12 | 
 13 | describe('stop_app_device plugin', () => {
 14 |   beforeEach(() => {
 15 |     sessionStore.clear();
 16 |   });
 17 | 
 18 |   describe('Export Field Validation (Literal)', () => {
 19 |     it('should have correct name', () => {
 20 |       expect(stopAppDevice.name).toBe('stop_app_device');
 21 |     });
 22 | 
 23 |     it('should have correct description', () => {
 24 |       expect(stopAppDevice.description).toBe('Stops a running app on a connected device.');
 25 |     });
 26 | 
 27 |     it('should have handler function', () => {
 28 |       expect(typeof stopAppDevice.handler).toBe('function');
 29 |     });
 30 | 
 31 |     it('should require processId in public schema', () => {
 32 |       const schema = z.object(stopAppDevice.schema).strict();
 33 |       expect(schema.safeParse({ processId: 12345 }).success).toBe(true);
 34 |       expect(schema.safeParse({}).success).toBe(false);
 35 |       expect(schema.safeParse({ deviceId: 'test-device-123' }).success).toBe(false);
 36 | 
 37 |       expect(Object.keys(stopAppDevice.schema)).toEqual(['processId']);
 38 |     });
 39 |   });
 40 | 
 41 |   describe('Handler Requirements', () => {
 42 |     it('should require deviceId when not provided', async () => {
 43 |       const result = await stopAppDevice.handler({ processId: 12345 });
 44 | 
 45 |       expect(result.isError).toBe(true);
 46 |       expect(result.content[0].text).toContain('deviceId is required');
 47 |     });
 48 |   });
 49 | 
 50 |   describe('Command Generation', () => {
 51 |     it('should generate correct devicectl command with basic parameters', async () => {
 52 |       let capturedCommand: unknown[] = [];
 53 |       let capturedDescription: string = '';
 54 |       let capturedUseShell: boolean = false;
 55 |       let capturedEnv: unknown = undefined;
 56 | 
 57 |       const mockExecutor = createMockExecutor({
 58 |         success: true,
 59 |         output: 'App terminated successfully',
 60 |         process: { pid: 12345 },
 61 |       });
 62 | 
 63 |       const trackingExecutor = async (
 64 |         command: unknown[],
 65 |         description: string,
 66 |         useShell: boolean,
 67 |         env: unknown,
 68 |       ) => {
 69 |         capturedCommand = command;
 70 |         capturedDescription = description;
 71 |         capturedUseShell = useShell;
 72 |         capturedEnv = env;
 73 |         return mockExecutor(command, description, useShell, env);
 74 |       };
 75 | 
 76 |       await stop_app_deviceLogic(
 77 |         {
 78 |           deviceId: 'test-device-123',
 79 |           processId: 12345,
 80 |         },
 81 |         trackingExecutor,
 82 |       );
 83 | 
 84 |       expect(capturedCommand).toEqual([
 85 |         'xcrun',
 86 |         'devicectl',
 87 |         'device',
 88 |         'process',
 89 |         'terminate',
 90 |         '--device',
 91 |         'test-device-123',
 92 |         '--pid',
 93 |         '12345',
 94 |       ]);
 95 |       expect(capturedDescription).toBe('Stop app on device');
 96 |       expect(capturedUseShell).toBe(true);
 97 |       expect(capturedEnv).toBe(undefined);
 98 |     });
 99 | 
100 |     it('should generate correct command with different device ID and process ID', async () => {
101 |       let capturedCommand: unknown[] = [];
102 | 
103 |       const mockExecutor = createMockExecutor({
104 |         success: true,
105 |         output: 'Process terminated',
106 |         process: { pid: 12345 },
107 |       });
108 | 
109 |       const trackingExecutor = async (command: unknown[]) => {
110 |         capturedCommand = command;
111 |         return mockExecutor(command);
112 |       };
113 | 
114 |       await stop_app_deviceLogic(
115 |         {
116 |           deviceId: 'different-device-uuid',
117 |           processId: 99999,
118 |         },
119 |         trackingExecutor,
120 |       );
121 | 
122 |       expect(capturedCommand).toEqual([
123 |         'xcrun',
124 |         'devicectl',
125 |         'device',
126 |         'process',
127 |         'terminate',
128 |         '--device',
129 |         'different-device-uuid',
130 |         '--pid',
131 |         '99999',
132 |       ]);
133 |     });
134 | 
135 |     it('should generate correct command with large process ID', async () => {
136 |       let capturedCommand: unknown[] = [];
137 | 
138 |       const mockExecutor = createMockExecutor({
139 |         success: true,
140 |         output: 'Process terminated',
141 |         process: { pid: 12345 },
142 |       });
143 | 
144 |       const trackingExecutor = async (command: unknown[]) => {
145 |         capturedCommand = command;
146 |         return mockExecutor(command);
147 |       };
148 | 
149 |       await stop_app_deviceLogic(
150 |         {
151 |           deviceId: 'test-device-123',
152 |           processId: 2147483647,
153 |         },
154 |         trackingExecutor,
155 |       );
156 | 
157 |       expect(capturedCommand).toEqual([
158 |         'xcrun',
159 |         'devicectl',
160 |         'device',
161 |         'process',
162 |         'terminate',
163 |         '--device',
164 |         'test-device-123',
165 |         '--pid',
166 |         '2147483647',
167 |       ]);
168 |     });
169 |   });
170 | 
171 |   describe('Success Path Tests', () => {
172 |     it('should return successful stop response', async () => {
173 |       const mockExecutor = createMockExecutor({
174 |         success: true,
175 |         output: 'App terminated successfully',
176 |       });
177 | 
178 |       const result = await stop_app_deviceLogic(
179 |         {
180 |           deviceId: 'test-device-123',
181 |           processId: 12345,
182 |         },
183 |         mockExecutor,
184 |       );
185 | 
186 |       expect(result).toEqual({
187 |         content: [
188 |           {
189 |             type: 'text',
190 |             text: '✅ App stopped successfully\n\nApp terminated successfully',
191 |           },
192 |         ],
193 |       });
194 |     });
195 | 
196 |     it('should return successful stop with detailed output', async () => {
197 |       const mockExecutor = createMockExecutor({
198 |         success: true,
199 |         output: 'Terminating process...\nProcess ID: 12345\nTermination completed successfully',
200 |       });
201 | 
202 |       const result = await stop_app_deviceLogic(
203 |         {
204 |           deviceId: 'device-456',
205 |           processId: 67890,
206 |         },
207 |         mockExecutor,
208 |       );
209 | 
210 |       expect(result).toEqual({
211 |         content: [
212 |           {
213 |             type: 'text',
214 |             text: '✅ App stopped successfully\n\nTerminating process...\nProcess ID: 12345\nTermination completed successfully',
215 |           },
216 |         ],
217 |       });
218 |     });
219 | 
220 |     it('should return successful stop with empty output', async () => {
221 |       const mockExecutor = createMockExecutor({
222 |         success: true,
223 |         output: '',
224 |       });
225 | 
226 |       const result = await stop_app_deviceLogic(
227 |         {
228 |           deviceId: 'empty-output-device',
229 |           processId: 54321,
230 |         },
231 |         mockExecutor,
232 |       );
233 | 
234 |       expect(result).toEqual({
235 |         content: [
236 |           {
237 |             type: 'text',
238 |             text: '✅ App stopped successfully\n\n',
239 |           },
240 |         ],
241 |       });
242 |     });
243 |   });
244 | 
245 |   describe('Error Handling', () => {
246 |     it('should return stop failure response', async () => {
247 |       const mockExecutor = createMockExecutor({
248 |         success: false,
249 |         error: 'Terminate failed: Process not found',
250 |       });
251 | 
252 |       const result = await stop_app_deviceLogic(
253 |         {
254 |           deviceId: 'test-device-123',
255 |           processId: 99999,
256 |         },
257 |         mockExecutor,
258 |       );
259 | 
260 |       expect(result).toEqual({
261 |         content: [
262 |           {
263 |             type: 'text',
264 |             text: 'Failed to stop app: Terminate failed: Process not found',
265 |           },
266 |         ],
267 |         isError: true,
268 |       });
269 |     });
270 | 
271 |     it('should return exception handling response', async () => {
272 |       const mockExecutor = createMockExecutor(new Error('Network error'));
273 | 
274 |       const result = await stop_app_deviceLogic(
275 |         {
276 |           deviceId: 'test-device-123',
277 |           processId: 12345,
278 |         },
279 |         mockExecutor,
280 |       );
281 | 
282 |       expect(result).toEqual({
283 |         content: [
284 |           {
285 |             type: 'text',
286 |             text: 'Failed to stop app on device: Network error',
287 |           },
288 |         ],
289 |         isError: true,
290 |       });
291 |     });
292 | 
293 |     it('should return string error handling response', async () => {
294 |       const mockExecutor = createMockExecutor('String error');
295 | 
296 |       const result = await stop_app_deviceLogic(
297 |         {
298 |           deviceId: 'test-device-123',
299 |           processId: 12345,
300 |         },
301 |         mockExecutor,
302 |       );
303 | 
304 |       expect(result).toEqual({
305 |         content: [
306 |           {
307 |             type: 'text',
308 |             text: 'Failed to stop app on device: String error',
309 |           },
310 |         ],
311 |         isError: true,
312 |       });
313 |     });
314 |   });
315 | });
316 | 
```

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

```typescript
  1 | /**
  2 |  * Common Test Utilities - Shared logic for test tools
  3 |  *
  4 |  * This module provides shared functionality for all test-related tools across different platforms.
  5 |  * It includes common test execution logic, xcresult parsing, and utility functions used by
  6 |  * platform-specific test tools.
  7 |  *
  8 |  * Responsibilities:
  9 |  * - Parsing xcresult bundles into human-readable format
 10 |  * - Shared test execution logic with platform-specific handling
 11 |  * - Common error handling and cleanup for test operations
 12 |  * - Temporary directory management for xcresult files
 13 |  */
 14 | 
 15 | import { promisify } from 'util';
 16 | import { exec } from 'child_process';
 17 | import { mkdtemp, rm } from 'fs/promises';
 18 | import { tmpdir } from 'os';
 19 | import { join } from 'path';
 20 | import { log } from './logger.ts';
 21 | import { XcodePlatform } from './xcode.ts';
 22 | import { executeXcodeBuildCommand } from './build/index.ts';
 23 | import { createTextResponse, consolidateContentForClaudeCode } from './validation.ts';
 24 | import { normalizeTestRunnerEnv } from './environment.ts';
 25 | import { ToolResponse } from '../types/common.ts';
 26 | import { CommandExecutor, CommandExecOptions, getDefaultCommandExecutor } from './command.ts';
 27 | 
 28 | /**
 29 |  * Type definition for test summary structure from xcresulttool
 30 |  */
 31 | interface TestSummary {
 32 |   title?: string;
 33 |   result?: string;
 34 |   totalTestCount?: number;
 35 |   passedTests?: number;
 36 |   failedTests?: number;
 37 |   skippedTests?: number;
 38 |   expectedFailures?: number;
 39 |   environmentDescription?: string;
 40 |   devicesAndConfigurations?: Array<{
 41 |     device?: {
 42 |       deviceName?: string;
 43 |       platform?: string;
 44 |       osVersion?: string;
 45 |     };
 46 |   }>;
 47 |   testFailures?: Array<{
 48 |     testName?: string;
 49 |     targetName?: string;
 50 |     failureText?: string;
 51 |   }>;
 52 |   topInsights?: Array<{
 53 |     impact?: string;
 54 |     text?: string;
 55 |   }>;
 56 | }
 57 | 
 58 | /**
 59 |  * Parse xcresult bundle using xcrun xcresulttool
 60 |  */
 61 | export async function parseXcresultBundle(resultBundlePath: string): Promise<string> {
 62 |   try {
 63 |     const execAsync = promisify(exec);
 64 |     const { stdout } = await execAsync(
 65 |       `xcrun xcresulttool get test-results summary --path "${resultBundlePath}"`,
 66 |     );
 67 | 
 68 |     // Parse JSON response and format as human-readable
 69 |     const summary = JSON.parse(stdout) as TestSummary;
 70 |     return formatTestSummary(summary);
 71 |   } catch (error) {
 72 |     const errorMessage = error instanceof Error ? error.message : String(error);
 73 |     log('error', `Error parsing xcresult bundle: ${errorMessage}`);
 74 |     throw error;
 75 |   }
 76 | }
 77 | 
 78 | /**
 79 |  * Format test summary JSON into human-readable text
 80 |  */
 81 | function formatTestSummary(summary: TestSummary): string {
 82 |   const lines: string[] = [];
 83 | 
 84 |   lines.push(`Test Summary: ${summary.title ?? 'Unknown'}`);
 85 |   lines.push(`Overall Result: ${summary.result ?? 'Unknown'}`);
 86 |   lines.push('');
 87 | 
 88 |   lines.push('Test Counts:');
 89 |   lines.push(`  Total: ${summary.totalTestCount ?? 0}`);
 90 |   lines.push(`  Passed: ${summary.passedTests ?? 0}`);
 91 |   lines.push(`  Failed: ${summary.failedTests ?? 0}`);
 92 |   lines.push(`  Skipped: ${summary.skippedTests ?? 0}`);
 93 |   lines.push(`  Expected Failures: ${summary.expectedFailures ?? 0}`);
 94 |   lines.push('');
 95 | 
 96 |   if (summary.environmentDescription) {
 97 |     lines.push(`Environment: ${summary.environmentDescription}`);
 98 |     lines.push('');
 99 |   }
100 | 
101 |   if (
102 |     summary.devicesAndConfigurations &&
103 |     Array.isArray(summary.devicesAndConfigurations) &&
104 |     summary.devicesAndConfigurations.length > 0
105 |   ) {
106 |     const device = summary.devicesAndConfigurations[0].device;
107 |     if (device) {
108 |       lines.push(
109 |         `Device: ${device.deviceName ?? 'Unknown'} (${device.platform ?? 'Unknown'} ${device.osVersion ?? 'Unknown'})`,
110 |       );
111 |       lines.push('');
112 |     }
113 |   }
114 | 
115 |   if (
116 |     summary.testFailures &&
117 |     Array.isArray(summary.testFailures) &&
118 |     summary.testFailures.length > 0
119 |   ) {
120 |     lines.push('Test Failures:');
121 |     summary.testFailures.forEach((failure, index: number) => {
122 |       lines.push(
123 |         `  ${index + 1}. ${failure.testName ?? 'Unknown Test'} (${failure.targetName ?? 'Unknown Target'})`,
124 |       );
125 |       if (failure.failureText) {
126 |         lines.push(`     ${failure.failureText}`);
127 |       }
128 |     });
129 |     lines.push('');
130 |   }
131 | 
132 |   if (summary.topInsights && Array.isArray(summary.topInsights) && summary.topInsights.length > 0) {
133 |     lines.push('Insights:');
134 |     summary.topInsights.forEach((insight, index: number) => {
135 |       lines.push(
136 |         `  ${index + 1}. [${insight.impact ?? 'Unknown'}] ${insight.text ?? 'No description'}`,
137 |       );
138 |     });
139 |   }
140 | 
141 |   return lines.join('\n');
142 | }
143 | 
144 | /**
145 |  * Internal logic for running tests with platform-specific handling
146 |  */
147 | export async function handleTestLogic(
148 |   params: {
149 |     workspacePath?: string;
150 |     projectPath?: string;
151 |     scheme: string;
152 |     configuration: string;
153 |     simulatorName?: string;
154 |     simulatorId?: string;
155 |     deviceId?: string;
156 |     useLatestOS?: boolean;
157 |     derivedDataPath?: string;
158 |     extraArgs?: string[];
159 |     preferXcodebuild?: boolean;
160 |     platform: XcodePlatform;
161 |     testRunnerEnv?: Record<string, string>;
162 |   },
163 |   executor?: CommandExecutor,
164 | ): Promise<ToolResponse> {
165 |   log(
166 |     'info',
167 |     `Starting test run for scheme ${params.scheme} on platform ${params.platform} (internal)`,
168 |   );
169 | 
170 |   try {
171 |     // Create temporary directory for xcresult bundle
172 |     const tempDir = await mkdtemp(join(tmpdir(), 'xcodebuild-test-'));
173 |     const resultBundlePath = join(tempDir, 'TestResults.xcresult');
174 | 
175 |     // Add resultBundlePath to extraArgs
176 |     const extraArgs = [...(params.extraArgs ?? []), `-resultBundlePath`, resultBundlePath];
177 | 
178 |     // Prepare execution options with TEST_RUNNER_ environment variables
179 |     const execOpts: CommandExecOptions | undefined = params.testRunnerEnv
180 |       ? { env: normalizeTestRunnerEnv(params.testRunnerEnv) }
181 |       : undefined;
182 | 
183 |     // Run the test command
184 |     const testResult = await executeXcodeBuildCommand(
185 |       {
186 |         ...params,
187 |         extraArgs,
188 |       },
189 |       {
190 |         platform: params.platform,
191 |         simulatorName: params.simulatorName,
192 |         simulatorId: params.simulatorId,
193 |         deviceId: params.deviceId,
194 |         useLatestOS: params.useLatestOS,
195 |         logPrefix: 'Test Run',
196 |       },
197 |       params.preferXcodebuild,
198 |       'test',
199 |       executor ?? getDefaultCommandExecutor(),
200 |       execOpts,
201 |     );
202 | 
203 |     // Parse xcresult bundle if it exists, regardless of whether tests passed or failed
204 |     // Test failures are expected and should not prevent xcresult parsing
205 |     try {
206 |       log('info', `Attempting to parse xcresult bundle at: ${resultBundlePath}`);
207 | 
208 |       // Check if the file exists
209 |       try {
210 |         const { stat } = await import('fs/promises');
211 |         await stat(resultBundlePath);
212 |         log('info', `xcresult bundle exists at: ${resultBundlePath}`);
213 |       } catch {
214 |         log('warn', `xcresult bundle does not exist at: ${resultBundlePath}`);
215 |         throw new Error(`xcresult bundle not found at ${resultBundlePath}`);
216 |       }
217 | 
218 |       const testSummary = await parseXcresultBundle(resultBundlePath);
219 |       log('info', 'Successfully parsed xcresult bundle');
220 | 
221 |       // Clean up temporary directory
222 |       await rm(tempDir, { recursive: true, force: true });
223 | 
224 |       // Return combined result - preserve isError from testResult (test failures should be marked as errors)
225 |       const combinedResponse: ToolResponse = {
226 |         content: [
227 |           ...(testResult.content || []),
228 |           {
229 |             type: 'text',
230 |             text: '\nTest Results Summary:\n' + testSummary,
231 |           },
232 |         ],
233 |         isError: testResult.isError,
234 |       };
235 | 
236 |       // Apply Claude Code workaround if enabled
237 |       return consolidateContentForClaudeCode(combinedResponse);
238 |     } catch (parseError) {
239 |       // If parsing fails, return original test result
240 |       log('warn', `Failed to parse xcresult bundle: ${parseError}`);
241 | 
242 |       // Clean up temporary directory even if parsing fails
243 |       try {
244 |         await rm(tempDir, { recursive: true, force: true });
245 |       } catch (cleanupError) {
246 |         log('warn', `Failed to clean up temporary directory: ${cleanupError}`);
247 |       }
248 | 
249 |       return consolidateContentForClaudeCode(testResult);
250 |     }
251 |   } catch (error) {
252 |     const errorMessage = error instanceof Error ? error.message : String(error);
253 |     log('error', `Error during test run: ${errorMessage}`);
254 |     return consolidateContentForClaudeCode(
255 |       createTextResponse(`Error during test run: ${errorMessage}`, true),
256 |     );
257 |   }
258 | }
259 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for swift_package_test plugin
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using dependency injection for deterministic testing
  5 |  */
  6 | 
  7 | import { describe, it, expect } from 'vitest';
  8 | import {
  9 |   createMockExecutor,
 10 |   createMockFileSystemExecutor,
 11 |   createNoopExecutor,
 12 | } from '../../../../test-utils/mock-executors.ts';
 13 | import swiftPackageTest, { swift_package_testLogic } from '../swift_package_test.ts';
 14 | 
 15 | describe('swift_package_test plugin', () => {
 16 |   describe('Export Field Validation (Literal)', () => {
 17 |     it('should have correct name', () => {
 18 |       expect(swiftPackageTest.name).toBe('swift_package_test');
 19 |     });
 20 | 
 21 |     it('should have correct description', () => {
 22 |       expect(swiftPackageTest.description).toBe('Runs tests for a Swift Package with swift test');
 23 |     });
 24 | 
 25 |     it('should have handler function', () => {
 26 |       expect(typeof swiftPackageTest.handler).toBe('function');
 27 |     });
 28 | 
 29 |     it('should validate schema correctly', () => {
 30 |       // Test required fields
 31 |       expect(swiftPackageTest.schema.packagePath.safeParse('/test/package').success).toBe(true);
 32 |       expect(swiftPackageTest.schema.packagePath.safeParse('').success).toBe(true);
 33 | 
 34 |       // Test optional fields
 35 |       expect(swiftPackageTest.schema.testProduct.safeParse('MyTests').success).toBe(true);
 36 |       expect(swiftPackageTest.schema.testProduct.safeParse(undefined).success).toBe(true);
 37 |       expect(swiftPackageTest.schema.filter.safeParse('Test.*').success).toBe(true);
 38 |       expect(swiftPackageTest.schema.filter.safeParse(undefined).success).toBe(true);
 39 |       expect(swiftPackageTest.schema.configuration.safeParse('debug').success).toBe(true);
 40 |       expect(swiftPackageTest.schema.configuration.safeParse('release').success).toBe(true);
 41 |       expect(swiftPackageTest.schema.configuration.safeParse(undefined).success).toBe(true);
 42 |       expect(swiftPackageTest.schema.parallel.safeParse(true).success).toBe(true);
 43 |       expect(swiftPackageTest.schema.parallel.safeParse(undefined).success).toBe(true);
 44 |       expect(swiftPackageTest.schema.showCodecov.safeParse(true).success).toBe(true);
 45 |       expect(swiftPackageTest.schema.showCodecov.safeParse(undefined).success).toBe(true);
 46 |       expect(swiftPackageTest.schema.parseAsLibrary.safeParse(true).success).toBe(true);
 47 |       expect(swiftPackageTest.schema.parseAsLibrary.safeParse(undefined).success).toBe(true);
 48 | 
 49 |       // Test invalid inputs
 50 |       expect(swiftPackageTest.schema.packagePath.safeParse(null).success).toBe(false);
 51 |       expect(swiftPackageTest.schema.configuration.safeParse('invalid').success).toBe(false);
 52 |       expect(swiftPackageTest.schema.parallel.safeParse('yes').success).toBe(false);
 53 |       expect(swiftPackageTest.schema.showCodecov.safeParse('yes').success).toBe(false);
 54 |       expect(swiftPackageTest.schema.parseAsLibrary.safeParse('yes').success).toBe(false);
 55 |     });
 56 |   });
 57 | 
 58 |   describe('Command Generation Testing', () => {
 59 |     it('should build correct command for basic test', async () => {
 60 |       const calls: any[] = [];
 61 |       const mockExecutor = async (
 62 |         args: string[],
 63 |         name: string,
 64 |         hideOutput: boolean,
 65 |         workingDir: string | undefined,
 66 |       ) => {
 67 |         calls.push({ args, name, hideOutput, workingDir });
 68 |         return {
 69 |           success: true,
 70 |           output: 'Test Passed',
 71 |           error: undefined,
 72 |           process: { pid: 12345 },
 73 |         };
 74 |       };
 75 | 
 76 |       await swift_package_testLogic(
 77 |         {
 78 |           packagePath: '/test/package',
 79 |         },
 80 |         mockExecutor,
 81 |       );
 82 | 
 83 |       expect(calls).toHaveLength(1);
 84 |       expect(calls[0]).toEqual({
 85 |         args: ['swift', 'test', '--package-path', '/test/package'],
 86 |         name: 'Swift Package Test',
 87 |         hideOutput: true,
 88 |         workingDir: undefined,
 89 |       });
 90 |     });
 91 | 
 92 |     it('should build correct command with all parameters', async () => {
 93 |       const calls: any[] = [];
 94 |       const mockExecutor = async (
 95 |         args: string[],
 96 |         name: string,
 97 |         hideOutput: boolean,
 98 |         workingDir: string | undefined,
 99 |       ) => {
100 |         calls.push({ args, name, hideOutput, workingDir });
101 |         return {
102 |           success: true,
103 |           output: 'Tests completed',
104 |           error: undefined,
105 |           process: { pid: 12345 },
106 |         };
107 |       };
108 | 
109 |       await swift_package_testLogic(
110 |         {
111 |           packagePath: '/test/package',
112 |           testProduct: 'MyTests',
113 |           filter: 'Test.*',
114 |           configuration: 'release',
115 |           parallel: false,
116 |           showCodecov: true,
117 |           parseAsLibrary: true,
118 |         },
119 |         mockExecutor,
120 |       );
121 | 
122 |       expect(calls).toHaveLength(1);
123 |       expect(calls[0]).toEqual({
124 |         args: [
125 |           'swift',
126 |           'test',
127 |           '--package-path',
128 |           '/test/package',
129 |           '-c',
130 |           'release',
131 |           '--test-product',
132 |           'MyTests',
133 |           '--filter',
134 |           'Test.*',
135 |           '--no-parallel',
136 |           '--show-code-coverage',
137 |           '-Xswiftc',
138 |           '-parse-as-library',
139 |         ],
140 |         name: 'Swift Package Test',
141 |         hideOutput: true,
142 |         workingDir: undefined,
143 |       });
144 |     });
145 |   });
146 | 
147 |   describe('Response Logic Testing', () => {
148 |     it('should handle empty packagePath parameter', async () => {
149 |       // When packagePath is empty, the function should still process it
150 |       // but the command execution may fail, which is handled by the executor
151 |       const mockExecutor = createMockExecutor({
152 |         success: true,
153 |         output: 'Tests completed with empty path',
154 |       });
155 | 
156 |       const result = await swift_package_testLogic({ packagePath: '' }, mockExecutor);
157 | 
158 |       expect(result.isError).toBe(false);
159 |       expect(result.content[0].text).toBe('✅ Swift package tests completed.');
160 |     });
161 | 
162 |     it('should return successful test response', async () => {
163 |       const mockExecutor = createMockExecutor({
164 |         success: true,
165 |         output: 'All tests passed.',
166 |       });
167 | 
168 |       const result = await swift_package_testLogic(
169 |         {
170 |           packagePath: '/test/package',
171 |         },
172 |         mockExecutor,
173 |       );
174 | 
175 |       expect(result).toEqual({
176 |         content: [
177 |           { type: 'text', text: '✅ Swift package tests completed.' },
178 |           {
179 |             type: 'text',
180 |             text: '💡 Next: Execute your app with swift_package_run if tests passed',
181 |           },
182 |           { type: 'text', text: 'All tests passed.' },
183 |         ],
184 |         isError: false,
185 |       });
186 |     });
187 | 
188 |     it('should return error response for test failure', async () => {
189 |       const mockExecutor = createMockExecutor({
190 |         success: false,
191 |         error: '2 tests failed',
192 |       });
193 | 
194 |       const result = await swift_package_testLogic(
195 |         {
196 |           packagePath: '/test/package',
197 |         },
198 |         mockExecutor,
199 |       );
200 | 
201 |       expect(result).toEqual({
202 |         content: [
203 |           {
204 |             type: 'text',
205 |             text: 'Error: Swift package tests failed\nDetails: 2 tests failed',
206 |           },
207 |         ],
208 |         isError: true,
209 |       });
210 |     });
211 | 
212 |     it('should handle spawn error', async () => {
213 |       const mockExecutor = async () => {
214 |         throw new Error('spawn ENOENT');
215 |       };
216 | 
217 |       const result = await swift_package_testLogic(
218 |         {
219 |           packagePath: '/test/package',
220 |         },
221 |         mockExecutor,
222 |       );
223 | 
224 |       expect(result).toEqual({
225 |         content: [
226 |           {
227 |             type: 'text',
228 |             text: 'Error: Failed to execute swift test\nDetails: spawn ENOENT',
229 |           },
230 |         ],
231 |         isError: true,
232 |       });
233 |     });
234 | 
235 |     it('should handle successful test with parameters', async () => {
236 |       const mockExecutor = createMockExecutor({
237 |         success: true,
238 |         output: 'Tests completed.',
239 |       });
240 | 
241 |       const result = await swift_package_testLogic(
242 |         {
243 |           packagePath: '/test/package',
244 |           testProduct: 'MyTests',
245 |           filter: 'Test.*',
246 |           configuration: 'release',
247 |           parallel: false,
248 |           showCodecov: true,
249 |           parseAsLibrary: true,
250 |         },
251 |         mockExecutor,
252 |       );
253 | 
254 |       expect(result).toEqual({
255 |         content: [
256 |           { type: 'text', text: '✅ Swift package tests completed.' },
257 |           {
258 |             type: 'text',
259 |             text: '💡 Next: Execute your app with swift_package_run if tests passed',
260 |           },
261 |           { type: 'text', text: 'Tests completed.' },
262 |         ],
263 |         isError: false,
264 |       });
265 |     });
266 |   });
267 | });
268 | 
```

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

```typescript
  1 | import { describe, it, expect, beforeEach } from 'vitest';
  2 | import { z } from 'zod';
  3 | import {
  4 |   createMockExecutor,
  5 |   createMockFileSystemExecutor,
  6 |   createNoopExecutor,
  7 | } from '../../../../test-utils/mock-executors.ts';
  8 | import { sessionStore } from '../../../../utils/session-store.ts';
  9 | import installAppSim, { install_app_simLogic } from '../install_app_sim.ts';
 10 | 
 11 | describe('install_app_sim tool', () => {
 12 |   beforeEach(() => {
 13 |     sessionStore.clear();
 14 |   });
 15 | 
 16 |   describe('Export Field Validation (Literal)', () => {
 17 |     it('should have correct name', () => {
 18 |       expect(installAppSim.name).toBe('install_app_sim');
 19 |     });
 20 | 
 21 |     it('should have concise description', () => {
 22 |       expect(installAppSim.description).toBe('Installs an app in an iOS simulator.');
 23 |     });
 24 | 
 25 |     it('should expose public schema with only appPath', () => {
 26 |       const schema = z.object(installAppSim.schema);
 27 | 
 28 |       expect(schema.safeParse({ appPath: '/path/to/app.app' }).success).toBe(true);
 29 |       expect(schema.safeParse({ appPath: 42 }).success).toBe(false);
 30 |       expect(schema.safeParse({}).success).toBe(false);
 31 | 
 32 |       expect(Object.keys(installAppSim.schema)).toEqual(['appPath']);
 33 |     });
 34 |   });
 35 | 
 36 |   describe('Handler Requirements', () => {
 37 |     it('should require simulatorId when not provided', async () => {
 38 |       const result = await installAppSim.handler({ appPath: '/path/to/app.app' });
 39 | 
 40 |       expect(result.isError).toBe(true);
 41 |       expect(result.content[0].text).toContain('Missing required session defaults');
 42 |       expect(result.content[0].text).toContain('simulatorId is required');
 43 |       expect(result.content[0].text).toContain('session-set-defaults');
 44 |     });
 45 | 
 46 |     it('should validate appPath when simulatorId default exists', async () => {
 47 |       sessionStore.setDefaults({ simulatorId: 'SIM-UUID' });
 48 | 
 49 |       const result = await installAppSim.handler({});
 50 | 
 51 |       expect(result.isError).toBe(true);
 52 |       expect(result.content[0].text).toContain('Parameter validation failed');
 53 |       expect(result.content[0].text).toContain('appPath: Required');
 54 |       expect(result.content[0].text).toContain(
 55 |         'Tip: set session defaults via session-set-defaults',
 56 |       );
 57 |     });
 58 |   });
 59 | 
 60 |   describe('Command Generation', () => {
 61 |     it('should generate correct simctl install command', async () => {
 62 |       const executorCalls: unknown[] = [];
 63 |       const mockExecutor = (...args: unknown[]) => {
 64 |         executorCalls.push(args);
 65 |         return Promise.resolve({
 66 |           success: true,
 67 |           output: 'App installed',
 68 |           error: undefined,
 69 |           process: { pid: 12345 },
 70 |         });
 71 |       };
 72 | 
 73 |       const mockFileSystem = createMockFileSystemExecutor({
 74 |         existsSync: () => true,
 75 |       });
 76 | 
 77 |       await install_app_simLogic(
 78 |         {
 79 |           simulatorId: 'test-uuid-123',
 80 |           appPath: '/path/to/app.app',
 81 |         },
 82 |         mockExecutor,
 83 |         mockFileSystem,
 84 |       );
 85 | 
 86 |       expect(executorCalls).toEqual([
 87 |         [
 88 |           ['xcrun', 'simctl', 'install', 'test-uuid-123', '/path/to/app.app'],
 89 |           'Install App in Simulator',
 90 |           true,
 91 |           undefined,
 92 |         ],
 93 |         [
 94 |           ['defaults', 'read', '/path/to/app.app/Info', 'CFBundleIdentifier'],
 95 |           'Extract Bundle ID',
 96 |           false,
 97 |           undefined,
 98 |         ],
 99 |       ]);
100 |     });
101 | 
102 |     it('should generate command with different simulator identifier', async () => {
103 |       const executorCalls: unknown[] = [];
104 |       const mockExecutor = (...args: unknown[]) => {
105 |         executorCalls.push(args);
106 |         return Promise.resolve({
107 |           success: true,
108 |           output: 'App installed',
109 |           error: undefined,
110 |           process: { pid: 12345 },
111 |         });
112 |       };
113 | 
114 |       const mockFileSystem = createMockFileSystemExecutor({
115 |         existsSync: () => true,
116 |       });
117 | 
118 |       await install_app_simLogic(
119 |         {
120 |           simulatorId: 'different-uuid-456',
121 |           appPath: '/different/path/MyApp.app',
122 |         },
123 |         mockExecutor,
124 |         mockFileSystem,
125 |       );
126 | 
127 |       expect(executorCalls).toEqual([
128 |         [
129 |           ['xcrun', 'simctl', 'install', 'different-uuid-456', '/different/path/MyApp.app'],
130 |           'Install App in Simulator',
131 |           true,
132 |           undefined,
133 |         ],
134 |         [
135 |           ['defaults', 'read', '/different/path/MyApp.app/Info', 'CFBundleIdentifier'],
136 |           'Extract Bundle ID',
137 |           false,
138 |           undefined,
139 |         ],
140 |       ]);
141 |     });
142 |   });
143 | 
144 |   describe('Logic Behavior (Literal Returns)', () => {
145 |     it('should handle file does not exist', async () => {
146 |       const mockFileSystem = createMockFileSystemExecutor({
147 |         existsSync: () => false,
148 |       });
149 | 
150 |       const result = await install_app_simLogic(
151 |         {
152 |           simulatorId: 'test-uuid-123',
153 |           appPath: '/path/to/app.app',
154 |         },
155 |         createNoopExecutor(),
156 |         mockFileSystem,
157 |       );
158 | 
159 |       expect(result).toEqual({
160 |         content: [
161 |           {
162 |             type: 'text',
163 |             text: "File not found: '/path/to/app.app'. Please check the path and try again.",
164 |           },
165 |         ],
166 |         isError: true,
167 |       });
168 |     });
169 | 
170 |     it('should handle successful install', async () => {
171 |       let callCount = 0;
172 |       const mockExecutor = () => {
173 |         callCount++;
174 |         if (callCount === 1) {
175 |           return Promise.resolve({
176 |             success: true,
177 |             output: 'App installed',
178 |             error: undefined,
179 |             process: { pid: 12345 },
180 |           });
181 |         }
182 |         return Promise.resolve({
183 |           success: true,
184 |           output: 'com.example.myapp',
185 |           error: undefined,
186 |           process: { pid: 12345 },
187 |         });
188 |       };
189 | 
190 |       const mockFileSystem = createMockFileSystemExecutor({
191 |         existsSync: () => true,
192 |       });
193 | 
194 |       const result = await install_app_simLogic(
195 |         {
196 |           simulatorId: 'test-uuid-123',
197 |           appPath: '/path/to/app.app',
198 |         },
199 |         mockExecutor,
200 |         mockFileSystem,
201 |       );
202 | 
203 |       expect(result).toEqual({
204 |         content: [
205 |           {
206 |             type: 'text',
207 |             text: 'App installed successfully in simulator test-uuid-123',
208 |           },
209 |           {
210 |             type: 'text',
211 |             text: `Next Steps:
212 | 1. Open the Simulator app: open_sim({})
213 | 2. Launch the app: launch_app_sim({ simulatorId: "test-uuid-123", bundleId: "com.example.myapp" })`,
214 |           },
215 |         ],
216 |       });
217 |     });
218 | 
219 |     it('should handle command failure', async () => {
220 |       const mockExecutor = () =>
221 |         Promise.resolve({
222 |           success: false,
223 |           output: '',
224 |           error: 'Install failed',
225 |           process: { pid: 12345 },
226 |         });
227 | 
228 |       const mockFileSystem = createMockFileSystemExecutor({
229 |         existsSync: () => true,
230 |       });
231 | 
232 |       const result = await install_app_simLogic(
233 |         {
234 |           simulatorId: 'test-uuid-123',
235 |           appPath: '/path/to/app.app',
236 |         },
237 |         mockExecutor,
238 |         mockFileSystem,
239 |       );
240 | 
241 |       expect(result).toEqual({
242 |         content: [
243 |           {
244 |             type: 'text',
245 |             text: 'Install app in simulator operation failed: Install failed',
246 |           },
247 |         ],
248 |       });
249 |     });
250 | 
251 |     it('should handle exception with Error object', async () => {
252 |       const mockExecutor = () => Promise.reject(new Error('Command execution failed'));
253 | 
254 |       const mockFileSystem = createMockFileSystemExecutor({
255 |         existsSync: () => true,
256 |       });
257 | 
258 |       const result = await install_app_simLogic(
259 |         {
260 |           simulatorId: 'test-uuid-123',
261 |           appPath: '/path/to/app.app',
262 |         },
263 |         mockExecutor,
264 |         mockFileSystem,
265 |       );
266 | 
267 |       expect(result).toEqual({
268 |         content: [
269 |           {
270 |             type: 'text',
271 |             text: 'Install app in simulator operation failed: Command execution failed',
272 |           },
273 |         ],
274 |       });
275 |     });
276 | 
277 |     it('should handle exception with string error', async () => {
278 |       const mockExecutor = () => Promise.reject('String error');
279 | 
280 |       const mockFileSystem = createMockFileSystemExecutor({
281 |         existsSync: () => true,
282 |       });
283 | 
284 |       const result = await install_app_simLogic(
285 |         {
286 |           simulatorId: 'test-uuid-123',
287 |           appPath: '/path/to/app.app',
288 |         },
289 |         mockExecutor,
290 |         mockFileSystem,
291 |       );
292 | 
293 |       expect(result).toEqual({
294 |         content: [
295 |           {
296 |             type: 'text',
297 |             text: 'Install app in simulator operation failed: String error',
298 |           },
299 |         ],
300 |       });
301 |     });
302 |   });
303 | });
304 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/swift-package/swift_package_run.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from 'zod';
  2 | import path from 'node:path';
  3 | import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.ts';
  4 | import { log } from '../../../utils/logging/index.ts';
  5 | import type { CommandExecutor } from '../../../utils/execution/index.ts';
  6 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
  7 | import { ToolResponse, createTextContent } from '../../../types/common.ts';
  8 | import { addProcess } from './active-processes.ts';
  9 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
 10 | 
 11 | // Define schema as ZodObject
 12 | const swiftPackageRunSchema = z.object({
 13 |   packagePath: z.string().describe('Path to the Swift package root (Required)'),
 14 |   executableName: z
 15 |     .string()
 16 |     .optional()
 17 |     .describe('Name of executable to run (defaults to package name)'),
 18 |   arguments: z.array(z.string()).optional().describe('Arguments to pass to the executable'),
 19 |   configuration: z
 20 |     .enum(['debug', 'release'])
 21 |     .optional()
 22 |     .describe("Build configuration: 'debug' (default) or 'release'"),
 23 |   timeout: z.number().optional().describe('Timeout in seconds (default: 30, max: 300)'),
 24 |   background: z
 25 |     .boolean()
 26 |     .optional()
 27 |     .describe('Run in background and return immediately (default: false)'),
 28 |   parseAsLibrary: z
 29 |     .boolean()
 30 |     .optional()
 31 |     .describe('Add -parse-as-library flag for @main support (default: false)'),
 32 | });
 33 | 
 34 | // Use z.infer for type safety
 35 | type SwiftPackageRunParams = z.infer<typeof swiftPackageRunSchema>;
 36 | 
 37 | export async function swift_package_runLogic(
 38 |   params: SwiftPackageRunParams,
 39 |   executor: CommandExecutor,
 40 | ): Promise<ToolResponse> {
 41 |   const resolvedPath = path.resolve(params.packagePath);
 42 |   const timeout = Math.min(params.timeout ?? 30, 300) * 1000; // Convert to ms, max 5 minutes
 43 | 
 44 |   // Detect test environment to prevent real spawn calls during testing
 45 |   const isTestEnvironment = process.env.VITEST === 'true' || process.env.NODE_ENV === 'test';
 46 | 
 47 |   const swiftArgs = ['run', '--package-path', resolvedPath];
 48 | 
 49 |   if (params.configuration && params.configuration.toLowerCase() === 'release') {
 50 |     swiftArgs.push('-c', 'release');
 51 |   } else if (params.configuration && params.configuration.toLowerCase() !== 'debug') {
 52 |     return createTextResponse("Invalid configuration. Use 'debug' or 'release'.", true);
 53 |   }
 54 | 
 55 |   if (params.parseAsLibrary) {
 56 |     swiftArgs.push('-Xswiftc', '-parse-as-library');
 57 |   }
 58 | 
 59 |   if (params.executableName) {
 60 |     swiftArgs.push(params.executableName);
 61 |   }
 62 | 
 63 |   // Add double dash before executable arguments
 64 |   if (params.arguments && params.arguments.length > 0) {
 65 |     swiftArgs.push('--');
 66 |     swiftArgs.push(...params.arguments);
 67 |   }
 68 | 
 69 |   log('info', `Running swift ${swiftArgs.join(' ')}`);
 70 | 
 71 |   try {
 72 |     if (params.background) {
 73 |       // Background mode: Use CommandExecutor but don't wait for completion
 74 |       if (isTestEnvironment) {
 75 |         // In test environment, return mock response without real process
 76 |         const mockPid = 12345;
 77 |         return {
 78 |           content: [
 79 |             createTextContent(
 80 |               `🚀 Started executable in background (PID: ${mockPid})\n` +
 81 |                 `💡 Process is running independently. Use swift_package_stop with PID ${mockPid} to terminate when needed.`,
 82 |             ),
 83 |           ],
 84 |         };
 85 |       } else {
 86 |         // Production: use CommandExecutor to start the process
 87 |         const command = ['swift', ...swiftArgs];
 88 |         // Filter out undefined values from process.env
 89 |         const cleanEnv = Object.fromEntries(
 90 |           Object.entries(process.env).filter(([, value]) => value !== undefined),
 91 |         ) as Record<string, string>;
 92 |         const result = await executor(
 93 |           command,
 94 |           'Swift Package Run (Background)',
 95 |           true,
 96 |           cleanEnv,
 97 |           true,
 98 |         );
 99 | 
100 |         // Store the process in active processes system if available
101 |         if (result.process?.pid) {
102 |           addProcess(result.process.pid, {
103 |             process: {
104 |               kill: (signal?: string) => {
105 |                 // Adapt string signal to NodeJS.Signals
106 |                 if (result.process) {
107 |                   result.process.kill(signal as NodeJS.Signals);
108 |                 }
109 |               },
110 |               on: (event: string, callback: () => void) => {
111 |                 if (result.process) {
112 |                   result.process.on(event, callback);
113 |                 }
114 |               },
115 |               pid: result.process.pid,
116 |             },
117 |             startedAt: new Date(),
118 |           });
119 | 
120 |           return {
121 |             content: [
122 |               createTextContent(
123 |                 `🚀 Started executable in background (PID: ${result.process.pid})\n` +
124 |                   `💡 Process is running independently. Use swift_package_stop with PID ${result.process.pid} to terminate when needed.`,
125 |               ),
126 |             ],
127 |           };
128 |         } else {
129 |           return {
130 |             content: [
131 |               createTextContent(
132 |                 `🚀 Started executable in background\n` +
133 |                   `💡 Process is running independently. PID not available for this execution.`,
134 |               ),
135 |             ],
136 |           };
137 |         }
138 |       }
139 |     } else {
140 |       // Foreground mode: use CommandExecutor but handle long-running processes
141 |       const command = ['swift', ...swiftArgs];
142 | 
143 |       // Create a promise that will either complete with the command result or timeout
144 |       const commandPromise = executor(command, 'Swift Package Run', true, undefined);
145 | 
146 |       const timeoutPromise = new Promise<{
147 |         success: boolean;
148 |         output: string;
149 |         error: string;
150 |         timedOut: boolean;
151 |       }>((resolve) => {
152 |         setTimeout(() => {
153 |           resolve({
154 |             success: false,
155 |             output: '',
156 |             error: `Process timed out after ${timeout / 1000} seconds`,
157 |             timedOut: true,
158 |           });
159 |         }, timeout);
160 |       });
161 | 
162 |       // Race between command completion and timeout
163 |       const result = await Promise.race([commandPromise, timeoutPromise]);
164 | 
165 |       if ('timedOut' in result && result.timedOut) {
166 |         // For timeout case, the process may still be running - provide timeout response
167 |         if (isTestEnvironment) {
168 |           // In test environment, return mock response
169 |           const mockPid = 12345;
170 |           return {
171 |             content: [
172 |               createTextContent(
173 |                 `⏱️ Process timed out after ${timeout / 1000} seconds but may continue running.`,
174 |               ),
175 |               createTextContent(`PID: ${mockPid} (mock)`),
176 |               createTextContent(
177 |                 `💡 Process may still be running. Use swift_package_stop with PID ${mockPid} to terminate when needed.`,
178 |               ),
179 |               createTextContent(result.output || '(no output so far)'),
180 |             ],
181 |           };
182 |         } else {
183 |           // Production: timeout occurred, but we don't start a new process
184 |           return {
185 |             content: [
186 |               createTextContent(`⏱️ Process timed out after ${timeout / 1000} seconds.`),
187 |               createTextContent(
188 |                 `💡 Process execution exceeded the timeout limit. Consider using background mode for long-running executables.`,
189 |               ),
190 |               createTextContent(result.output || '(no output so far)'),
191 |             ],
192 |           };
193 |         }
194 |       }
195 | 
196 |       if (result.success) {
197 |         return {
198 |           content: [
199 |             createTextContent('✅ Swift executable completed successfully.'),
200 |             createTextContent('💡 Process finished cleanly. Check output for results.'),
201 |             createTextContent(result.output || '(no output)'),
202 |           ],
203 |         };
204 |       } else {
205 |         const content = [
206 |           createTextContent('❌ Swift executable failed.'),
207 |           createTextContent(result.output || '(no output)'),
208 |         ];
209 |         if (result.error) {
210 |           content.push(createTextContent(`Errors:\n${result.error}`));
211 |         }
212 |         return { content };
213 |       }
214 |     }
215 |   } catch (error) {
216 |     const message = error instanceof Error ? error.message : String(error);
217 |     log('error', `Swift run failed: ${message}`);
218 |     return createErrorResponse('Failed to execute swift run', message);
219 |   }
220 | }
221 | 
222 | export default {
223 |   name: 'swift_package_run',
224 |   description: 'Runs an executable target from a Swift Package with swift run',
225 |   schema: swiftPackageRunSchema.shape, // MCP SDK compatibility
226 |   handler: createTypedTool(
227 |     swiftPackageRunSchema,
228 |     swift_package_runLogic,
229 |     getDefaultCommandExecutor,
230 |   ),
231 | };
232 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for install_app_device plugin (device-shared)
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using dependency injection for deterministic testing
  5 |  */
  6 | 
  7 | import { describe, it, expect, beforeEach } from 'vitest';
  8 | import { z } from 'zod';
  9 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
 10 | import installAppDevice, { install_app_deviceLogic } from '../install_app_device.ts';
 11 | import { sessionStore } from '../../../../utils/session-store.ts';
 12 | 
 13 | describe('install_app_device plugin', () => {
 14 |   beforeEach(() => {
 15 |     sessionStore.clear();
 16 |   });
 17 | 
 18 |   describe('Handler Requirements', () => {
 19 |     it('should require deviceId when session defaults are missing', async () => {
 20 |       const result = await installAppDevice.handler({
 21 |         appPath: '/path/to/test.app',
 22 |       });
 23 | 
 24 |       expect(result.isError).toBe(true);
 25 |       expect(result.content[0].text).toContain('deviceId is required');
 26 |     });
 27 |   });
 28 | 
 29 |   describe('Export Field Validation (Literal)', () => {
 30 |     it('should have correct name', () => {
 31 |       expect(installAppDevice.name).toBe('install_app_device');
 32 |     });
 33 | 
 34 |     it('should have correct description', () => {
 35 |       expect(installAppDevice.description).toBe('Installs an app on a connected device.');
 36 |     });
 37 | 
 38 |     it('should have handler function', () => {
 39 |       expect(typeof installAppDevice.handler).toBe('function');
 40 |     });
 41 | 
 42 |     it('should require appPath in public schema', () => {
 43 |       const schema = z.object(installAppDevice.schema).strict();
 44 |       expect(schema.safeParse({ appPath: '/path/to/test.app' }).success).toBe(true);
 45 |       expect(schema.safeParse({}).success).toBe(false);
 46 |       expect(schema.safeParse({ deviceId: 'test-device-123' }).success).toBe(false);
 47 | 
 48 |       expect(Object.keys(installAppDevice.schema)).toEqual(['appPath']);
 49 |     });
 50 |   });
 51 | 
 52 |   describe('Command Generation', () => {
 53 |     it('should generate correct devicectl command with basic parameters', async () => {
 54 |       let capturedCommand: unknown[] = [];
 55 |       let capturedDescription: string = '';
 56 |       let capturedUseShell: boolean = false;
 57 |       let capturedEnv: unknown = undefined;
 58 | 
 59 |       const mockExecutor = createMockExecutor({
 60 |         success: true,
 61 |         output: 'App installation successful',
 62 |         process: { pid: 12345 },
 63 |       });
 64 | 
 65 |       const trackingExecutor = async (
 66 |         command: unknown[],
 67 |         description: string,
 68 |         useShell: boolean,
 69 |         env: unknown,
 70 |       ) => {
 71 |         capturedCommand = command;
 72 |         capturedDescription = description;
 73 |         capturedUseShell = useShell;
 74 |         capturedEnv = env;
 75 |         return mockExecutor(command, description, useShell, env);
 76 |       };
 77 | 
 78 |       await install_app_deviceLogic(
 79 |         {
 80 |           deviceId: 'test-device-123',
 81 |           appPath: '/path/to/test.app',
 82 |         },
 83 |         trackingExecutor,
 84 |       );
 85 | 
 86 |       expect(capturedCommand).toEqual([
 87 |         'xcrun',
 88 |         'devicectl',
 89 |         'device',
 90 |         'install',
 91 |         'app',
 92 |         '--device',
 93 |         'test-device-123',
 94 |         '/path/to/test.app',
 95 |       ]);
 96 |       expect(capturedDescription).toBe('Install app on device');
 97 |       expect(capturedUseShell).toBe(true);
 98 |       expect(capturedEnv).toBe(undefined);
 99 |     });
100 | 
101 |     it('should generate correct command with different device ID', async () => {
102 |       let capturedCommand: unknown[] = [];
103 | 
104 |       const mockExecutor = createMockExecutor({
105 |         success: true,
106 |         output: 'App installation successful',
107 |         process: { pid: 12345 },
108 |       });
109 | 
110 |       const trackingExecutor = async (command: unknown[]) => {
111 |         capturedCommand = command;
112 |         return mockExecutor(command);
113 |       };
114 | 
115 |       await install_app_deviceLogic(
116 |         {
117 |           deviceId: 'different-device-uuid',
118 |           appPath: '/apps/MyApp.app',
119 |         },
120 |         trackingExecutor,
121 |       );
122 | 
123 |       expect(capturedCommand).toEqual([
124 |         'xcrun',
125 |         'devicectl',
126 |         'device',
127 |         'install',
128 |         'app',
129 |         '--device',
130 |         'different-device-uuid',
131 |         '/apps/MyApp.app',
132 |       ]);
133 |     });
134 | 
135 |     it('should generate correct command with paths containing spaces', async () => {
136 |       let capturedCommand: unknown[] = [];
137 | 
138 |       const mockExecutor = createMockExecutor({
139 |         success: true,
140 |         output: 'App installation successful',
141 |         process: { pid: 12345 },
142 |       });
143 | 
144 |       const trackingExecutor = async (command: unknown[]) => {
145 |         capturedCommand = command;
146 |         return mockExecutor(command);
147 |       };
148 | 
149 |       await install_app_deviceLogic(
150 |         {
151 |           deviceId: 'test-device-123',
152 |           appPath: '/path/to/My App.app',
153 |         },
154 |         trackingExecutor,
155 |       );
156 | 
157 |       expect(capturedCommand).toEqual([
158 |         'xcrun',
159 |         'devicectl',
160 |         'device',
161 |         'install',
162 |         'app',
163 |         '--device',
164 |         'test-device-123',
165 |         '/path/to/My App.app',
166 |       ]);
167 |     });
168 |   });
169 | 
170 |   describe('Success Path Tests', () => {
171 |     it('should return successful installation response', async () => {
172 |       const mockExecutor = createMockExecutor({
173 |         success: true,
174 |         output: 'App installation successful',
175 |       });
176 | 
177 |       const result = await install_app_deviceLogic(
178 |         {
179 |           deviceId: 'test-device-123',
180 |           appPath: '/path/to/test.app',
181 |         },
182 |         mockExecutor,
183 |       );
184 | 
185 |       expect(result).toEqual({
186 |         content: [
187 |           {
188 |             type: 'text',
189 |             text: '✅ App installed successfully on device test-device-123\n\nApp installation successful',
190 |           },
191 |         ],
192 |       });
193 |     });
194 | 
195 |     it('should return successful installation with detailed output', async () => {
196 |       const mockExecutor = createMockExecutor({
197 |         success: true,
198 |         output:
199 |           'Installing app...\nApp bundle: /path/to/test.app\nInstallation completed successfully',
200 |       });
201 | 
202 |       const result = await install_app_deviceLogic(
203 |         {
204 |           deviceId: 'device-456',
205 |           appPath: '/apps/TestApp.app',
206 |         },
207 |         mockExecutor,
208 |       );
209 | 
210 |       expect(result).toEqual({
211 |         content: [
212 |           {
213 |             type: 'text',
214 |             text: '✅ App installed successfully on device device-456\n\nInstalling app...\nApp bundle: /path/to/test.app\nInstallation completed successfully',
215 |           },
216 |         ],
217 |       });
218 |     });
219 | 
220 |     it('should return successful installation with empty output', async () => {
221 |       const mockExecutor = createMockExecutor({
222 |         success: true,
223 |         output: '',
224 |       });
225 | 
226 |       const result = await install_app_deviceLogic(
227 |         {
228 |           deviceId: 'empty-output-device',
229 |           appPath: '/path/to/app.app',
230 |         },
231 |         mockExecutor,
232 |       );
233 | 
234 |       expect(result).toEqual({
235 |         content: [
236 |           {
237 |             type: 'text',
238 |             text: '✅ App installed successfully on device empty-output-device\n\n',
239 |           },
240 |         ],
241 |       });
242 |     });
243 |   });
244 | 
245 |   describe('Error Handling', () => {
246 |     it('should return installation failure response', async () => {
247 |       const mockExecutor = createMockExecutor({
248 |         success: false,
249 |         error: 'Installation failed: App not found',
250 |       });
251 | 
252 |       const result = await install_app_deviceLogic(
253 |         {
254 |           deviceId: 'test-device-123',
255 |           appPath: '/path/to/nonexistent.app',
256 |         },
257 |         mockExecutor,
258 |       );
259 | 
260 |       expect(result).toEqual({
261 |         content: [
262 |           {
263 |             type: 'text',
264 |             text: 'Failed to install app: Installation failed: App not found',
265 |           },
266 |         ],
267 |         isError: true,
268 |       });
269 |     });
270 | 
271 |     it('should return exception handling response', async () => {
272 |       const mockExecutor = createMockExecutor(new Error('Network error'));
273 | 
274 |       const result = await install_app_deviceLogic(
275 |         {
276 |           deviceId: 'test-device-123',
277 |           appPath: '/path/to/test.app',
278 |         },
279 |         mockExecutor,
280 |       );
281 | 
282 |       expect(result).toEqual({
283 |         content: [
284 |           {
285 |             type: 'text',
286 |             text: 'Failed to install app on device: Network error',
287 |           },
288 |         ],
289 |         isError: true,
290 |       });
291 |     });
292 | 
293 |     it('should return string error handling response', async () => {
294 |       const mockExecutor = createMockExecutor('String error');
295 | 
296 |       const result = await install_app_deviceLogic(
297 |         {
298 |           deviceId: 'test-device-123',
299 |           appPath: '/path/to/test.app',
300 |         },
301 |         mockExecutor,
302 |       );
303 | 
304 |       expect(result).toEqual({
305 |         content: [
306 |           {
307 |             type: 'text',
308 |             text: 'Failed to install app on device: String error',
309 |           },
310 |         ],
311 |         isError: true,
312 |       });
313 |     });
314 |   });
315 | });
316 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for describe_ui tool plugin
  3 |  */
  4 | 
  5 | import { describe, it, expect, beforeEach } from 'vitest';
  6 | import { z } from 'zod';
  7 | import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts';
  8 | import describeUIPlugin, { describe_uiLogic } from '../describe_ui.ts';
  9 | 
 10 | describe('Describe UI Plugin', () => {
 11 |   let mockCalls: any[] = [];
 12 | 
 13 |   mockCalls = [];
 14 | 
 15 |   describe('Export Field Validation (Literal)', () => {
 16 |     it('should have correct name', () => {
 17 |       expect(describeUIPlugin.name).toBe('describe_ui');
 18 |     });
 19 | 
 20 |     it('should have correct description', () => {
 21 |       expect(describeUIPlugin.description).toBe(
 22 |         'Gets entire view hierarchy with precise frame coordinates (x, y, width, height) for all visible elements. Use this before UI interactions or after layout changes - do NOT guess coordinates from screenshots. Returns JSON tree with frame data for accurate automation.',
 23 |       );
 24 |     });
 25 | 
 26 |     it('should have handler function', () => {
 27 |       expect(typeof describeUIPlugin.handler).toBe('function');
 28 |     });
 29 | 
 30 |     it('should validate schema fields with safeParse', () => {
 31 |       const schema = z.object(describeUIPlugin.schema);
 32 | 
 33 |       // Valid case
 34 |       expect(
 35 |         schema.safeParse({
 36 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 37 |         }).success,
 38 |       ).toBe(true);
 39 | 
 40 |       // Invalid simulatorUuid
 41 |       expect(
 42 |         schema.safeParse({
 43 |           simulatorUuid: 'invalid-uuid',
 44 |         }).success,
 45 |       ).toBe(false);
 46 | 
 47 |       // Missing simulatorUuid
 48 |       expect(schema.safeParse({}).success).toBe(false);
 49 |     });
 50 |   });
 51 | 
 52 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 53 |     it('should handle missing simulatorUuid via schema validation', async () => {
 54 |       // Test the actual handler (not just the logic function)
 55 |       // This demonstrates that Zod validation catches missing parameters
 56 |       const result = await describeUIPlugin.handler({});
 57 | 
 58 |       expect(result.isError).toBe(true);
 59 |       expect(result.content[0].text).toContain('Parameter validation failed');
 60 |       expect(result.content[0].text).toContain('simulatorUuid: Required');
 61 |     });
 62 | 
 63 |     it('should handle invalid simulatorUuid format via schema validation', async () => {
 64 |       // Test the actual handler with invalid UUID format
 65 |       const result = await describeUIPlugin.handler({
 66 |         simulatorUuid: 'invalid-uuid-format',
 67 |       });
 68 | 
 69 |       expect(result.isError).toBe(true);
 70 |       expect(result.content[0].text).toContain('Parameter validation failed');
 71 |       expect(result.content[0].text).toContain('Invalid Simulator UUID format');
 72 |     });
 73 | 
 74 |     it('should return success for valid describe_ui execution', async () => {
 75 |       const uiHierarchy =
 76 |         '{"elements": [{"type": "Button", "frame": {"x": 100, "y": 200, "width": 50, "height": 30}}]}';
 77 | 
 78 |       const mockExecutor = createMockExecutor({
 79 |         success: true,
 80 |         output: uiHierarchy,
 81 |         error: undefined,
 82 |         process: { pid: 12345 },
 83 |       });
 84 | 
 85 |       // Create mock axe helpers
 86 |       const mockAxeHelpers = {
 87 |         getAxePath: () => '/usr/local/bin/axe',
 88 |         getBundledAxeEnvironment: () => ({}),
 89 |       };
 90 | 
 91 |       // Wrap executor to track calls
 92 |       const executorCalls: any[] = [];
 93 |       const trackingExecutor = async (...args: any[]) => {
 94 |         executorCalls.push(args);
 95 |         return mockExecutor(...args);
 96 |       };
 97 | 
 98 |       const result = await describe_uiLogic(
 99 |         {
100 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
101 |         },
102 |         trackingExecutor,
103 |         mockAxeHelpers,
104 |       );
105 | 
106 |       expect(executorCalls[0]).toEqual([
107 |         ['/usr/local/bin/axe', 'describe-ui', '--udid', '12345678-1234-1234-1234-123456789012'],
108 |         '[AXe]: describe-ui',
109 |         false,
110 |         {},
111 |       ]);
112 | 
113 |       expect(result).toEqual({
114 |         content: [
115 |           {
116 |             type: 'text',
117 |             text: 'Accessibility hierarchy retrieved successfully:\n```json\n{"elements": [{"type": "Button", "frame": {"x": 100, "y": 200, "width": 50, "height": 30}}]}\n```',
118 |           },
119 |           {
120 |             type: 'text',
121 |             text: `Next Steps:
122 | - Use frame coordinates for tap/swipe (center: x+width/2, y+height/2)
123 | - Re-run describe_ui after layout changes
124 | - Screenshots are for visual verification only`,
125 |           },
126 |         ],
127 |       });
128 |     });
129 | 
130 |     it('should handle DependencyError when axe is not available', async () => {
131 |       // Create mock axe helpers that return null for axe path
132 |       const mockAxeHelpers = {
133 |         getAxePath: () => null,
134 |         getBundledAxeEnvironment: () => ({}),
135 |         createAxeNotAvailableResponse: () => ({
136 |           content: [
137 |             {
138 |               type: 'text',
139 |               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.',
140 |             },
141 |           ],
142 |           isError: true,
143 |         }),
144 |       };
145 | 
146 |       const result = await describe_uiLogic(
147 |         {
148 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
149 |         },
150 |         createNoopExecutor(),
151 |         mockAxeHelpers,
152 |       );
153 | 
154 |       expect(result).toEqual({
155 |         content: [
156 |           {
157 |             type: 'text',
158 |             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.',
159 |           },
160 |         ],
161 |         isError: true,
162 |       });
163 |     });
164 | 
165 |     it('should handle AxeError from failed command execution', async () => {
166 |       const mockExecutor = createMockExecutor({
167 |         success: false,
168 |         output: '',
169 |         error: 'axe command failed',
170 |         process: { pid: 12345 },
171 |       });
172 | 
173 |       // Create mock axe helpers
174 |       const mockAxeHelpers = {
175 |         getAxePath: () => '/usr/local/bin/axe',
176 |         getBundledAxeEnvironment: () => ({}),
177 |       };
178 | 
179 |       const result = await describe_uiLogic(
180 |         {
181 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
182 |         },
183 |         mockExecutor,
184 |         mockAxeHelpers,
185 |       );
186 | 
187 |       expect(result).toEqual({
188 |         content: [
189 |           {
190 |             type: 'text',
191 |             text: "Error: Failed to get accessibility hierarchy: axe command 'describe-ui' failed.\nDetails: axe command failed",
192 |           },
193 |         ],
194 |         isError: true,
195 |       });
196 |     });
197 | 
198 |     it('should handle SystemError from command execution', async () => {
199 |       const mockExecutor = createMockExecutor(new Error('ENOENT: no such file or directory'));
200 | 
201 |       // Create mock axe helpers
202 |       const mockAxeHelpers = {
203 |         getAxePath: () => '/usr/local/bin/axe',
204 |         getBundledAxeEnvironment: () => ({}),
205 |       };
206 | 
207 |       const result = await describe_uiLogic(
208 |         {
209 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
210 |         },
211 |         mockExecutor,
212 |         mockAxeHelpers,
213 |       );
214 | 
215 |       expect(result).toEqual({
216 |         content: [
217 |           {
218 |             type: 'text',
219 |             text: expect.stringContaining(
220 |               'Error: System error executing axe: Failed to execute axe command: ENOENT: no such file or directory',
221 |             ),
222 |           },
223 |         ],
224 |         isError: true,
225 |       });
226 |     });
227 | 
228 |     it('should handle unexpected Error objects', async () => {
229 |       const mockExecutor = createMockExecutor(new Error('Unexpected error'));
230 | 
231 |       // Create mock axe helpers
232 |       const mockAxeHelpers = {
233 |         getAxePath: () => '/usr/local/bin/axe',
234 |         getBundledAxeEnvironment: () => ({}),
235 |       };
236 | 
237 |       const result = await describe_uiLogic(
238 |         {
239 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
240 |         },
241 |         mockExecutor,
242 |         mockAxeHelpers,
243 |       );
244 | 
245 |       expect(result).toEqual({
246 |         content: [
247 |           {
248 |             type: 'text',
249 |             text: expect.stringContaining(
250 |               'Error: System error executing axe: Failed to execute axe command: Unexpected error',
251 |             ),
252 |           },
253 |         ],
254 |         isError: true,
255 |       });
256 |     });
257 | 
258 |     it('should handle unexpected string errors', async () => {
259 |       const mockExecutor = createMockExecutor('String error');
260 | 
261 |       // Create mock axe helpers
262 |       const mockAxeHelpers = {
263 |         getAxePath: () => '/usr/local/bin/axe',
264 |         getBundledAxeEnvironment: () => ({}),
265 |       };
266 | 
267 |       const result = await describe_uiLogic(
268 |         {
269 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
270 |         },
271 |         mockExecutor,
272 |         mockAxeHelpers,
273 |       );
274 | 
275 |       expect(result).toEqual({
276 |         content: [
277 |           {
278 |             type: 'text',
279 |             text: 'Error: System error executing axe: Failed to execute axe command: String error',
280 |           },
281 |         ],
282 |         isError: true,
283 |       });
284 |     });
285 |   });
286 | });
287 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for swift_package_build plugin
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using dependency injection for deterministic testing
  5 |  */
  6 | 
  7 | import { describe, it, expect, beforeEach } from 'vitest';
  8 | import {
  9 |   createMockExecutor,
 10 |   createMockFileSystemExecutor,
 11 |   createNoopExecutor,
 12 | } from '../../../../test-utils/mock-executors.ts';
 13 | import swiftPackageBuild, { swift_package_buildLogic } from '../swift_package_build.ts';
 14 | 
 15 | describe('swift_package_build plugin', () => {
 16 |   describe('Export Field Validation (Literal)', () => {
 17 |     it('should have correct name', () => {
 18 |       expect(swiftPackageBuild.name).toBe('swift_package_build');
 19 |     });
 20 | 
 21 |     it('should have correct description', () => {
 22 |       expect(swiftPackageBuild.description).toBe('Builds a Swift Package with swift build');
 23 |     });
 24 | 
 25 |     it('should have handler function', () => {
 26 |       expect(typeof swiftPackageBuild.handler).toBe('function');
 27 |     });
 28 | 
 29 |     it('should validate schema correctly', () => {
 30 |       // Test required fields
 31 |       expect(swiftPackageBuild.schema.packagePath.safeParse('/test/package').success).toBe(true);
 32 |       expect(swiftPackageBuild.schema.packagePath.safeParse('').success).toBe(true);
 33 | 
 34 |       // Test optional fields
 35 |       expect(swiftPackageBuild.schema.targetName.safeParse('MyTarget').success).toBe(true);
 36 |       expect(swiftPackageBuild.schema.targetName.safeParse(undefined).success).toBe(true);
 37 |       expect(swiftPackageBuild.schema.configuration.safeParse('debug').success).toBe(true);
 38 |       expect(swiftPackageBuild.schema.configuration.safeParse('release').success).toBe(true);
 39 |       expect(swiftPackageBuild.schema.configuration.safeParse(undefined).success).toBe(true);
 40 |       expect(swiftPackageBuild.schema.architectures.safeParse(['arm64']).success).toBe(true);
 41 |       expect(swiftPackageBuild.schema.architectures.safeParse(undefined).success).toBe(true);
 42 |       expect(swiftPackageBuild.schema.parseAsLibrary.safeParse(true).success).toBe(true);
 43 |       expect(swiftPackageBuild.schema.parseAsLibrary.safeParse(undefined).success).toBe(true);
 44 | 
 45 |       // Test invalid inputs
 46 |       expect(swiftPackageBuild.schema.packagePath.safeParse(null).success).toBe(false);
 47 |       expect(swiftPackageBuild.schema.configuration.safeParse('invalid').success).toBe(false);
 48 |       expect(swiftPackageBuild.schema.architectures.safeParse('not-array').success).toBe(false);
 49 |       expect(swiftPackageBuild.schema.parseAsLibrary.safeParse('yes').success).toBe(false);
 50 |     });
 51 |   });
 52 | 
 53 |   let executorCalls: any[] = [];
 54 | 
 55 |   beforeEach(() => {
 56 |     executorCalls = [];
 57 |   });
 58 | 
 59 |   describe('Command Generation Testing', () => {
 60 |     it('should build correct command for basic build', async () => {
 61 |       const executor = async (args: any, description: any, useShell: any, cwd: any) => {
 62 |         executorCalls.push({ args, description, useShell, cwd });
 63 |         return {
 64 |           success: true,
 65 |           output: 'Build succeeded',
 66 |           error: undefined,
 67 |           process: { pid: 12345 },
 68 |         };
 69 |       };
 70 | 
 71 |       await swift_package_buildLogic(
 72 |         {
 73 |           packagePath: '/test/package',
 74 |         },
 75 |         executor,
 76 |       );
 77 | 
 78 |       expect(executorCalls).toEqual([
 79 |         {
 80 |           args: ['swift', 'build', '--package-path', '/test/package'],
 81 |           description: 'Swift Package Build',
 82 |           useShell: true,
 83 |           cwd: undefined,
 84 |         },
 85 |       ]);
 86 |     });
 87 | 
 88 |     it('should build correct command with release configuration', async () => {
 89 |       const executor = async (args: any, description: any, useShell: any, cwd: any) => {
 90 |         executorCalls.push({ args, description, useShell, cwd });
 91 |         return {
 92 |           success: true,
 93 |           output: 'Build succeeded',
 94 |           error: undefined,
 95 |           process: { pid: 12345 },
 96 |         };
 97 |       };
 98 | 
 99 |       await swift_package_buildLogic(
100 |         {
101 |           packagePath: '/test/package',
102 |           configuration: 'release',
103 |         },
104 |         executor,
105 |       );
106 | 
107 |       expect(executorCalls).toEqual([
108 |         {
109 |           args: ['swift', 'build', '--package-path', '/test/package', '-c', 'release'],
110 |           description: 'Swift Package Build',
111 |           useShell: true,
112 |           cwd: undefined,
113 |         },
114 |       ]);
115 |     });
116 | 
117 |     it('should build correct command with all parameters', async () => {
118 |       const executor = async (args: any, description: any, useShell: any, cwd: any) => {
119 |         executorCalls.push({ args, description, useShell, cwd });
120 |         return {
121 |           success: true,
122 |           output: 'Build succeeded',
123 |           error: undefined,
124 |           process: { pid: 12345 },
125 |         };
126 |       };
127 | 
128 |       await swift_package_buildLogic(
129 |         {
130 |           packagePath: '/test/package',
131 |           targetName: 'MyTarget',
132 |           configuration: 'release',
133 |           architectures: ['arm64', 'x86_64'],
134 |           parseAsLibrary: true,
135 |         },
136 |         executor,
137 |       );
138 | 
139 |       expect(executorCalls).toEqual([
140 |         {
141 |           args: [
142 |             'swift',
143 |             'build',
144 |             '--package-path',
145 |             '/test/package',
146 |             '-c',
147 |             'release',
148 |             '--target',
149 |             'MyTarget',
150 |             '--arch',
151 |             'arm64',
152 |             '--arch',
153 |             'x86_64',
154 |             '-Xswiftc',
155 |             '-parse-as-library',
156 |           ],
157 |           description: 'Swift Package Build',
158 |           useShell: true,
159 |           cwd: undefined,
160 |         },
161 |       ]);
162 |     });
163 |   });
164 | 
165 |   describe('Response Logic Testing', () => {
166 |     it('should handle missing packagePath parameter (Zod handles validation)', async () => {
167 |       // Note: With createTypedTool, Zod validation happens before the logic function is called
168 |       // So we test with a valid but minimal parameter set since validation is handled upstream
169 |       const executor = createMockExecutor({
170 |         success: true,
171 |         output: 'Build succeeded',
172 |       });
173 | 
174 |       const result = await swift_package_buildLogic({ packagePath: '/test/package' }, executor);
175 | 
176 |       // The logic function should execute normally with valid parameters
177 |       // Zod validation errors are handled by createTypedTool wrapper
178 |       expect(result.isError).toBe(false);
179 |     });
180 | 
181 |     it('should return successful build response', async () => {
182 |       const executor = createMockExecutor({
183 |         success: true,
184 |         output: 'Build complete.',
185 |       });
186 | 
187 |       const result = await swift_package_buildLogic(
188 |         {
189 |           packagePath: '/test/package',
190 |         },
191 |         executor,
192 |       );
193 | 
194 |       expect(result).toEqual({
195 |         content: [
196 |           { type: 'text', text: '✅ Swift package build succeeded.' },
197 |           {
198 |             type: 'text',
199 |             text: '💡 Next: Run tests with swift_package_test or execute with swift_package_run',
200 |           },
201 |           { type: 'text', text: 'Build complete.' },
202 |         ],
203 |         isError: false,
204 |       });
205 |     });
206 | 
207 |     it('should return error response for build failure', async () => {
208 |       const executor = createMockExecutor({
209 |         success: false,
210 |         error: 'Compilation failed: error in main.swift',
211 |       });
212 | 
213 |       const result = await swift_package_buildLogic(
214 |         {
215 |           packagePath: '/test/package',
216 |         },
217 |         executor,
218 |       );
219 | 
220 |       expect(result).toEqual({
221 |         content: [
222 |           {
223 |             type: 'text',
224 |             text: 'Error: Swift package build failed\nDetails: Compilation failed: error in main.swift',
225 |           },
226 |         ],
227 |         isError: true,
228 |       });
229 |     });
230 | 
231 |     it('should handle spawn error', async () => {
232 |       const executor = async () => {
233 |         throw new Error('spawn ENOENT');
234 |       };
235 | 
236 |       const result = await swift_package_buildLogic(
237 |         {
238 |           packagePath: '/test/package',
239 |         },
240 |         executor,
241 |       );
242 | 
243 |       expect(result).toEqual({
244 |         content: [
245 |           {
246 |             type: 'text',
247 |             text: 'Error: Failed to execute swift build\nDetails: spawn ENOENT',
248 |           },
249 |         ],
250 |         isError: true,
251 |       });
252 |     });
253 | 
254 |     it('should handle successful build with parameters', async () => {
255 |       const executor = createMockExecutor({
256 |         success: true,
257 |         output: 'Build complete.',
258 |       });
259 | 
260 |       const result = await swift_package_buildLogic(
261 |         {
262 |           packagePath: '/test/package',
263 |           targetName: 'MyTarget',
264 |           configuration: 'release',
265 |           architectures: ['arm64', 'x86_64'],
266 |           parseAsLibrary: true,
267 |         },
268 |         executor,
269 |       );
270 | 
271 |       expect(result).toEqual({
272 |         content: [
273 |           { type: 'text', text: '✅ Swift package build succeeded.' },
274 |           {
275 |             type: 'text',
276 |             text: '💡 Next: Run tests with swift_package_test or execute with swift_package_run',
277 |           },
278 |           { type: 'text', text: 'Build complete.' },
279 |         ],
280 |         isError: false,
281 |       });
282 |     });
283 |   });
284 | });
285 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/doctor/__tests__/doctor.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for doctor plugin
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using dependency injection for deterministic testing
  5 |  */
  6 | 
  7 | import { describe, it, expect, beforeEach } from 'vitest';
  8 | import { z } from 'zod';
  9 | import doctor, { runDoctor, type DoctorDependencies } from '../doctor.ts';
 10 | 
 11 | function createDeps(overrides?: Partial<DoctorDependencies>): DoctorDependencies {
 12 |   const base: DoctorDependencies = {
 13 |     binaryChecker: {
 14 |       async checkBinaryAvailability(binary: string) {
 15 |         // default: all available with generic version
 16 |         return { available: true, version: `${binary} version 1.0.0` };
 17 |       },
 18 |     },
 19 |     xcode: {
 20 |       async getXcodeInfo() {
 21 |         return {
 22 |           version: 'Xcode 15.0 - Build version 15A240d',
 23 |           path: '/Applications/Xcode.app/Contents/Developer',
 24 |           selectedXcode: '/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild',
 25 |           xcrunVersion: 'xcrun version 65',
 26 |         };
 27 |       },
 28 |     },
 29 |     env: {
 30 |       getEnvironmentVariables() {
 31 |         const x: Record<string, string | undefined> = {
 32 |           XCODEBUILDMCP_DEBUG: 'true',
 33 |           INCREMENTAL_BUILDS_ENABLED: '1',
 34 |           PATH: '/usr/local/bin:/usr/bin:/bin',
 35 |           DEVELOPER_DIR: '/Applications/Xcode.app/Contents/Developer',
 36 |           HOME: '/Users/testuser',
 37 |           USER: 'testuser',
 38 |           TMPDIR: '/tmp',
 39 |           NODE_ENV: 'test',
 40 |           SENTRY_DISABLED: 'false',
 41 |         };
 42 |         return x;
 43 |       },
 44 |       getSystemInfo() {
 45 |         return {
 46 |           platform: 'darwin',
 47 |           release: '25.0.0',
 48 |           arch: 'arm64',
 49 |           cpus: '10 x Apple M3',
 50 |           memory: '32 GB',
 51 |           hostname: 'localhost',
 52 |           username: 'testuser',
 53 |           homedir: '/Users/testuser',
 54 |           tmpdir: '/tmp',
 55 |         };
 56 |       },
 57 |       getNodeInfo() {
 58 |         return {
 59 |           version: 'v22.0.0',
 60 |           execPath: '/usr/local/bin/node',
 61 |           pid: '123',
 62 |           ppid: '1',
 63 |           platform: 'darwin',
 64 |           arch: 'arm64',
 65 |           cwd: '/',
 66 |           argv: 'node build/index.js',
 67 |         };
 68 |       },
 69 |     },
 70 |     plugins: {
 71 |       async getPluginSystemInfo() {
 72 |         return {
 73 |           totalPlugins: 1,
 74 |           pluginDirectories: 1,
 75 |           pluginsByDirectory: { doctor: ['doctor'] },
 76 |           systemMode: 'plugin-based',
 77 |         };
 78 |       },
 79 |     },
 80 |     features: {
 81 |       areAxeToolsAvailable: () => true,
 82 |       isXcodemakeEnabled: () => true,
 83 |       isXcodemakeAvailable: async () => true,
 84 |       doesMakefileExist: () => true,
 85 |     },
 86 |     runtime: {
 87 |       async getRuntimeToolInfo() {
 88 |         return {
 89 |           mode: 'static' as const,
 90 |           enabledWorkflows: ['doctor', 'discovery'],
 91 |           enabledTools: ['doctor', 'discover_tools'],
 92 |           totalRegistered: 2,
 93 |         };
 94 |       },
 95 |     },
 96 |   };
 97 | 
 98 |   return {
 99 |     ...base,
100 |     ...overrides,
101 |     binaryChecker: {
102 |       ...base.binaryChecker,
103 |       ...(overrides?.binaryChecker ?? {}),
104 |     },
105 |     xcode: {
106 |       ...base.xcode,
107 |       ...(overrides?.xcode ?? {}),
108 |     },
109 |     env: {
110 |       ...base.env,
111 |       ...(overrides?.env ?? {}),
112 |     },
113 |     plugins: {
114 |       ...base.plugins,
115 |       ...(overrides?.plugins ?? {}),
116 |     },
117 |     features: {
118 |       ...base.features,
119 |       ...(overrides?.features ?? {}),
120 |     },
121 |   };
122 | }
123 | 
124 | describe('doctor tool', () => {
125 |   // Reset any state if needed
126 | 
127 |   describe('Export Field Validation (Literal)', () => {
128 |     it('should have correct name', () => {
129 |       expect(doctor.name).toBe('doctor');
130 |     });
131 | 
132 |     it('should have correct description', () => {
133 |       expect(doctor.description).toBe(
134 |         'Provides comprehensive information about the MCP server environment, available dependencies, and configuration status.',
135 |       );
136 |     });
137 | 
138 |     it('should have handler function', () => {
139 |       expect(typeof doctor.handler).toBe('function');
140 |     });
141 | 
142 |     it('should have correct schema with enabled boolean field', () => {
143 |       const schema = z.object(doctor.schema);
144 | 
145 |       // Valid inputs
146 |       expect(schema.safeParse({ enabled: true }).success).toBe(true);
147 |       expect(schema.safeParse({ enabled: false }).success).toBe(true);
148 |       expect(schema.safeParse({}).success).toBe(true); // enabled is optional
149 | 
150 |       // Invalid inputs
151 |       expect(schema.safeParse({ enabled: 'true' }).success).toBe(false);
152 |       expect(schema.safeParse({ enabled: 1 }).success).toBe(false);
153 |       expect(schema.safeParse({ enabled: null }).success).toBe(false);
154 |     });
155 |   });
156 | 
157 |   describe('Handler Behavior (Complete Literal Returns)', () => {
158 |     it('should handle successful doctor execution', async () => {
159 |       const deps = createDeps();
160 |       const result = await runDoctor({ enabled: true }, deps);
161 | 
162 |       expect(result.content).toEqual([
163 |         {
164 |           type: 'text',
165 |           text: result.content[0].text,
166 |         },
167 |       ]);
168 |       expect(typeof result.content[0].text).toBe('string');
169 |     });
170 | 
171 |     it('should handle plugin loading failure', async () => {
172 |       const deps = createDeps({
173 |         plugins: {
174 |           async getPluginSystemInfo() {
175 |             return { error: 'Plugin loading failed', systemMode: 'error' };
176 |           },
177 |         },
178 |       });
179 | 
180 |       const result = await runDoctor({ enabled: true }, deps);
181 | 
182 |       expect(result.content).toEqual([
183 |         {
184 |           type: 'text',
185 |           text: result.content[0].text,
186 |         },
187 |       ]);
188 |       expect(typeof result.content[0].text).toBe('string');
189 |     });
190 | 
191 |     it('should handle xcode command failure', async () => {
192 |       const deps = createDeps({
193 |         xcode: {
194 |           async getXcodeInfo() {
195 |             return { error: 'Xcode not found' };
196 |           },
197 |         },
198 |       });
199 |       const result = await runDoctor({ enabled: true }, deps);
200 | 
201 |       expect(result.content).toEqual([
202 |         {
203 |           type: 'text',
204 |           text: result.content[0].text,
205 |         },
206 |       ]);
207 |       expect(typeof result.content[0].text).toBe('string');
208 |     });
209 | 
210 |     it('should handle xcodemake check failure', async () => {
211 |       const deps = createDeps({
212 |         features: {
213 |           areAxeToolsAvailable: () => true,
214 |           isXcodemakeEnabled: () => true,
215 |           isXcodemakeAvailable: async () => false,
216 |           doesMakefileExist: () => true,
217 |         },
218 |         binaryChecker: {
219 |           async checkBinaryAvailability(binary: string) {
220 |             if (binary === 'xcodemake') return { available: false };
221 |             return { available: true, version: `${binary} version 1.0.0` };
222 |           },
223 |         },
224 |       });
225 |       const result = await runDoctor({ enabled: true }, deps);
226 | 
227 |       expect(result.content).toEqual([
228 |         {
229 |           type: 'text',
230 |           text: result.content[0].text,
231 |         },
232 |       ]);
233 |       expect(typeof result.content[0].text).toBe('string');
234 |     });
235 | 
236 |     it('should handle axe tools not available', async () => {
237 |       const deps = createDeps({
238 |         features: {
239 |           areAxeToolsAvailable: () => false,
240 |           isXcodemakeEnabled: () => false,
241 |           isXcodemakeAvailable: async () => false,
242 |           doesMakefileExist: () => false,
243 |         },
244 |         binaryChecker: {
245 |           async checkBinaryAvailability(binary: string) {
246 |             if (binary === 'axe') return { available: false };
247 |             if (binary === 'xcodemake') return { available: false };
248 |             if (binary === 'mise') return { available: true, version: 'mise 1.0.0' };
249 |             return { available: true };
250 |           },
251 |         },
252 |         env: {
253 |           getEnvironmentVariables() {
254 |             const x: Record<string, string | undefined> = {
255 |               XCODEBUILDMCP_DEBUG: 'true',
256 |               INCREMENTAL_BUILDS_ENABLED: '0',
257 |               PATH: '/usr/local/bin:/usr/bin:/bin',
258 |               DEVELOPER_DIR: '/Applications/Xcode.app/Contents/Developer',
259 |               HOME: '/Users/testuser',
260 |               USER: 'testuser',
261 |               TMPDIR: '/tmp',
262 |               NODE_ENV: 'test',
263 |               SENTRY_DISABLED: 'true',
264 |             };
265 |             return x;
266 |           },
267 |           getSystemInfo: () => ({
268 |             platform: 'darwin',
269 |             release: '25.0.0',
270 |             arch: 'arm64',
271 |             cpus: '10 x Apple M3',
272 |             memory: '32 GB',
273 |             hostname: 'localhost',
274 |             username: 'testuser',
275 |             homedir: '/Users/testuser',
276 |             tmpdir: '/tmp',
277 |           }),
278 |           getNodeInfo: () => ({
279 |             version: 'v22.0.0',
280 |             execPath: '/usr/local/bin/node',
281 |             pid: '123',
282 |             ppid: '1',
283 |             platform: 'darwin',
284 |             arch: 'arm64',
285 |             cwd: '/',
286 |             argv: 'node build/index.js',
287 |           }),
288 |         },
289 |       });
290 | 
291 |       const result = await runDoctor({ enabled: true }, deps);
292 | 
293 |       expect(result.content).toEqual([
294 |         {
295 |           type: 'text',
296 |           text: result.content[0].text,
297 |         },
298 |       ]);
299 |       expect(typeof result.content[0].text).toBe('string');
300 |     });
301 |   });
302 | });
303 | 
```

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

```typescript
  1 | /**
  2 |  * Pure dependency injection test for launch_mac_app plugin
  3 |  *
  4 |  * Tests plugin structure and macOS app launching functionality including parameter validation,
  5 |  * command generation, file validation, and response formatting.
  6 |  *
  7 |  * Uses manual call tracking and createMockFileSystemExecutor for file operations.
  8 |  */
  9 | 
 10 | import { describe, it, expect } from 'vitest';
 11 | import { z } from 'zod';
 12 | import { createMockFileSystemExecutor } from '../../../../test-utils/mock-executors.ts';
 13 | import launchMacApp, { launch_mac_appLogic } from '../launch_mac_app.ts';
 14 | 
 15 | describe('launch_mac_app plugin', () => {
 16 |   describe('Export Field Validation (Literal)', () => {
 17 |     it('should have correct name', () => {
 18 |       expect(launchMacApp.name).toBe('launch_mac_app');
 19 |     });
 20 | 
 21 |     it('should have correct description', () => {
 22 |       expect(launchMacApp.description).toBe(
 23 |         "Launches a macOS application. IMPORTANT: You MUST provide the appPath parameter. Example: launch_mac_app({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_launch_macos_app.",
 24 |       );
 25 |     });
 26 | 
 27 |     it('should have handler function', () => {
 28 |       expect(typeof launchMacApp.handler).toBe('function');
 29 |     });
 30 | 
 31 |     it('should validate schema with valid inputs', () => {
 32 |       const schema = z.object(launchMacApp.schema);
 33 |       expect(
 34 |         schema.safeParse({
 35 |           appPath: '/path/to/MyApp.app',
 36 |         }).success,
 37 |       ).toBe(true);
 38 |       expect(
 39 |         schema.safeParse({
 40 |           appPath: '/Applications/Calculator.app',
 41 |           args: ['--debug'],
 42 |         }).success,
 43 |       ).toBe(true);
 44 |       expect(
 45 |         schema.safeParse({
 46 |           appPath: '/path/to/MyApp.app',
 47 |           args: ['--debug', '--verbose'],
 48 |         }).success,
 49 |       ).toBe(true);
 50 |     });
 51 | 
 52 |     it('should validate schema with invalid inputs', () => {
 53 |       const schema = z.object(launchMacApp.schema);
 54 |       expect(schema.safeParse({}).success).toBe(false);
 55 |       expect(schema.safeParse({ appPath: null }).success).toBe(false);
 56 |       expect(schema.safeParse({ appPath: 123 }).success).toBe(false);
 57 |       expect(schema.safeParse({ appPath: '/path/to/MyApp.app', args: 'not-array' }).success).toBe(
 58 |         false,
 59 |       );
 60 |     });
 61 |   });
 62 | 
 63 |   describe('Input Validation', () => {
 64 |     it('should handle non-existent app path', async () => {
 65 |       const mockExecutor = async () => Promise.resolve({ stdout: '', stderr: '' });
 66 |       const mockFileSystem = createMockFileSystemExecutor({
 67 |         existsSync: () => false,
 68 |       });
 69 | 
 70 |       const result = await launch_mac_appLogic(
 71 |         {
 72 |           appPath: '/path/to/NonExistent.app',
 73 |         },
 74 |         mockExecutor,
 75 |         mockFileSystem,
 76 |       );
 77 | 
 78 |       expect(result).toEqual({
 79 |         content: [
 80 |           {
 81 |             type: 'text',
 82 |             text: "File not found: '/path/to/NonExistent.app'. Please check the path and try again.",
 83 |           },
 84 |         ],
 85 |         isError: true,
 86 |       });
 87 |     });
 88 |   });
 89 | 
 90 |   describe('Command Generation', () => {
 91 |     it('should generate correct command with minimal parameters', async () => {
 92 |       const calls: any[] = [];
 93 |       const mockExecutor = async (command: string[]) => {
 94 |         calls.push({ command });
 95 |         return { stdout: '', stderr: '' };
 96 |       };
 97 | 
 98 |       const mockFileSystem = createMockFileSystemExecutor({
 99 |         existsSync: () => true,
100 |       });
101 | 
102 |       await launch_mac_appLogic(
103 |         {
104 |           appPath: '/path/to/MyApp.app',
105 |         },
106 |         mockExecutor,
107 |         mockFileSystem,
108 |       );
109 | 
110 |       expect(calls).toHaveLength(1);
111 |       expect(calls[0].command).toEqual(['open', '/path/to/MyApp.app']);
112 |     });
113 | 
114 |     it('should generate correct command with args parameter', async () => {
115 |       const calls: any[] = [];
116 |       const mockExecutor = async (command: string[]) => {
117 |         calls.push({ command });
118 |         return { stdout: '', stderr: '' };
119 |       };
120 | 
121 |       const mockFileSystem = createMockFileSystemExecutor({
122 |         existsSync: () => true,
123 |       });
124 | 
125 |       await launch_mac_appLogic(
126 |         {
127 |           appPath: '/path/to/MyApp.app',
128 |           args: ['--debug', '--verbose'],
129 |         },
130 |         mockExecutor,
131 |         mockFileSystem,
132 |       );
133 | 
134 |       expect(calls).toHaveLength(1);
135 |       expect(calls[0].command).toEqual([
136 |         'open',
137 |         '/path/to/MyApp.app',
138 |         '--args',
139 |         '--debug',
140 |         '--verbose',
141 |       ]);
142 |     });
143 | 
144 |     it('should generate correct command with empty args array', async () => {
145 |       const calls: any[] = [];
146 |       const mockExecutor = async (command: string[]) => {
147 |         calls.push({ command });
148 |         return { stdout: '', stderr: '' };
149 |       };
150 | 
151 |       const mockFileSystem = createMockFileSystemExecutor({
152 |         existsSync: () => true,
153 |       });
154 | 
155 |       await launch_mac_appLogic(
156 |         {
157 |           appPath: '/path/to/MyApp.app',
158 |           args: [],
159 |         },
160 |         mockExecutor,
161 |         mockFileSystem,
162 |       );
163 | 
164 |       expect(calls).toHaveLength(1);
165 |       expect(calls[0].command).toEqual(['open', '/path/to/MyApp.app']);
166 |     });
167 | 
168 |     it('should handle paths with spaces correctly', async () => {
169 |       const calls: any[] = [];
170 |       const mockExecutor = async (command: string[]) => {
171 |         calls.push({ command });
172 |         return { stdout: '', stderr: '' };
173 |       };
174 | 
175 |       const mockFileSystem = createMockFileSystemExecutor({
176 |         existsSync: () => true,
177 |       });
178 | 
179 |       await launch_mac_appLogic(
180 |         {
181 |           appPath: '/Applications/My App.app',
182 |         },
183 |         mockExecutor,
184 |         mockFileSystem,
185 |       );
186 | 
187 |       expect(calls).toHaveLength(1);
188 |       expect(calls[0].command).toEqual(['open', '/Applications/My App.app']);
189 |     });
190 |   });
191 | 
192 |   describe('Response Processing', () => {
193 |     it('should return successful launch response', async () => {
194 |       const mockExecutor = async () => Promise.resolve({ stdout: '', stderr: '' });
195 | 
196 |       const mockFileSystem = createMockFileSystemExecutor({
197 |         existsSync: () => true,
198 |       });
199 | 
200 |       const result = await launch_mac_appLogic(
201 |         {
202 |           appPath: '/path/to/MyApp.app',
203 |         },
204 |         mockExecutor,
205 |         mockFileSystem,
206 |       );
207 | 
208 |       expect(result).toEqual({
209 |         content: [
210 |           {
211 |             type: 'text',
212 |             text: '✅ macOS app launched successfully: /path/to/MyApp.app',
213 |           },
214 |         ],
215 |       });
216 |     });
217 | 
218 |     it('should return successful launch response with args', async () => {
219 |       const mockExecutor = async () => Promise.resolve({ stdout: '', stderr: '' });
220 | 
221 |       const mockFileSystem = createMockFileSystemExecutor({
222 |         existsSync: () => true,
223 |       });
224 | 
225 |       const result = await launch_mac_appLogic(
226 |         {
227 |           appPath: '/path/to/MyApp.app',
228 |           args: ['--debug', '--verbose'],
229 |         },
230 |         mockExecutor,
231 |         mockFileSystem,
232 |       );
233 | 
234 |       expect(result).toEqual({
235 |         content: [
236 |           {
237 |             type: 'text',
238 |             text: '✅ macOS app launched successfully: /path/to/MyApp.app',
239 |           },
240 |         ],
241 |       });
242 |     });
243 | 
244 |     it('should handle launch failure with Error object', async () => {
245 |       const mockExecutor = async () => {
246 |         throw new Error('App not found');
247 |       };
248 | 
249 |       const mockFileSystem = createMockFileSystemExecutor({
250 |         existsSync: () => true,
251 |       });
252 | 
253 |       const result = await launch_mac_appLogic(
254 |         {
255 |           appPath: '/path/to/MyApp.app',
256 |         },
257 |         mockExecutor,
258 |         mockFileSystem,
259 |       );
260 | 
261 |       expect(result).toEqual({
262 |         content: [
263 |           {
264 |             type: 'text',
265 |             text: '❌ Launch macOS app operation failed: App not found',
266 |           },
267 |         ],
268 |         isError: true,
269 |       });
270 |     });
271 | 
272 |     it('should handle launch failure with string error', async () => {
273 |       const mockExecutor = async () => {
274 |         throw 'Permission denied';
275 |       };
276 | 
277 |       const mockFileSystem = createMockFileSystemExecutor({
278 |         existsSync: () => true,
279 |       });
280 | 
281 |       const result = await launch_mac_appLogic(
282 |         {
283 |           appPath: '/path/to/MyApp.app',
284 |         },
285 |         mockExecutor,
286 |         mockFileSystem,
287 |       );
288 | 
289 |       expect(result).toEqual({
290 |         content: [
291 |           {
292 |             type: 'text',
293 |             text: '❌ Launch macOS app operation failed: Permission denied',
294 |           },
295 |         ],
296 |         isError: true,
297 |       });
298 |     });
299 | 
300 |     it('should handle launch failure with unknown error type', async () => {
301 |       const mockExecutor = async () => {
302 |         throw 123;
303 |       };
304 | 
305 |       const mockFileSystem = createMockFileSystemExecutor({
306 |         existsSync: () => true,
307 |       });
308 | 
309 |       const result = await launch_mac_appLogic(
310 |         {
311 |           appPath: '/path/to/MyApp.app',
312 |         },
313 |         mockExecutor,
314 |         mockFileSystem,
315 |       );
316 | 
317 |       expect(result).toEqual({
318 |         content: [
319 |           {
320 |             type: 'text',
321 |             text: '❌ Launch macOS app operation failed: 123',
322 |           },
323 |         ],
324 |         isError: true,
325 |       });
326 |     });
327 |   });
328 | });
329 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/project-discovery/__tests__/get_mac_bundle_id.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach } from 'vitest';
  2 | import { z } from 'zod';
  3 | import plugin, { get_mac_bundle_idLogic } from '../get_mac_bundle_id.ts';
  4 | import {
  5 |   createMockFileSystemExecutor,
  6 |   createCommandMatchingMockExecutor,
  7 | } from '../../../../test-utils/mock-executors.ts';
  8 | 
  9 | describe('get_mac_bundle_id plugin', () => {
 10 |   // Helper function to create mock executor for command matching
 11 |   const createMockExecutorForCommands = (results: Record<string, string | Error>) => {
 12 |     return createCommandMatchingMockExecutor(
 13 |       Object.fromEntries(
 14 |         Object.entries(results).map(([command, result]) => [
 15 |           command,
 16 |           result instanceof Error
 17 |             ? { success: false, error: result.message }
 18 |             : { success: true, output: result },
 19 |         ]),
 20 |       ),
 21 |     );
 22 |   };
 23 | 
 24 |   describe('Export Field Validation (Literal)', () => {
 25 |     it('should have correct name', () => {
 26 |       expect(plugin.name).toBe('get_mac_bundle_id');
 27 |     });
 28 | 
 29 |     it('should have correct description', () => {
 30 |       expect(plugin.description).toBe(
 31 |         "Extracts the bundle identifier from a macOS app bundle (.app). IMPORTANT: You MUST provide the appPath parameter. Example: get_mac_bundle_id({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_get_macos_bundle_id.",
 32 |       );
 33 |     });
 34 | 
 35 |     it('should have handler function', () => {
 36 |       expect(typeof plugin.handler).toBe('function');
 37 |     });
 38 | 
 39 |     it('should validate schema with valid inputs', () => {
 40 |       const schema = z.object(plugin.schema);
 41 |       expect(schema.safeParse({ appPath: '/Applications/TextEdit.app' }).success).toBe(true);
 42 |       expect(schema.safeParse({ appPath: '/Users/dev/MyApp.app' }).success).toBe(true);
 43 |     });
 44 | 
 45 |     it('should validate schema with invalid inputs', () => {
 46 |       const schema = z.object(plugin.schema);
 47 |       expect(schema.safeParse({}).success).toBe(false);
 48 |       expect(schema.safeParse({ appPath: 123 }).success).toBe(false);
 49 |       expect(schema.safeParse({ appPath: null }).success).toBe(false);
 50 |       expect(schema.safeParse({ appPath: undefined }).success).toBe(false);
 51 |     });
 52 |   });
 53 | 
 54 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 55 |     // Note: appPath validation is now handled by Zod schema validation in createTypedTool
 56 |     // This test would not reach the logic function as Zod validation occurs before it
 57 | 
 58 |     it('should return error when file exists validation fails', async () => {
 59 |       const mockExecutor = createMockExecutorForCommands({});
 60 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
 61 |         existsSync: () => false,
 62 |       });
 63 | 
 64 |       const result = await get_mac_bundle_idLogic(
 65 |         { appPath: '/Applications/MyApp.app' },
 66 |         mockExecutor,
 67 |         mockFileSystemExecutor,
 68 |       );
 69 | 
 70 |       expect(result).toEqual({
 71 |         content: [
 72 |           {
 73 |             type: 'text',
 74 |             text: "File not found: '/Applications/MyApp.app'. Please check the path and try again.",
 75 |           },
 76 |         ],
 77 |         isError: true,
 78 |       });
 79 |     });
 80 | 
 81 |     it('should return success with bundle ID using defaults read', async () => {
 82 |       const mockExecutor = createMockExecutorForCommands({
 83 |         'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier':
 84 |           'com.example.MyMacApp',
 85 |       });
 86 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
 87 |         existsSync: () => true,
 88 |       });
 89 | 
 90 |       const result = await get_mac_bundle_idLogic(
 91 |         { appPath: '/Applications/MyApp.app' },
 92 |         mockExecutor,
 93 |         mockFileSystemExecutor,
 94 |       );
 95 | 
 96 |       expect(result).toEqual({
 97 |         content: [
 98 |           {
 99 |             type: 'text',
100 |             text: '✅ Bundle ID: com.example.MyMacApp',
101 |           },
102 |           {
103 |             type: 'text',
104 |             text: `Next Steps:
105 | - Launch: launch_mac_app({ appPath: "/Applications/MyApp.app" })
106 | - Build again: build_macos({ scheme: "SCHEME_NAME" })`,
107 |           },
108 |         ],
109 |         isError: false,
110 |       });
111 |     });
112 | 
113 |     it('should fallback to PlistBuddy when defaults read fails', async () => {
114 |       const mockExecutor = createMockExecutorForCommands({
115 |         'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier': new Error(
116 |           'defaults read failed',
117 |         ),
118 |         '/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/Applications/MyApp.app/Contents/Info.plist"':
119 |           'com.example.MyMacApp',
120 |       });
121 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
122 |         existsSync: () => true,
123 |       });
124 | 
125 |       const result = await get_mac_bundle_idLogic(
126 |         { appPath: '/Applications/MyApp.app' },
127 |         mockExecutor,
128 |         mockFileSystemExecutor,
129 |       );
130 | 
131 |       expect(result).toEqual({
132 |         content: [
133 |           {
134 |             type: 'text',
135 |             text: '✅ Bundle ID: com.example.MyMacApp',
136 |           },
137 |           {
138 |             type: 'text',
139 |             text: `Next Steps:
140 | - Launch: launch_mac_app({ appPath: "/Applications/MyApp.app" })
141 | - Build again: build_macos({ scheme: "SCHEME_NAME" })`,
142 |           },
143 |         ],
144 |         isError: false,
145 |       });
146 |     });
147 | 
148 |     it('should return error when both extraction methods fail', async () => {
149 |       const mockExecutor = createMockExecutorForCommands({
150 |         'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier': new Error(
151 |           'Command failed',
152 |         ),
153 |         '/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/Applications/MyApp.app/Contents/Info.plist"':
154 |           new Error('Command failed'),
155 |       });
156 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
157 |         existsSync: () => true,
158 |       });
159 | 
160 |       const result = await get_mac_bundle_idLogic(
161 |         { appPath: '/Applications/MyApp.app' },
162 |         mockExecutor,
163 |         mockFileSystemExecutor,
164 |       );
165 | 
166 |       expect(result.isError).toBe(true);
167 |       expect(result.content).toHaveLength(2);
168 |       expect(result.content[0].type).toBe('text');
169 |       expect(result.content[0].text).toContain('Error extracting macOS bundle ID');
170 |       expect(result.content[0].text).toContain('Could not extract bundle ID from Info.plist');
171 |       expect(result.content[0].text).toContain('Command failed');
172 |       expect(result.content[1].type).toBe('text');
173 |       expect(result.content[1].text).toBe(
174 |         'Make sure the path points to a valid macOS app bundle (.app directory).',
175 |       );
176 |     });
177 | 
178 |     it('should handle Error objects in catch blocks', async () => {
179 |       const mockExecutor = createMockExecutorForCommands({
180 |         'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier': new Error(
181 |           'Custom error message',
182 |         ),
183 |         '/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/Applications/MyApp.app/Contents/Info.plist"':
184 |           new Error('Custom error message'),
185 |       });
186 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
187 |         existsSync: () => true,
188 |       });
189 | 
190 |       const result = await get_mac_bundle_idLogic(
191 |         { appPath: '/Applications/MyApp.app' },
192 |         mockExecutor,
193 |         mockFileSystemExecutor,
194 |       );
195 | 
196 |       expect(result.isError).toBe(true);
197 |       expect(result.content).toHaveLength(2);
198 |       expect(result.content[0].type).toBe('text');
199 |       expect(result.content[0].text).toContain('Error extracting macOS bundle ID');
200 |       expect(result.content[0].text).toContain('Could not extract bundle ID from Info.plist');
201 |       expect(result.content[0].text).toContain('Custom error message');
202 |       expect(result.content[1].type).toBe('text');
203 |       expect(result.content[1].text).toBe(
204 |         'Make sure the path points to a valid macOS app bundle (.app directory).',
205 |       );
206 |     });
207 | 
208 |     it('should handle string errors in catch blocks', async () => {
209 |       const mockExecutor = createMockExecutorForCommands({
210 |         'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier': new Error(
211 |           'String error',
212 |         ),
213 |         '/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/Applications/MyApp.app/Contents/Info.plist"':
214 |           new Error('String error'),
215 |       });
216 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
217 |         existsSync: () => true,
218 |       });
219 | 
220 |       const result = await get_mac_bundle_idLogic(
221 |         { appPath: '/Applications/MyApp.app' },
222 |         mockExecutor,
223 |         mockFileSystemExecutor,
224 |       );
225 | 
226 |       expect(result.isError).toBe(true);
227 |       expect(result.content).toHaveLength(2);
228 |       expect(result.content[0].type).toBe('text');
229 |       expect(result.content[0].text).toContain('Error extracting macOS bundle ID');
230 |       expect(result.content[0].text).toContain('Could not extract bundle ID from Info.plist');
231 |       expect(result.content[0].text).toContain('String error');
232 |       expect(result.content[1].type).toBe('text');
233 |       expect(result.content[1].text).toBe(
234 |         'Make sure the path points to a valid macOS app bundle (.app directory).',
235 |       );
236 |     });
237 |   });
238 | });
239 | 
```

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

```typescript
  1 | /**
  2 |  * Project Discovery Plugin: Discover Projects
  3 |  *
  4 |  * Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj)
  5 |  * and workspace (.xcworkspace) files.
  6 |  */
  7 | 
  8 | import { z } from 'zod';
  9 | import * as path from 'node:path';
 10 | import { log } from '../../../utils/logging/index.ts';
 11 | import { ToolResponse, createTextContent } from '../../../types/common.ts';
 12 | import { getDefaultFileSystemExecutor, getDefaultCommandExecutor } from '../../../utils/command.ts';
 13 | import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
 14 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
 15 | 
 16 | // Constants
 17 | const DEFAULT_MAX_DEPTH = 5;
 18 | const SKIPPED_DIRS = new Set(['build', 'DerivedData', 'Pods', '.git', 'node_modules']);
 19 | 
 20 | // Type definition for Dirent-like objects returned by readdir with withFileTypes: true
 21 | interface DirentLike {
 22 |   name: string;
 23 |   isDirectory(): boolean;
 24 |   isSymbolicLink(): boolean;
 25 | }
 26 | 
 27 | /**
 28 |  * Recursively scans directories to find Xcode projects and workspaces.
 29 |  */
 30 | async function _findProjectsRecursive(
 31 |   currentDirAbs: string,
 32 |   workspaceRootAbs: string,
 33 |   currentDepth: number,
 34 |   maxDepth: number,
 35 |   results: { projects: string[]; workspaces: string[] },
 36 |   fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(),
 37 | ): Promise<void> {
 38 |   // Explicit depth check (now simplified as maxDepth is always non-negative)
 39 |   if (currentDepth >= maxDepth) {
 40 |     log('debug', `Max depth ${maxDepth} reached at ${currentDirAbs}, stopping recursion.`);
 41 |     return;
 42 |   }
 43 | 
 44 |   log('debug', `Scanning directory: ${currentDirAbs} at depth ${currentDepth}`);
 45 |   const normalizedWorkspaceRoot = path.normalize(workspaceRootAbs);
 46 | 
 47 |   try {
 48 |     // Use the injected fileSystemExecutor
 49 |     const entries = await fileSystemExecutor.readdir(currentDirAbs, { withFileTypes: true });
 50 |     for (const rawEntry of entries) {
 51 |       // Cast the unknown entry to DirentLike interface for type safety
 52 |       const entry = rawEntry as DirentLike;
 53 |       const absoluteEntryPath = path.join(currentDirAbs, entry.name);
 54 |       const relativePath = path.relative(workspaceRootAbs, absoluteEntryPath);
 55 | 
 56 |       // --- Skip conditions ---
 57 |       if (entry.isSymbolicLink()) {
 58 |         log('debug', `Skipping symbolic link: ${relativePath}`);
 59 |         continue;
 60 |       }
 61 | 
 62 |       // Skip common build/dependency directories by name
 63 |       if (entry.isDirectory() && SKIPPED_DIRS.has(entry.name)) {
 64 |         log('debug', `Skipping standard directory: ${relativePath}`);
 65 |         continue;
 66 |       }
 67 | 
 68 |       // Ensure entry is within the workspace root (security/sanity check)
 69 |       if (!path.normalize(absoluteEntryPath).startsWith(normalizedWorkspaceRoot)) {
 70 |         log(
 71 |           'warn',
 72 |           `Skipping entry outside workspace root: ${absoluteEntryPath} (Workspace: ${workspaceRootAbs})`,
 73 |         );
 74 |         continue;
 75 |       }
 76 | 
 77 |       // --- Process entries ---
 78 |       if (entry.isDirectory()) {
 79 |         let isXcodeBundle = false;
 80 | 
 81 |         if (entry.name.endsWith('.xcodeproj')) {
 82 |           results.projects.push(absoluteEntryPath); // Use absolute path
 83 |           log('debug', `Found project: ${absoluteEntryPath}`);
 84 |           isXcodeBundle = true;
 85 |         } else if (entry.name.endsWith('.xcworkspace')) {
 86 |           results.workspaces.push(absoluteEntryPath); // Use absolute path
 87 |           log('debug', `Found workspace: ${absoluteEntryPath}`);
 88 |           isXcodeBundle = true;
 89 |         }
 90 | 
 91 |         // Recurse into regular directories, but not into found project/workspace bundles
 92 |         if (!isXcodeBundle) {
 93 |           await _findProjectsRecursive(
 94 |             absoluteEntryPath,
 95 |             workspaceRootAbs,
 96 |             currentDepth + 1,
 97 |             maxDepth,
 98 |             results,
 99 |             fileSystemExecutor,
100 |           );
101 |         }
102 |       }
103 |     }
104 |   } catch (error) {
105 |     let code;
106 |     let message = 'Unknown error';
107 | 
108 |     if (error instanceof Error) {
109 |       message = error.message;
110 |       if ('code' in error) {
111 |         code = error.code;
112 |       }
113 |     } else if (typeof error === 'object' && error !== null) {
114 |       if ('message' in error && typeof error.message === 'string') {
115 |         message = error.message;
116 |       }
117 |       if ('code' in error && typeof error.code === 'string') {
118 |         code = error.code;
119 |       }
120 |     } else {
121 |       message = String(error);
122 |     }
123 | 
124 |     if (code === 'EPERM' || code === 'EACCES') {
125 |       log('debug', `Permission denied scanning directory: ${currentDirAbs}`);
126 |     } else {
127 |       log(
128 |         'warning',
129 |         `Error scanning directory ${currentDirAbs}: ${message} (Code: ${code ?? 'N/A'})`,
130 |       );
131 |     }
132 |   }
133 | }
134 | 
135 | // Define schema as ZodObject
136 | const discoverProjsSchema = z.object({
137 |   workspaceRoot: z.string().describe('The absolute path of the workspace root to scan within.'),
138 |   scanPath: z
139 |     .string()
140 |     .optional()
141 |     .describe('Optional: Path relative to workspace root to scan. Defaults to workspace root.'),
142 |   maxDepth: z
143 |     .number()
144 |     .int()
145 |     .nonnegative()
146 |     .optional()
147 |     .describe(`Optional: Maximum directory depth to scan. Defaults to ${DEFAULT_MAX_DEPTH}.`),
148 | });
149 | 
150 | // Use z.infer for type safety
151 | type DiscoverProjsParams = z.infer<typeof discoverProjsSchema>;
152 | 
153 | /**
154 |  * Business logic for discovering projects.
155 |  * Exported for testing purposes.
156 |  */
157 | export async function discover_projsLogic(
158 |   params: DiscoverProjsParams,
159 |   fileSystemExecutor: FileSystemExecutor,
160 | ): Promise<ToolResponse> {
161 |   // Apply defaults
162 |   const scanPath = params.scanPath ?? '.';
163 |   const maxDepth = params.maxDepth ?? DEFAULT_MAX_DEPTH;
164 |   const workspaceRoot = params.workspaceRoot;
165 | 
166 |   const relativeScanPath = scanPath;
167 | 
168 |   // Calculate and validate the absolute scan path
169 |   const requestedScanPath = path.resolve(workspaceRoot, relativeScanPath ?? '.');
170 |   let absoluteScanPath = requestedScanPath;
171 |   const normalizedWorkspaceRoot = path.normalize(workspaceRoot);
172 |   if (!path.normalize(absoluteScanPath).startsWith(normalizedWorkspaceRoot)) {
173 |     log(
174 |       'warn',
175 |       `Requested scan path '${relativeScanPath}' resolved outside workspace root '${workspaceRoot}'. Defaulting scan to workspace root.`,
176 |     );
177 |     absoluteScanPath = normalizedWorkspaceRoot;
178 |   }
179 | 
180 |   const results = { projects: [], workspaces: [] };
181 | 
182 |   log(
183 |     'info',
184 |     `Starting project discovery request: path=${absoluteScanPath}, maxDepth=${maxDepth}, workspace=${workspaceRoot}`,
185 |   );
186 | 
187 |   try {
188 |     // Ensure the scan path exists and is a directory
189 |     const stats = await fileSystemExecutor.stat(absoluteScanPath);
190 |     if (!stats.isDirectory()) {
191 |       const errorMsg = `Scan path is not a directory: ${absoluteScanPath}`;
192 |       log('error', errorMsg);
193 |       // Return ToolResponse error format
194 |       return {
195 |         content: [createTextContent(errorMsg)],
196 |         isError: true,
197 |       };
198 |     }
199 |   } catch (error) {
200 |     let code;
201 |     let message = 'Unknown error accessing scan path';
202 | 
203 |     // Type guards - refined
204 |     if (error instanceof Error) {
205 |       message = error.message;
206 |       // Check for code property specific to Node.js fs errors
207 |       if ('code' in error) {
208 |         code = error.code;
209 |       }
210 |     } else if (typeof error === 'object' && error !== null) {
211 |       if ('message' in error && typeof error.message === 'string') {
212 |         message = error.message;
213 |       }
214 |       if ('code' in error && typeof error.code === 'string') {
215 |         code = error.code;
216 |       }
217 |     } else {
218 |       message = String(error);
219 |     }
220 | 
221 |     const errorMsg = `Failed to access scan path: ${absoluteScanPath}. Error: ${message}`;
222 |     log('error', `${errorMsg} - Code: ${code ?? 'N/A'}`);
223 |     return {
224 |       content: [createTextContent(errorMsg)],
225 |       isError: true,
226 |     };
227 |   }
228 | 
229 |   // Start the recursive scan from the validated absolute path
230 |   await _findProjectsRecursive(
231 |     absoluteScanPath,
232 |     workspaceRoot,
233 |     0,
234 |     maxDepth,
235 |     results,
236 |     fileSystemExecutor,
237 |   );
238 | 
239 |   log(
240 |     'info',
241 |     `Discovery finished. Found ${results.projects.length} projects and ${results.workspaces.length} workspaces.`,
242 |   );
243 | 
244 |   const responseContent = [
245 |     createTextContent(
246 |       `Discovery finished. Found ${results.projects.length} projects and ${results.workspaces.length} workspaces.`,
247 |     ),
248 |   ];
249 | 
250 |   // Sort results for consistent output
251 |   results.projects.sort();
252 |   results.workspaces.sort();
253 | 
254 |   if (results.projects.length > 0) {
255 |     responseContent.push(
256 |       createTextContent(`Projects found:\n - ${results.projects.join('\n - ')}`),
257 |     );
258 |   }
259 | 
260 |   if (results.workspaces.length > 0) {
261 |     responseContent.push(
262 |       createTextContent(`Workspaces found:\n - ${results.workspaces.join('\n - ')}`),
263 |     );
264 |   }
265 | 
266 |   return {
267 |     content: responseContent,
268 |     isError: false,
269 |   };
270 | }
271 | 
272 | export default {
273 |   name: 'discover_projs',
274 |   description:
275 |     'Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj) and workspace (.xcworkspace) files.',
276 |   schema: discoverProjsSchema.shape, // MCP SDK compatibility
277 |   handler: createTypedTool(
278 |     discoverProjsSchema,
279 |     (params: DiscoverProjsParams) => {
280 |       return discover_projsLogic(params, getDefaultFileSystemExecutor());
281 |     },
282 |     getDefaultCommandExecutor,
283 |   ),
284 | };
285 | 
```
Page 6/14FirstPrevNextLast