#
tokens: 49580/50000 10/337 files (page 10/14)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 10 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/ui-testing/__tests__/long_press.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for long_press tool plugin
  3 |  */
  4 | 
  5 | import { describe, it, expect } from 'vitest';
  6 | import { z } from 'zod';
  7 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
  8 | import longPressPlugin, { long_pressLogic } from '../long_press.ts';
  9 | 
 10 | describe('Long Press Plugin', () => {
 11 |   // Setup for each test - no vitest mocks to clear
 12 | 
 13 |   describe('Export Field Validation (Literal)', () => {
 14 |     it('should have correct name', () => {
 15 |       expect(longPressPlugin.name).toBe('long_press');
 16 |     });
 17 | 
 18 |     it('should have correct description', () => {
 19 |       expect(longPressPlugin.description).toBe(
 20 |         "Long press at specific coordinates for given duration (ms). Use describe_ui for precise coordinates (don't guess from screenshots).",
 21 |       );
 22 |     });
 23 | 
 24 |     it('should have handler function', () => {
 25 |       expect(typeof longPressPlugin.handler).toBe('function');
 26 |     });
 27 | 
 28 |     it('should validate schema fields with safeParse', () => {
 29 |       const schema = z.object(longPressPlugin.schema);
 30 | 
 31 |       // Valid case
 32 |       expect(
 33 |         schema.safeParse({
 34 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 35 |           x: 100,
 36 |           y: 200,
 37 |           duration: 1500,
 38 |         }).success,
 39 |       ).toBe(true);
 40 | 
 41 |       // Invalid simulatorUuid
 42 |       expect(
 43 |         schema.safeParse({
 44 |           simulatorUuid: 'invalid-uuid',
 45 |           x: 100,
 46 |           y: 200,
 47 |           duration: 1500,
 48 |         }).success,
 49 |       ).toBe(false);
 50 | 
 51 |       // Invalid x (not integer)
 52 |       expect(
 53 |         schema.safeParse({
 54 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 55 |           x: 100.5,
 56 |           y: 200,
 57 |           duration: 1500,
 58 |         }).success,
 59 |       ).toBe(false);
 60 | 
 61 |       // Invalid y (not integer)
 62 |       expect(
 63 |         schema.safeParse({
 64 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 65 |           x: 100,
 66 |           y: 200.5,
 67 |           duration: 1500,
 68 |         }).success,
 69 |       ).toBe(false);
 70 | 
 71 |       // Invalid duration (not positive)
 72 |       expect(
 73 |         schema.safeParse({
 74 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 75 |           x: 100,
 76 |           y: 200,
 77 |           duration: 0,
 78 |         }).success,
 79 |       ).toBe(false);
 80 | 
 81 |       // Invalid duration (negative)
 82 |       expect(
 83 |         schema.safeParse({
 84 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 85 |           x: 100,
 86 |           y: 200,
 87 |           duration: -100,
 88 |         }).success,
 89 |       ).toBe(false);
 90 |     });
 91 |   });
 92 | 
 93 |   describe('Command Generation', () => {
 94 |     it('should generate correct axe command for basic long press', async () => {
 95 |       let capturedCommand: string[] = [];
 96 |       const trackingExecutor = async (command: string[]) => {
 97 |         capturedCommand = command;
 98 |         return {
 99 |           success: true,
100 |           output: 'long press completed',
101 |           error: undefined,
102 |           process: { pid: 12345 },
103 |         };
104 |       };
105 | 
106 |       const mockAxeHelpers = {
107 |         getAxePath: () => '/usr/local/bin/axe',
108 |         getBundledAxeEnvironment: () => ({}),
109 |         createAxeNotAvailableResponse: () => ({
110 |           content: [{ type: 'text', text: 'Mock axe not available' }],
111 |           isError: true,
112 |         }),
113 |       };
114 | 
115 |       await long_pressLogic(
116 |         {
117 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
118 |           x: 100,
119 |           y: 200,
120 |           duration: 1500,
121 |         },
122 |         trackingExecutor,
123 |         mockAxeHelpers,
124 |       );
125 | 
126 |       expect(capturedCommand).toEqual([
127 |         '/usr/local/bin/axe',
128 |         'touch',
129 |         '-x',
130 |         '100',
131 |         '-y',
132 |         '200',
133 |         '--down',
134 |         '--up',
135 |         '--delay',
136 |         '1.5',
137 |         '--udid',
138 |         '12345678-1234-1234-1234-123456789012',
139 |       ]);
140 |     });
141 | 
142 |     it('should generate correct axe command for long press with different coordinates', async () => {
143 |       let capturedCommand: string[] = [];
144 |       const trackingExecutor = async (command: string[]) => {
145 |         capturedCommand = command;
146 |         return {
147 |           success: true,
148 |           output: 'long press completed',
149 |           error: undefined,
150 |           process: { pid: 12345 },
151 |         };
152 |       };
153 | 
154 |       const mockAxeHelpers = {
155 |         getAxePath: () => '/usr/local/bin/axe',
156 |         getBundledAxeEnvironment: () => ({}),
157 |         createAxeNotAvailableResponse: () => ({
158 |           content: [{ type: 'text', text: 'Mock axe not available' }],
159 |           isError: true,
160 |         }),
161 |       };
162 | 
163 |       await long_pressLogic(
164 |         {
165 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
166 |           x: 50,
167 |           y: 75,
168 |           duration: 2000,
169 |         },
170 |         trackingExecutor,
171 |         mockAxeHelpers,
172 |       );
173 | 
174 |       expect(capturedCommand).toEqual([
175 |         '/usr/local/bin/axe',
176 |         'touch',
177 |         '-x',
178 |         '50',
179 |         '-y',
180 |         '75',
181 |         '--down',
182 |         '--up',
183 |         '--delay',
184 |         '2',
185 |         '--udid',
186 |         '12345678-1234-1234-1234-123456789012',
187 |       ]);
188 |     });
189 | 
190 |     it('should generate correct axe command for short duration long press', async () => {
191 |       let capturedCommand: string[] = [];
192 |       const trackingExecutor = async (command: string[]) => {
193 |         capturedCommand = command;
194 |         return {
195 |           success: true,
196 |           output: 'long press completed',
197 |           error: undefined,
198 |           process: { pid: 12345 },
199 |         };
200 |       };
201 | 
202 |       const mockAxeHelpers = {
203 |         getAxePath: () => '/usr/local/bin/axe',
204 |         getBundledAxeEnvironment: () => ({}),
205 |         createAxeNotAvailableResponse: () => ({
206 |           content: [{ type: 'text', text: 'Mock axe not available' }],
207 |           isError: true,
208 |         }),
209 |       };
210 | 
211 |       await long_pressLogic(
212 |         {
213 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
214 |           x: 300,
215 |           y: 400,
216 |           duration: 500,
217 |         },
218 |         trackingExecutor,
219 |         mockAxeHelpers,
220 |       );
221 | 
222 |       expect(capturedCommand).toEqual([
223 |         '/usr/local/bin/axe',
224 |         'touch',
225 |         '-x',
226 |         '300',
227 |         '-y',
228 |         '400',
229 |         '--down',
230 |         '--up',
231 |         '--delay',
232 |         '0.5',
233 |         '--udid',
234 |         '12345678-1234-1234-1234-123456789012',
235 |       ]);
236 |     });
237 | 
238 |     it('should generate correct axe command with bundled axe path', async () => {
239 |       let capturedCommand: string[] = [];
240 |       const trackingExecutor = async (command: string[]) => {
241 |         capturedCommand = command;
242 |         return {
243 |           success: true,
244 |           output: 'long press completed',
245 |           error: undefined,
246 |           process: { pid: 12345 },
247 |         };
248 |       };
249 | 
250 |       const mockAxeHelpers = {
251 |         getAxePath: () => '/path/to/bundled/axe',
252 |         getBundledAxeEnvironment: () => ({ AXE_PATH: '/some/path' }),
253 |         createAxeNotAvailableResponse: () => ({
254 |           content: [{ type: 'text', text: 'Mock axe not available' }],
255 |           isError: true,
256 |         }),
257 |       };
258 | 
259 |       await long_pressLogic(
260 |         {
261 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
262 |           x: 150,
263 |           y: 250,
264 |           duration: 3000,
265 |         },
266 |         trackingExecutor,
267 |         mockAxeHelpers,
268 |       );
269 | 
270 |       expect(capturedCommand).toEqual([
271 |         '/path/to/bundled/axe',
272 |         'touch',
273 |         '-x',
274 |         '150',
275 |         '-y',
276 |         '250',
277 |         '--down',
278 |         '--up',
279 |         '--delay',
280 |         '3',
281 |         '--udid',
282 |         '12345678-1234-1234-1234-123456789012',
283 |       ]);
284 |     });
285 |   });
286 | 
287 |   describe('Handler Behavior (Complete Literal Returns)', () => {
288 |     it('should return success for valid long press execution', async () => {
289 |       const mockExecutor = createMockExecutor({
290 |         success: true,
291 |         output: 'long press completed',
292 |         error: '',
293 |       });
294 | 
295 |       const mockAxeHelpers = {
296 |         getAxePath: () => '/usr/local/bin/axe',
297 |         getBundledAxeEnvironment: () => ({}),
298 |         createAxeNotAvailableResponse: () => ({
299 |           content: [{ type: 'text', text: 'Mock axe not available' }],
300 |           isError: true,
301 |         }),
302 |       };
303 | 
304 |       const result = await long_pressLogic(
305 |         {
306 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
307 |           x: 100,
308 |           y: 200,
309 |           duration: 1500,
310 |         },
311 |         mockExecutor,
312 |         mockAxeHelpers,
313 |       );
314 | 
315 |       expect(result).toEqual({
316 |         content: [
317 |           {
318 |             type: 'text',
319 |             text: 'Long press at (100, 200) for 1500ms simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
320 |           },
321 |         ],
322 |         isError: false,
323 |       });
324 |     });
325 | 
326 |     it('should handle DependencyError when axe is not available', async () => {
327 |       const mockExecutor = createMockExecutor({
328 |         success: true,
329 |         output: '',
330 |         error: undefined,
331 |         process: { pid: 12345 },
332 |       });
333 | 
334 |       const mockAxeHelpers = {
335 |         getAxePath: () => null, // Mock axe not found
336 |         getBundledAxeEnvironment: () => ({}),
337 |         createAxeNotAvailableResponse: () => ({
338 |           content: [
339 |             {
340 |               type: 'text',
341 |               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.',
342 |             },
343 |           ],
344 |           isError: true,
345 |         }),
346 |       };
347 | 
348 |       const result = await long_pressLogic(
349 |         {
350 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
351 |           x: 100,
352 |           y: 200,
353 |           duration: 1500,
354 |         },
355 |         mockExecutor,
356 |         mockAxeHelpers,
357 |       );
358 | 
359 |       expect(result).toEqual({
360 |         content: [
361 |           {
362 |             type: 'text',
363 |             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.',
364 |           },
365 |         ],
366 |         isError: true,
367 |       });
368 |     });
369 | 
370 |     it('should handle AxeError from failed command execution', async () => {
371 |       const mockExecutor = createMockExecutor({
372 |         success: false,
373 |         output: '',
374 |         error: 'axe command failed',
375 |         process: { pid: 12345 },
376 |       });
377 | 
378 |       const mockAxeHelpers = {
379 |         getAxePath: () => '/usr/local/bin/axe',
380 |         getBundledAxeEnvironment: () => ({}),
381 |         createAxeNotAvailableResponse: () => ({
382 |           content: [{ type: 'text', text: 'Mock axe not available' }],
383 |           isError: true,
384 |         }),
385 |       };
386 | 
387 |       const result = await long_pressLogic(
388 |         {
389 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
390 |           x: 100,
391 |           y: 200,
392 |           duration: 1500,
393 |         },
394 |         mockExecutor,
395 |         mockAxeHelpers,
396 |       );
397 | 
398 |       expect(result).toEqual({
399 |         content: [
400 |           {
401 |             type: 'text',
402 |             text: "Error: Failed to simulate long press at (100, 200): axe command 'touch' failed.\nDetails: axe command failed",
403 |           },
404 |         ],
405 |         isError: true,
406 |       });
407 |     });
408 | 
409 |     it('should handle SystemError from command execution', async () => {
410 |       const mockExecutor = () => {
411 |         throw new Error('ENOENT: no such file or directory');
412 |       };
413 | 
414 |       const mockAxeHelpers = {
415 |         getAxePath: () => '/usr/local/bin/axe',
416 |         getBundledAxeEnvironment: () => ({}),
417 |         createAxeNotAvailableResponse: () => ({
418 |           content: [{ type: 'text', text: 'Mock axe not available' }],
419 |           isError: true,
420 |         }),
421 |       };
422 | 
423 |       const result = await long_pressLogic(
424 |         {
425 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
426 |           x: 100,
427 |           y: 200,
428 |           duration: 1500,
429 |         },
430 |         mockExecutor,
431 |         mockAxeHelpers,
432 |       );
433 | 
434 |       expect(result).toEqual({
435 |         content: [
436 |           {
437 |             type: 'text',
438 |             text: expect.stringContaining(
439 |               'Error: System error executing axe: Failed to execute axe command: ENOENT: no such file or directory',
440 |             ),
441 |           },
442 |         ],
443 |         isError: true,
444 |       });
445 |     });
446 | 
447 |     it('should handle unexpected Error objects', async () => {
448 |       const mockExecutor = () => {
449 |         throw new Error('Unexpected error');
450 |       };
451 | 
452 |       const mockAxeHelpers = {
453 |         getAxePath: () => '/usr/local/bin/axe',
454 |         getBundledAxeEnvironment: () => ({}),
455 |         createAxeNotAvailableResponse: () => ({
456 |           content: [{ type: 'text', text: 'Mock axe not available' }],
457 |           isError: true,
458 |         }),
459 |       };
460 | 
461 |       const result = await long_pressLogic(
462 |         {
463 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
464 |           x: 100,
465 |           y: 200,
466 |           duration: 1500,
467 |         },
468 |         mockExecutor,
469 |         mockAxeHelpers,
470 |       );
471 | 
472 |       expect(result).toEqual({
473 |         content: [
474 |           {
475 |             type: 'text',
476 |             text: expect.stringContaining(
477 |               'Error: System error executing axe: Failed to execute axe command: Unexpected error',
478 |             ),
479 |           },
480 |         ],
481 |         isError: true,
482 |       });
483 |     });
484 | 
485 |     it('should handle unexpected string errors', async () => {
486 |       const mockExecutor = () => {
487 |         throw 'String error';
488 |       };
489 | 
490 |       const mockAxeHelpers = {
491 |         getAxePath: () => '/usr/local/bin/axe',
492 |         getBundledAxeEnvironment: () => ({}),
493 |         createAxeNotAvailableResponse: () => ({
494 |           content: [{ type: 'text', text: 'Mock axe not available' }],
495 |           isError: true,
496 |         }),
497 |       };
498 | 
499 |       const result = await long_pressLogic(
500 |         {
501 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
502 |           x: 100,
503 |           y: 200,
504 |           duration: 1500,
505 |         },
506 |         mockExecutor,
507 |         mockAxeHelpers,
508 |       );
509 | 
510 |       expect(result).toEqual({
511 |         content: [
512 |           {
513 |             type: 'text',
514 |             text: 'Error: System error executing axe: Failed to execute axe command: String error',
515 |           },
516 |         ],
517 |         isError: true,
518 |       });
519 |     });
520 |   });
521 | });
522 | 
```

--------------------------------------------------------------------------------
/docs/RELOADEROO.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Reloaderoo Integration Guide
  2 | 
  3 | This guide explains how to use Reloaderoo v1.1.2+ for testing and developing XcodeBuildMCP with both CLI inspection tools and transparent proxy capabilities.
  4 | 
  5 | ## Overview
  6 | 
  7 | **Reloaderoo** is a dual-mode MCP development tool that operates as both a CLI inspection tool and a transparent proxy server for the Model Context Protocol (MCP). It provides two distinct operational modes for different development workflows.
  8 | 
  9 | ## Installation
 10 | 
 11 | Reloaderoo is available via npm and can be used with npx for universal compatibility.
 12 | 
 13 | ```bash
 14 | # Use npx to run reloaderoo (works on any system)
 15 | npx reloaderoo@latest --help
 16 | 
 17 | # Or install globally if preferred
 18 | npm install -g reloaderoo
 19 | reloaderoo --help
 20 | ```
 21 | 
 22 | ## Two Operational Modes
 23 | 
 24 | ### 🔍 **CLI Mode** (Inspection & Testing)
 25 | 
 26 | Direct command-line access to MCP servers without client setup - perfect for testing and debugging:
 27 | 
 28 | **Key Benefits:**
 29 | - ✅ **One-shot commands** - Test tools, list resources, get server info
 30 | - ✅ **No MCP client required** - Perfect for testing and debugging
 31 | - ✅ **Raw JSON output** - Ideal for scripts and automation  
 32 | - ✅ **8 inspection commands** - Complete MCP protocol coverage
 33 | - ✅ **AI agent friendly** - Designed for terminal-based AI development workflows
 34 | 
 35 | **Basic Commands:**
 36 | 
 37 | ```bash
 38 | # List all available tools
 39 | npx reloaderoo@latest inspect list-tools -- node build/index.js
 40 | 
 41 | # Call any tool with parameters  
 42 | npx reloaderoo@latest inspect call-tool <tool_name> --params '<json>' -- node build/index.js
 43 | 
 44 | # Get server information
 45 | npx reloaderoo@latest inspect server-info -- node build/index.js
 46 | 
 47 | # List available resources
 48 | npx reloaderoo@latest inspect list-resources -- node build/index.js
 49 | 
 50 | # Read a specific resource
 51 | npx reloaderoo@latest inspect read-resource "<uri>" -- node build/index.js
 52 | 
 53 | # List available prompts
 54 | npx reloaderoo@latest inspect list-prompts -- node build/index.js
 55 | 
 56 | # Get a specific prompt
 57 | npx reloaderoo@latest inspect get-prompt <name> --args '<json>' -- node build/index.js
 58 | 
 59 | # Check server connectivity
 60 | npx reloaderoo@latest inspect ping -- node build/index.js
 61 | ```
 62 | 
 63 | **Example Tool Calls:**
 64 | 
 65 | ```bash
 66 | # List connected devices
 67 | npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/index.js
 68 | 
 69 | # Get doctor information
 70 | npx reloaderoo@latest inspect call-tool doctor --params '{}' -- node build/index.js
 71 | 
 72 | # List iOS simulators
 73 | npx reloaderoo@latest inspect call-tool list_sims --params '{}' -- node build/index.js
 74 | 
 75 | # Read devices resource
 76 | npx reloaderoo@latest inspect read-resource "xcodebuildmcp://devices" -- node build/index.js
 77 | ```
 78 | 
 79 | ### 🔄 **Proxy Mode** (Hot-Reload Development)
 80 | 
 81 | Transparent MCP proxy server that enables seamless hot-reloading during development:
 82 | 
 83 | **Key Benefits:**
 84 | - ✅ **Hot-reload MCP servers** without disconnecting your AI client
 85 | - ✅ **Session persistence** - Keep your development context intact
 86 | - ✅ **Automatic `restart_server` tool** - AI agents can restart servers on demand
 87 | - ✅ **Transparent forwarding** - Full MCP protocol passthrough
 88 | - ✅ **Process management** - Spawns, monitors, and restarts your server process
 89 | 
 90 | **Usage:**
 91 | 
 92 | ```bash
 93 | # Start proxy mode (your AI client connects to this)
 94 | npx reloaderoo@latest proxy -- node build/index.js
 95 | 
 96 | # With debug logging
 97 | npx reloaderoo@latest proxy --log-level debug -- node build/index.js
 98 | 
 99 | # Then in your AI session, request:
100 | # "Please restart the MCP server to load my latest changes"
101 | ```
102 | 
103 | The AI agent will automatically call the `restart_server` tool, preserving your session while reloading code changes.
104 | 
105 | ## MCP Inspection Server Mode
106 | 
107 | Start CLI mode as a persistent MCP server for interactive debugging through MCP clients:
108 | 
109 | ```bash
110 | # Start reloaderoo in CLI mode as an MCP server
111 | npx reloaderoo@latest inspect mcp -- node build/index.js
112 | ```
113 | 
114 | This runs CLI mode as a persistent MCP server, exposing 8 debug tools through the MCP protocol:
115 | - `list_tools` - List all server tools
116 | - `call_tool` - Call any server tool
117 | - `list_resources` - List all server resources  
118 | - `read_resource` - Read any server resource
119 | - `list_prompts` - List all server prompts
120 | - `get_prompt` - Get any server prompt
121 | - `get_server_info` - Get comprehensive server information
122 | - `ping` - Test server connectivity
123 | 
124 | ## Claude Code Compatibility
125 | 
126 | When running under Claude Code, XcodeBuildMCP automatically detects the environment and consolidates multiple content blocks into single responses with `---` separators.
127 | 
128 | **Automatic Detection Methods:**
129 | 1. **Environment Variables**: `CLAUDECODE=1` or `CLAUDE_CODE_ENTRYPOINT=cli`
130 | 2. **Parent Process Analysis**: Checks if parent process contains 'claude'
131 | 3. **Graceful Fallback**: Falls back to environment variables if process detection fails
132 | 
133 | **No Configuration Required**: The consolidation happens automatically when Claude Code is detected.
134 | 
135 | ## Command Reference
136 | 
137 | ### Command Structure
138 | 
139 | ```bash
140 | npx reloaderoo@latest [options] [command]
141 | 
142 | Two modes, one tool:
143 | • Proxy MCP server that adds support for hot-reloading MCP servers.
144 | • CLI tool for inspecting MCP servers.
145 | 
146 | Global Options:
147 |   -V, --version    Output the version number
148 |   -h, --help       Display help for command
149 | 
150 | Commands:
151 |   proxy [options]  🔄 Run as MCP proxy server (default behavior)
152 |   inspect          🔍 Inspect and debug MCP servers
153 |   info [options]   📊 Display version and configuration information
154 |   help [command]   ❓ Display help for command
155 | ```
156 | 
157 | ### 🔄 **Proxy Mode Commands**
158 | 
159 | ```bash
160 | npx reloaderoo@latest proxy [options] -- <child-command> [child-args...]
161 | 
162 | Options:
163 |   -w, --working-dir <directory>    Working directory for the child process
164 |   -l, --log-level <level>          Log level (debug, info, notice, warning, error, critical)
165 |   -f, --log-file <path>            Custom log file path (logs to stderr by default)
166 |   -t, --restart-timeout <ms>       Timeout for restart operations (default: 30000ms)
167 |   -m, --max-restarts <number>      Maximum restart attempts (0-10, default: 3)
168 |   -d, --restart-delay <ms>         Delay between restart attempts (default: 1000ms)
169 |   -q, --quiet                      Suppress non-essential output
170 |   --no-auto-restart                Disable automatic restart on crashes
171 |   --debug                          Enable debug mode with verbose logging
172 |   --dry-run                        Validate configuration without starting proxy
173 | 
174 | Examples:
175 |   npx reloaderoo proxy -- node build/index.js
176 |   npx reloaderoo -- node build/index.js                    # Same as above (proxy is default)
177 |   npx reloaderoo proxy --log-level debug -- node build/index.js
178 | ```
179 | 
180 | ### 🔍 **CLI Mode Commands**
181 | 
182 | ```bash
183 | npx reloaderoo@latest inspect [subcommand] [options] -- <child-command> [child-args...]
184 | 
185 | Subcommands:
186 |   server-info [options]            Get server information and capabilities
187 |   list-tools [options]             List all available tools
188 |   call-tool [options] <name>       Call a specific tool
189 |   list-resources [options]         List all available resources
190 |   read-resource [options] <uri>    Read a specific resource
191 |   list-prompts [options]           List all available prompts
192 |   get-prompt [options] <name>      Get a specific prompt
193 |   ping [options]                   Check server connectivity
194 | 
195 | Examples:
196 |   npx reloaderoo@latest inspect list-tools -- node build/index.js
197 |   npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/index.js
198 |   npx reloaderoo@latest inspect server-info -- node build/index.js
199 | ```
200 | 
201 | ### **Info Command**
202 | 
203 | ```bash
204 | npx reloaderoo@latest info [options]
205 | 
206 | Options:
207 |   -v, --verbose                    Show detailed information
208 |   -h, --help                       Display help for command
209 |   
210 | Examples:
211 |   npx reloaderoo@latest info              # Show basic system information
212 |   npx reloaderoo@latest info --verbose    # Show detailed system information
213 | ```
214 | 
215 | ### Response Format
216 | 
217 | All CLI commands return structured JSON:
218 | 
219 | ```json
220 | {
221 |   "success": true,
222 |   "data": {
223 |     // Command-specific response data
224 |   },
225 |   "metadata": {
226 |     "command": "call-tool:list_devices",
227 |     "timestamp": "2025-07-25T08:32:47.042Z",
228 |     "duration": 1782
229 |   }
230 | }
231 | ```
232 | 
233 | ### Error Handling
234 | 
235 | When commands fail, you'll receive:
236 | 
237 | ```json
238 | {
239 |   "success": false,
240 |   "error": {
241 |     "message": "Error description",
242 |     "code": "ERROR_CODE"
243 |   },
244 |   "metadata": {
245 |     "command": "failed-command",
246 |     "timestamp": "2025-07-25T08:32:47.042Z",
247 |     "duration": 100
248 |   }
249 | }
250 | ```
251 | 
252 | ## Development Workflow
253 | 
254 | ### 🔍 **CLI Mode Workflow** (Testing & Debugging)
255 | 
256 | Perfect for testing individual tools or debugging server issues without MCP client setup:
257 | 
258 | ```bash
259 | # 1. Build XcodeBuildMCP
260 | npm run build
261 | 
262 | # 2. Test your server quickly
263 | npx reloaderoo@latest inspect list-tools -- node build/index.js
264 | 
265 | # 3. Call specific tools to verify behavior
266 | npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/index.js
267 | 
268 | # 4. Check server health and resources
269 | npx reloaderoo@latest inspect ping -- node build/index.js
270 | npx reloaderoo@latest inspect list-resources -- node build/index.js
271 | ```
272 | 
273 | ### 🔄 **Proxy Mode Workflow** (Hot-Reload Development)
274 | 
275 | For full development sessions with AI clients that need persistent connections:
276 | 
277 | #### 1. **Start Development Session**
278 | Configure your AI client to connect to reloaderoo proxy instead of your server directly:
279 | ```bash
280 | npx reloaderoo@latest proxy -- node build/index.js
281 | # or with debug logging:
282 | npx reloaderoo@latest proxy --log-level debug -- node build/index.js
283 | ```
284 | 
285 | #### 2. **Develop Your MCP Server**
286 | Work on your XcodeBuildMCP code as usual - make changes, add tools, modify functionality.
287 | 
288 | #### 3. **Test Changes Instantly**
289 | ```bash
290 | # Rebuild your changes
291 | npm run build
292 | 
293 | # Then ask your AI agent to restart the server:
294 | # "Please restart the MCP server to load my latest changes"
295 | ```
296 | 
297 | The agent will call the `restart_server` tool automatically. Your new capabilities are immediately available!
298 | 
299 | #### 4. **Continue Development**
300 | Your AI session continues with the updated server capabilities. No connection loss, no context reset.
301 | 
302 | ### 🛠️ **MCP Inspection Server** (Interactive CLI Debugging)
303 | 
304 | For interactive debugging through MCP clients:
305 | 
306 | ```bash
307 | # Start reloaderoo CLI mode as an MCP server
308 | npx reloaderoo@latest inspect mcp -- node build/index.js
309 | 
310 | # Then connect with an MCP client to access debug tools
311 | # Available tools: list_tools, call_tool, list_resources, etc.
312 | ```
313 | 
314 | ## Troubleshooting
315 | 
316 | ### 🔄 **Proxy Mode Issues**
317 | 
318 | **Server won't start in proxy mode:**
319 | ```bash
320 | # Check if XcodeBuildMCP runs independently first
321 | node build/index.js
322 | 
323 | # Then try with reloaderoo proxy to validate configuration
324 | npx reloaderoo@latest proxy -- node build/index.js
325 | ```
326 | 
327 | **Connection problems with MCP clients:**
328 | ```bash
329 | # Enable debug logging to see what's happening
330 | npx reloaderoo@latest proxy --log-level debug -- node build/index.js
331 | 
332 | # Check system info and configuration
333 | npx reloaderoo@latest info --verbose
334 | ```
335 | 
336 | **Restart failures in proxy mode:**
337 | ```bash
338 | # Increase restart timeout
339 | npx reloaderoo@latest proxy --restart-timeout 60000 -- node build/index.js
340 | 
341 | # Check restart limits  
342 | npx reloaderoo@latest proxy --max-restarts 5 -- node build/index.js
343 | ```
344 | 
345 | ### 🔍 **CLI Mode Issues**
346 | 
347 | **CLI commands failing:**
348 | ```bash
349 | # Test basic connectivity first
350 | npx reloaderoo@latest inspect ping -- node build/index.js
351 | 
352 | # Enable debug logging for CLI commands (via proxy debug mode)
353 | npx reloaderoo@latest proxy --log-level debug -- node build/index.js
354 | ```
355 | 
356 | **JSON parsing errors:**
357 | ```bash
358 | # Check server information for troubleshooting
359 | npx reloaderoo@latest inspect server-info -- node build/index.js
360 | 
361 | # Ensure your server outputs valid JSON
362 | node build/index.js | head -10
363 | ```
364 | 
365 | ### **General Issues**
366 | 
367 | **Command not found:**
368 | ```bash
369 | # Ensure npx can find reloaderoo
370 | npx reloaderoo@latest --help
371 | 
372 | # If that fails, try installing globally
373 | npm install -g reloaderoo
374 | ```
375 | 
376 | **Parameter validation:**
377 | ```bash
378 | # Ensure JSON parameters are properly quoted
379 | npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/index.js
380 | ```
381 | 
382 | ### **General Debug Mode**
383 | 
384 | ```bash
385 | # Get detailed information about what's happening
386 | npx reloaderoo@latest proxy --debug -- node build/index.js  # For proxy mode
387 | npx reloaderoo@latest proxy --log-level debug -- node build/index.js  # For detailed proxy logging
388 | 
389 | # View system information
390 | npx reloaderoo@latest info --verbose
391 | ```
392 | 
393 | ### Debug Tips
394 | 
395 | 1. **Always build first**: Run `npm run build` before testing
396 | 2. **Check tool names**: Use `inspect list-tools` to see exact tool names
397 | 3. **Validate JSON**: Ensure parameters are valid JSON strings
398 | 4. **Enable debug logging**: Use `--log-level debug` or `--debug` for verbose output
399 | 5. **Test connectivity**: Use `inspect ping` to verify server communication
400 | 
401 | ## Advanced Usage
402 | 
403 | ### Environment Variables
404 | 
405 | Configure reloaderoo behavior via environment variables:
406 | 
407 | ```bash
408 | # Logging Configuration
409 | export MCPDEV_PROXY_LOG_LEVEL=debug           # Log level (debug, info, notice, warning, error, critical)
410 | export MCPDEV_PROXY_LOG_FILE=/path/to/log     # Custom log file path (default: stderr)
411 | export MCPDEV_PROXY_DEBUG_MODE=true           # Enable debug mode (true/false)
412 | 
413 | # Process Management
414 | export MCPDEV_PROXY_RESTART_LIMIT=5           # Maximum restart attempts (0-10, default: 3)
415 | export MCPDEV_PROXY_AUTO_RESTART=true         # Enable/disable auto-restart (true/false)
416 | export MCPDEV_PROXY_TIMEOUT=30000             # Operation timeout in milliseconds
417 | export MCPDEV_PROXY_RESTART_DELAY=1000        # Delay between restart attempts in milliseconds
418 | export MCPDEV_PROXY_CWD=/path/to/directory     # Default working directory
419 | ```
420 | 
421 | ### Custom Working Directory
422 | 
423 | ```bash
424 | npx reloaderoo@latest proxy --working-dir /custom/path -- node build/index.js
425 | npx reloaderoo@latest inspect list-tools --working-dir /custom/path -- node build/index.js
426 | ```
427 | 
428 | ### Timeout Configuration
429 | 
430 | ```bash
431 | npx reloaderoo@latest proxy --restart-timeout 60000 -- node build/index.js
432 | ```
433 | 
434 | ## Integration with XcodeBuildMCP
435 | 
436 | Reloaderoo is specifically configured to work with XcodeBuildMCP's:
437 | 
438 | - **84+ Tools**: All workflow groups accessible via CLI
439 | - **4 Resources**: Direct access to devices, simulators, environment, swift-packages
440 | - **Dynamic Tool Discovery**: Compatible with `discover_tools` functionality
441 | - **Claude Code Detection**: Automatic consolidation of multiple content blocks
442 | - **Hot-Reload Support**: Seamless development workflow with `restart_server`
443 | 
444 | For more information about XcodeBuildMCP's architecture and capabilities, see:
445 | - [Architecture Guide](ARCHITECTURE.md)
446 | - [Plugin Development Guide](PLUGIN_DEVELOPMENT.md)
447 | - [Testing Guide](TESTING.md)
```

--------------------------------------------------------------------------------
/example_projects/iOS_Calculator/CalculatorAppPackage/Tests/CalculatorAppFeatureTests/CalculatorServiceTests.swift:
--------------------------------------------------------------------------------

```swift
  1 | import Testing
  2 | import Foundation
  3 | @testable import CalculatorAppFeature
  4 | 
  5 | // MARK: - Calculator Basic Tests
  6 | @Suite("Calculator Basic Functionality")
  7 | struct CalculatorBasicTests {
  8 |     
  9 |     @Test("Calculator initializes with correct default values")
 10 |     func testInitialState() {
 11 |         let calculator = CalculatorService()
 12 |         #expect(calculator.display == "0")
 13 |         #expect(calculator.currentValue == 0)
 14 |         #expect(calculator.previousValue == 0)
 15 |         #expect(calculator.currentOperation == nil)
 16 |         #expect(calculator.willResetDisplay == false)
 17 |     }
 18 |     
 19 |     @Test("Clear function resets calculator to initial state")
 20 |     func testClear() {
 21 |         let calculator = CalculatorService()
 22 |         calculator.inputNumber("5")
 23 |         calculator.setOperation(.add)
 24 |         calculator.inputNumber("3")
 25 |         
 26 |         calculator.clear()
 27 |         
 28 |         #expect(calculator.display == "0")
 29 |         #expect(calculator.currentValue == 0)
 30 |         #expect(calculator.previousValue == 0)
 31 |     }
 32 |     
 33 |     @Test("This test should fail to verify error reporting")
 34 |     func testIntentionalFailure() {
 35 |         let calculator = CalculatorService()
 36 |         // This test is designed to fail to test error reporting
 37 |         #expect(calculator.display == "999", "This should fail - display should be 0, not 999")
 38 |         #expect(calculator.currentOperation == nil)
 39 |         #expect(calculator.willResetDisplay == false)
 40 |     }
 41 | }
 42 | 
 43 | // MARK: - Number Input Tests
 44 | @Suite("Number Input")
 45 | struct NumberInputTests {
 46 |     
 47 |     @Test("Adding single digit numbers")
 48 |     func testSingleDigitInput() {
 49 |         let calculator = CalculatorService()
 50 |         
 51 |         calculator.inputNumber("5")
 52 |         #expect(calculator.display == "5")
 53 |         #expect(calculator.currentValue == 5)
 54 |     }
 55 |     
 56 |     @Test("Adding multiple digit numbers")
 57 |     func testMultipleDigitInput() {
 58 |         let calculator = CalculatorService()
 59 |         
 60 |         calculator.inputNumber("1")
 61 |         calculator.inputNumber("2")
 62 |         calculator.inputNumber("3")
 63 |         
 64 |         #expect(calculator.display == "123")
 65 |         #expect(calculator.currentValue == 123)
 66 |     }
 67 |     
 68 |     @Test("Adding decimal numbers")
 69 |     func testDecimalInput() {
 70 |         let calculator = CalculatorService()
 71 |         
 72 |         calculator.inputNumber("1")
 73 |         calculator.inputDecimal()
 74 |         calculator.inputNumber("5")
 75 |         
 76 |         #expect(calculator.display == "1.5")
 77 |         #expect(calculator.currentValue == 1.5)
 78 |     }
 79 |     
 80 |     @Test("Multiple decimal points should be ignored")
 81 |     func testMultipleDecimalPoints() {
 82 |         let calculator = CalculatorService()
 83 |         
 84 |         calculator.inputNumber("1")
 85 |         calculator.inputDecimal()
 86 |         calculator.inputNumber("5")
 87 |         calculator.inputDecimal() // This should be ignored
 88 |         calculator.inputNumber("2")
 89 |         
 90 |         #expect(calculator.display == "1.52")
 91 |         #expect(calculator.currentValue == 1.52)
 92 |     }
 93 |     
 94 |     @Test("Decimal point at start creates 0.")
 95 |     func testDecimalAtStart() {
 96 |         let calculator = CalculatorService()
 97 |         
 98 |         calculator.inputDecimal()
 99 |         calculator.inputNumber("5")
100 |         
101 |         #expect(calculator.display == "0.5")
102 |         #expect(calculator.currentValue == 0.5)
103 |     }
104 | }
105 | 
106 | // MARK: - Operation Tests
107 | @Suite("Mathematical Operations")
108 | struct OperationTests {
109 |     
110 |     @Test("Addition operation", arguments: [
111 |         (5.0, 3.0, 8.0),
112 |         (10.0, -2.0, 8.0),
113 |         (0.0, 5.0, 5.0),
114 |         (-3.0, -7.0, -10.0)
115 |     ])
116 |     func testAddition(a: Double, b: Double, expected: Double) {
117 |         let result = CalculatorService.Operation.add.calculate(a, b)
118 |         #expect(result == expected)
119 |     }
120 |     
121 |     @Test("Subtraction operation", arguments: [
122 |         (10.0, 3.0, 7.0),
123 |         (5.0, 8.0, -3.0),
124 |         (0.0, 5.0, -5.0),
125 |         (-3.0, -7.0, 4.0)
126 |     ])
127 |     func testSubtraction(a: Double, b: Double, expected: Double) {
128 |         let result = CalculatorService.Operation.subtract.calculate(a, b)
129 |         #expect(result == expected)
130 |     }
131 |     
132 |     @Test("Multiplication operation", arguments: [
133 |         (5.0, 3.0, 15.0),
134 |         (4.0, -2.0, -8.0),
135 |         (0.0, 5.0, 0.0),
136 |         (-3.0, -7.0, 21.0)
137 |     ])
138 |     func testMultiplication(a: Double, b: Double, expected: Double) {
139 |         let result = CalculatorService.Operation.multiply.calculate(a, b)
140 |         #expect(result == expected)
141 |     }
142 |     
143 |     @Test("Division operation", arguments: [
144 |         (10.0, 2.0, 5.0),
145 |         (15.0, 3.0, 5.0),
146 |         (-8.0, 2.0, -4.0),
147 |         (7.0, 2.0, 3.5)
148 |     ])
149 |     func testDivision(a: Double, b: Double, expected: Double) {
150 |         let result = CalculatorService.Operation.divide.calculate(a, b)
151 |         #expect(result == expected)
152 |     }
153 |     
154 |     @Test("Division by zero returns zero")
155 |     func testDivisionByZero() {
156 |         let result = CalculatorService.Operation.divide.calculate(10.0, 0.0)
157 |         #expect(result == 0.0)
158 |     }
159 | }
160 | 
161 | // MARK: - Calculator Integration Tests
162 | @Suite("Calculator Integration Tests")
163 | struct CalculatorIntegrationTests {
164 |     
165 |     @Test("Simple addition calculation")
166 |     func testSimpleAddition() {
167 |         let calculator = CalculatorService()
168 |         
169 |         calculator.inputNumber("5")
170 |         calculator.setOperation(.add)
171 |         calculator.inputNumber("3")
172 |         calculator.calculate()
173 |         
174 |         #expect(calculator.display == "8")
175 |         #expect(calculator.currentValue == 8)
176 |     }
177 |     
178 |     @Test("Chain calculations")
179 |     func testChainCalculations() {
180 |         let calculator = CalculatorService()
181 |         
182 |         calculator.inputNumber("5")
183 |         calculator.setOperation(.add)
184 |         calculator.inputNumber("3")
185 |         calculator.setOperation(.multiply) // Should calculate 5+3=8 first
186 |         calculator.inputNumber("2")
187 |         calculator.calculate()
188 |         
189 |         #expect(calculator.currentValue == 16) // (5+3) * 2 = 16
190 |     }
191 |     
192 |     @Test("Complex calculation sequence")
193 |     func testComplexCalculation() {
194 |         let calculator = CalculatorService()
195 |         
196 |         // Calculate: 10 + 5 * 2 - 3
197 |         calculator.inputNumber("1")
198 |         calculator.inputNumber("0")
199 |         calculator.setOperation(.add)
200 |         calculator.inputNumber("5")
201 |         calculator.setOperation(.multiply)
202 |         calculator.inputNumber("2")
203 |         calculator.setOperation(.subtract)
204 |         calculator.inputNumber("3")
205 |         calculator.calculate()
206 |         
207 |         #expect(calculator.currentValue == 27) // ((10+5)*2)-3 = 27
208 |     }
209 | 
210 |     @Test("Repetitive equals press repeats last operation")
211 |     func testRepetitiveEquals() {
212 |         let calculator = CalculatorService()
213 | 
214 |         calculator.inputNumber("5")
215 |         calculator.setOperation(.add)
216 |         calculator.inputNumber("3")
217 |         calculator.calculate() // 5 + 3 = 8
218 | 
219 |         #expect(calculator.currentValue == 8)
220 | 
221 |         calculator.calculate() // Should be 8 + 3 = 11
222 |         #expect(calculator.currentValue == 11)
223 | 
224 |         calculator.calculate() // Should be 11 + 3 = 14
225 |         #expect(calculator.currentValue == 14)
226 |     }
227 | 
228 |     @Test("Expression display updates correctly")
229 |     func testExpressionDisplay() {
230 |         let calculator = CalculatorService()
231 | 
232 |         calculator.inputNumber("1")
233 |         calculator.inputNumber("2")
234 |         #expect(calculator.expressionDisplay == "")
235 | 
236 |         calculator.setOperation(.add)
237 |         #expect(calculator.expressionDisplay == "12 +")
238 | 
239 |         calculator.inputNumber("3")
240 |         #expect(calculator.expressionDisplay == "12 +") 
241 | 
242 |         calculator.calculate()
243 |         #expect(calculator.expressionDisplay == "12 + 3 =")
244 |     }
245 | }
246 | 
247 | // MARK: - Special Functions Tests
248 | @Suite("Special Functions")
249 | struct SpecialFunctionsTests {
250 |     
251 |     @Test("Toggle sign on positive number")
252 |     func testToggleSignPositive() {
253 |         let calculator = CalculatorService()
254 |         
255 |         calculator.inputNumber("5")
256 |         calculator.toggleSign()
257 |         
258 |         #expect(calculator.display == "-5")
259 |         #expect(calculator.currentValue == -5)
260 |     }
261 |     
262 |     @Test("Toggle sign on negative number")
263 |     func testToggleSignNegative() {
264 |         let calculator = CalculatorService()
265 |         
266 |         calculator.inputNumber("5")
267 |         calculator.toggleSign()
268 |         calculator.toggleSign()
269 |         
270 |         #expect(calculator.display == "5")
271 |         #expect(calculator.currentValue == 5)
272 |     }
273 |     
274 |     @Test("Toggle sign on zero has no effect")
275 |     func testToggleSignZero() {
276 |         let calculator = CalculatorService()
277 |         
278 |         calculator.toggleSign()
279 |         
280 |         #expect(calculator.display == "0")
281 |         #expect(calculator.currentValue == 0)
282 |     }
283 |     
284 |     @Test("Percentage calculation", arguments: [
285 |         ("100", 1.0),
286 |         ("50", 0.5),
287 |         ("25", 0.25),
288 |         ("200", 2.0)
289 |     ])
290 |     func testPercentage(input: String, expected: Double) {
291 |         let calculator = CalculatorService()
292 |         
293 |         calculator.inputNumber(input)
294 |         calculator.percentage()
295 |         
296 |         #expect(calculator.currentValue == expected)
297 |     }
298 | }
299 | 
300 | // MARK: - Input Handler Tests
301 | @Suite("Input Handler Integration")
302 | struct InputHandlerTests {
303 |     
304 |     @Test("Number input through handler")
305 |     func testNumberInputThroughHandler() {
306 |         let calculator = CalculatorService()
307 |         let handler = CalculatorInputHandler(service: calculator)
308 |         
309 |         handler.handleInput("1")
310 |         handler.handleInput("2")
311 |         handler.handleInput("3")
312 |         
313 |         #expect(calculator.display == "123")
314 |     }
315 |     
316 |     @Test("Operation input through handler")
317 |     func testOperationInputThroughHandler() {
318 |         let calculator = CalculatorService()
319 |         let handler = CalculatorInputHandler(service: calculator)
320 |         
321 |         handler.handleInput("5")
322 |         handler.handleInput("+")
323 |         handler.handleInput("3")
324 |         handler.handleInput("=")
325 |         
326 |         #expect(calculator.currentValue == 8)
327 |     }
328 |     
329 |     @Test("Clear input through handler")
330 |     func testClearInputThroughHandler() {
331 |         let calculator = CalculatorService()
332 |         let handler = CalculatorInputHandler(service: calculator)
333 |         
334 |         handler.handleInput("5")
335 |         handler.handleInput("+")
336 |         handler.handleInput("3")
337 |         handler.handleInput("C")
338 |         
339 |         #expect(calculator.display == "0")
340 |         #expect(calculator.currentValue == 0)
341 |     }
342 |     
343 |     @Test("Decimal input through handler")
344 |     func testDecimalInputThroughHandler() {
345 |         let calculator = CalculatorService()
346 |         let handler = CalculatorInputHandler(service: calculator)
347 |         
348 |         handler.handleInput("1")
349 |         handler.handleInput(".")
350 |         handler.handleInput("5")
351 |         
352 |         #expect(calculator.display == "1.5")
353 |     }
354 | }
355 | 
356 | // MARK: - Edge Cases Tests
357 | @Suite("Edge Cases")
358 | struct EdgeCaseTests {
359 |     
360 |     @Test("Calculate without setting operation")
361 |     func testCalculateWithoutOperation() {
362 |         let calculator = CalculatorService()
363 |         
364 |         calculator.inputNumber("5")
365 |         calculator.calculate()
366 |         
367 |         #expect(calculator.currentValue == 5) // Should remain unchanged
368 |     }
369 |     
370 |     @Test("Setting operation without previous number")
371 |     func testOperationWithoutPreviousNumber() {
372 |         let calculator = CalculatorService()
373 |         
374 |         calculator.setOperation(.add)
375 |         calculator.inputNumber("5")
376 |         calculator.calculate()
377 |         
378 |         #expect(calculator.currentValue == 5) // 0 + 5 = 5
379 |     }
380 |     
381 |     @Test("Multiple equals presses")
382 |     func testMultipleEquals() {
383 |         let calculator = CalculatorService()
384 |         
385 |         calculator.inputNumber("5")
386 |         calculator.setOperation(.add)
387 |         calculator.inputNumber("3")
388 |         calculator.calculate()
389 |         
390 |         let firstResult = calculator.currentValue
391 |         calculator.calculate() // Second equals press
392 |         
393 |         #expect(firstResult == 8)
394 |         #expect(calculator.currentValue == 11) // Should repeat last operation: 8 + 3 = 11
395 |     }
396 | }
397 | 
398 | // MARK: - Error Handling Tests
399 | @Suite("Error Handling")
400 | struct ErrorHandlingTests {
401 |     
402 |     @Test("Calculator handles invalid input gracefully")
403 |     func testInvalidInputHandling() {
404 |         let calculator = CalculatorService()
405 |         let handler = CalculatorInputHandler(service: calculator)
406 |         
407 |         // Test pressing operation without any number
408 |         handler.handleInput("+")
409 |         handler.handleInput("5")
410 |         handler.handleInput("=")
411 |         
412 |         #expect(calculator.currentValue == 5) // Should be 0 + 5 = 5
413 |     }
414 |     
415 |     @Test("Calculator state after multiple clears")
416 |     func testMultipleClearOperations() {
417 |         let calculator = CalculatorService()
418 |         
419 |         calculator.inputNumber("123")
420 |         calculator.setOperation(.add)
421 |         calculator.inputNumber("456")
422 |         
423 |         // Multiple clear operations
424 |         calculator.clear()
425 |         calculator.clear()
426 |         calculator.clear()
427 |         
428 |         #expect(calculator.display == "0")
429 |         #expect(calculator.currentValue == 0)
430 |         #expect(calculator.currentOperation == nil)
431 |     }
432 |     
433 |     @Test("Large number error handling")
434 |     func testLargeNumberError() {
435 |         let calculator = CalculatorService()
436 |         calculator.inputNumber("1000000000000") // 1e12
437 |         calculator.setOperation(.multiply)
438 |         calculator.inputNumber("2")
439 |         calculator.calculate()
440 | 
441 |         #expect(calculator.hasError == true)
442 |         #expect(calculator.display == "Error")
443 |         #expect(calculator.expressionDisplay == "Number too large")
444 |     }
445 | }
446 | 
447 | // MARK: - Decimal Edge Cases
448 | @Suite("Decimal Edge Cases")
449 | struct DecimalEdgeCaseTests {
450 |     
451 |     @Test("Very small decimal numbers")
452 |     func testVerySmallDecimals() {
453 |         let calculator = CalculatorService()
454 |         
455 |         calculator.inputNumber("0")
456 |         calculator.inputDecimal()
457 |         calculator.inputNumber("0")
458 |         calculator.inputNumber("0")
459 |         calculator.inputNumber("1")
460 |         
461 |         #expect(calculator.display == "0.001")
462 |         #expect(calculator.currentValue == 0.001)
463 |     }
464 |     
465 |     @Test("Decimal operations precision")
466 |     func testDecimalPrecision() {
467 |         let calculator = CalculatorService()
468 |         
469 |         calculator.inputNumber("0")
470 |         calculator.inputDecimal()
471 |         calculator.inputNumber("1")
472 |         calculator.setOperation(.add)
473 |         calculator.inputNumber("0")
474 |         calculator.inputDecimal()
475 |         calculator.inputNumber("2")
476 |         calculator.calculate()
477 |         
478 |         // 0.1 + 0.2 should equal 0.3 (within floating point precision)
479 |         #expect(abs(calculator.currentValue - 0.3) < 0.0001)
480 |     }
481 | }
482 | 
```

--------------------------------------------------------------------------------
/scripts/analysis/tools-analysis.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | /**
  4 |  * XcodeBuildMCP Tools Analysis
  5 |  *
  6 |  * Core TypeScript module for analyzing XcodeBuildMCP tools using AST parsing.
  7 |  * Provides reliable extraction of tool information without fallback strategies.
  8 |  */
  9 | 
 10 | import {
 11 |   createSourceFile,
 12 |   forEachChild,
 13 |   isExportAssignment,
 14 |   isIdentifier,
 15 |   isNoSubstitutionTemplateLiteral,
 16 |   isObjectLiteralExpression,
 17 |   isPropertyAssignment,
 18 |   isStringLiteral,
 19 |   isTemplateExpression,
 20 |   isVariableDeclaration,
 21 |   isVariableStatement,
 22 |   type Node,
 23 |   type ObjectLiteralExpression,
 24 |   ScriptTarget,
 25 |   type SourceFile,
 26 |   SyntaxKind,
 27 | } from 'typescript';
 28 | import * as fs from 'fs';
 29 | import * as path from 'path';
 30 | import { glob } from 'glob';
 31 | import { fileURLToPath } from 'url';
 32 | 
 33 | // Get project root
 34 | const __filename = fileURLToPath(import.meta.url);
 35 | const __dirname = path.dirname(__filename);
 36 | const projectRoot = path.resolve(__dirname, '..', '..');
 37 | const toolsDir = path.join(projectRoot, 'src', 'mcp', 'tools');
 38 | 
 39 | export interface ToolInfo {
 40 |   name: string;
 41 |   workflow: string;
 42 |   path: string;
 43 |   relativePath: string;
 44 |   description: string;
 45 |   isCanonical: boolean;
 46 | }
 47 | 
 48 | export interface WorkflowInfo {
 49 |   name: string;
 50 |   displayName: string;
 51 |   description: string;
 52 |   tools: ToolInfo[];
 53 |   toolCount: number;
 54 |   canonicalCount: number;
 55 |   reExportCount: number;
 56 | }
 57 | 
 58 | export interface AnalysisStats {
 59 |   totalTools: number;
 60 |   canonicalTools: number;
 61 |   reExportTools: number;
 62 |   workflowCount: number;
 63 | }
 64 | 
 65 | export interface StaticAnalysisResult {
 66 |   workflows: WorkflowInfo[];
 67 |   tools: ToolInfo[];
 68 |   stats: AnalysisStats;
 69 | }
 70 | 
 71 | /**
 72 |  * Extract the description from a tool's default export using TypeScript AST
 73 |  */
 74 | function extractToolDescription(sourceFile: SourceFile): string {
 75 |   let description: string | null = null;
 76 | 
 77 |   function visit(node: Node): void {
 78 |     let objectExpression: ObjectLiteralExpression | null = null;
 79 | 
 80 |     // Look for export default { ... } - the standard TypeScript pattern
 81 |     // isExportEquals is undefined for `export default` and true for `export = `
 82 |     if (isExportAssignment(node) && !node.isExportEquals) {
 83 |       if (isObjectLiteralExpression(node.expression)) {
 84 |         objectExpression = node.expression;
 85 |       }
 86 |     }
 87 | 
 88 |     if (objectExpression) {
 89 |       // Found export default { ... }, now look for description property
 90 |       for (const property of objectExpression.properties) {
 91 |         if (
 92 |           isPropertyAssignment(property) &&
 93 |           isIdentifier(property.name) &&
 94 |           property.name.text === 'description'
 95 |         ) {
 96 |           // Extract the description value
 97 |           if (isStringLiteral(property.initializer)) {
 98 |             // This is the most common case - simple string literal
 99 |             description = property.initializer.text;
100 |           } else if (
101 |             isTemplateExpression(property.initializer) ||
102 |             isNoSubstitutionTemplateLiteral(property.initializer)
103 |           ) {
104 |             // Handle template literals - get the raw text and clean it
105 |             description = property.initializer.getFullText(sourceFile).trim();
106 |             // Remove surrounding backticks
107 |             if (description.startsWith('`') && description.endsWith('`')) {
108 |               description = description.slice(1, -1);
109 |             }
110 |           } else {
111 |             // Handle any other expression (multiline strings, computed values)
112 |             const fullText = property.initializer.getFullText(sourceFile).trim();
113 |             // This covers cases where the description spans multiple lines
114 |             // Remove surrounding quotes and normalize whitespace
115 |             let cleaned = fullText;
116 |             if (
117 |               (cleaned.startsWith('"') && cleaned.endsWith('"')) ||
118 |               (cleaned.startsWith("'") && cleaned.endsWith("'"))
119 |             ) {
120 |               cleaned = cleaned.slice(1, -1);
121 |             }
122 |             // Collapse multiple whitespaces and newlines into single spaces
123 |             description = cleaned.replace(/\s+/g, ' ').trim();
124 |           }
125 |           return; // Found description, stop looking
126 |         }
127 |       }
128 |     }
129 | 
130 |     forEachChild(node, visit);
131 |   }
132 | 
133 |   visit(sourceFile);
134 | 
135 |   if (description === null) {
136 |     throw new Error('Could not extract description from tool export default object');
137 |   }
138 | 
139 |   return description;
140 | }
141 | 
142 | /**
143 |  * Check if a file is a re-export by examining its content
144 |  */
145 | function isReExportFile(filePath: string): boolean {
146 |   const content = fs.readFileSync(filePath, 'utf-8');
147 | 
148 |   // Remove comments and empty lines, then check for re-export pattern
149 |   // First remove multi-line comments
150 |   const contentWithoutBlockComments = content.replace(/\/\*[\s\S]*?\*\//g, '');
151 | 
152 |   const cleanedLines = contentWithoutBlockComments
153 |     .split('\n')
154 |     .map((line) => {
155 |       // Remove inline comments but preserve the code before them
156 |       const codeBeforeComment = line.split('//')[0].trim();
157 |       return codeBeforeComment;
158 |     })
159 |     .filter((line) => line.length > 0);
160 | 
161 |   // Should have exactly one line: export { default } from '...';
162 |   if (cleanedLines.length !== 1) {
163 |     return false;
164 |   }
165 | 
166 |   const exportLine = cleanedLines[0];
167 |   return /^export\s*{\s*default\s*}\s*from\s*['"][^'"]+['"];?\s*$/.test(exportLine);
168 | }
169 | 
170 | /**
171 |  * Get workflow metadata from index.ts file if it exists
172 |  */
173 | async function getWorkflowMetadata(
174 |   workflowDir: string,
175 | ): Promise<{ displayName: string; description: string } | null> {
176 |   const indexPath = path.join(toolsDir, workflowDir, 'index.ts');
177 | 
178 |   if (!fs.existsSync(indexPath)) {
179 |     return null;
180 |   }
181 | 
182 |   try {
183 |     const content = fs.readFileSync(indexPath, 'utf-8');
184 |     const sourceFile = createSourceFile(indexPath, content, ScriptTarget.Latest, true);
185 | 
186 |     const workflowExport: { name?: string; description?: string } = {};
187 | 
188 |     function visit(node: Node): void {
189 |       // Look for: export const workflow = { ... }
190 |       if (
191 |         isVariableStatement(node) &&
192 |         node.modifiers?.some((mod) => mod.kind === SyntaxKind.ExportKeyword)
193 |       ) {
194 |         for (const declaration of node.declarationList.declarations) {
195 |           if (
196 |             isVariableDeclaration(declaration) &&
197 |             isIdentifier(declaration.name) &&
198 |             declaration.name.text === 'workflow' &&
199 |             declaration.initializer &&
200 |             isObjectLiteralExpression(declaration.initializer)
201 |           ) {
202 |             // Extract name and description properties
203 |             for (const property of declaration.initializer.properties) {
204 |               if (isPropertyAssignment(property) && isIdentifier(property.name)) {
205 |                 const propertyName = property.name.text;
206 | 
207 |                 if (propertyName === 'name' && isStringLiteral(property.initializer)) {
208 |                   workflowExport.name = property.initializer.text;
209 |                 } else if (
210 |                   propertyName === 'description' &&
211 |                   isStringLiteral(property.initializer)
212 |                 ) {
213 |                   workflowExport.description = property.initializer.text;
214 |                 }
215 |               }
216 |             }
217 |           }
218 |         }
219 |       }
220 | 
221 |       forEachChild(node, visit);
222 |     }
223 | 
224 |     visit(sourceFile);
225 | 
226 |     if (workflowExport.name && workflowExport.description) {
227 |       return {
228 |         displayName: workflowExport.name,
229 |         description: workflowExport.description,
230 |       };
231 |     }
232 |   } catch (error) {
233 |     console.error(`Warning: Could not parse workflow metadata from ${indexPath}: ${error}`);
234 |   }
235 | 
236 |   return null;
237 | }
238 | 
239 | /**
240 |  * Get a human-readable workflow name from directory name
241 |  */
242 | function getWorkflowDisplayName(workflowDir: string): string {
243 |   const displayNames: Record<string, string> = {
244 |     device: 'iOS Device Development',
245 |     discovery: 'Dynamic Tool Discovery',
246 |     doctor: 'System Doctor',
247 |     logging: 'Logging & Monitoring',
248 |     macos: 'macOS Development',
249 |     'project-discovery': 'Project Discovery',
250 |     'project-scaffolding': 'Project Scaffolding',
251 |     simulator: 'iOS Simulator Development',
252 |     'simulator-management': 'Simulator Management',
253 |     'swift-package': 'Swift Package Manager',
254 |     'ui-testing': 'UI Testing & Automation',
255 |     utilities: 'Utilities',
256 |   };
257 | 
258 |   return displayNames[workflowDir] || workflowDir;
259 | }
260 | 
261 | /**
262 |  * Get workflow description
263 |  */
264 | function getWorkflowDescription(workflowDir: string): string {
265 |   const descriptions: Record<string, string> = {
266 |     device: 'Physical device development, testing, and deployment',
267 |     discovery: 'Intelligent workflow enablement based on task descriptions',
268 |     doctor: 'System health checks and environment validation',
269 |     logging: 'Log capture and monitoring across platforms',
270 |     macos: 'Native macOS application development and testing',
271 |     'project-discovery': 'Project analysis and information gathering',
272 |     'project-scaffolding': 'Create new projects from templates',
273 |     simulator: 'Simulator-based development, testing, and deployment',
274 |     'simulator-management': 'Simulator environment and configuration management',
275 |     'swift-package': 'Swift Package development and testing',
276 |     'ui-testing': 'Automated UI interaction and testing',
277 |     utilities: 'General utility operations',
278 |   };
279 | 
280 |   return descriptions[workflowDir] || `${workflowDir} related tools`;
281 | }
282 | 
283 | /**
284 |  * Perform static analysis of all tools in the project
285 |  */
286 | export async function getStaticToolAnalysis(): Promise<StaticAnalysisResult> {
287 |   // Find all workflow directories
288 |   const workflowDirs = fs
289 |     .readdirSync(toolsDir, { withFileTypes: true })
290 |     .filter((dirent) => dirent.isDirectory())
291 |     .map((dirent) => dirent.name)
292 |     .sort();
293 | 
294 |   // Find all tool files
295 |   const files = await glob('**/*.ts', {
296 |     cwd: toolsDir,
297 |     ignore: [
298 |       '**/__tests__/**',
299 |       '**/index.ts',
300 |       '**/*.test.ts',
301 |       '**/lib/**',
302 |       '**/*-processes.ts', // Process management utilities
303 |       '**/*.deps.ts', // Dependency files
304 |       '**/*-utils.ts', // Utility files
305 |       '**/*-common.ts', // Common/shared code
306 |       '**/*-types.ts', // Type definition files
307 |     ],
308 |     absolute: true,
309 |   });
310 | 
311 |   const allTools: ToolInfo[] = [];
312 |   const workflowMap = new Map<string, ToolInfo[]>();
313 | 
314 |   let canonicalCount = 0;
315 |   let reExportCount = 0;
316 | 
317 |   // Initialize workflow map
318 |   for (const workflowDir of workflowDirs) {
319 |     workflowMap.set(workflowDir, []);
320 |   }
321 | 
322 |   // Process each tool file
323 |   for (const filePath of files) {
324 |     const toolName = path.basename(filePath, '.ts');
325 |     const workflowDir = path.basename(path.dirname(filePath));
326 |     const relativePath = path.relative(projectRoot, filePath);
327 | 
328 |     const isReExport = isReExportFile(filePath);
329 | 
330 |     let description = '';
331 | 
332 |     if (!isReExport) {
333 |       // Extract description from canonical tool using AST
334 |       try {
335 |         const content = fs.readFileSync(filePath, 'utf-8');
336 |         const sourceFile = createSourceFile(filePath, content, ScriptTarget.Latest, true);
337 | 
338 |         description = extractToolDescription(sourceFile);
339 |         canonicalCount++;
340 |       } catch (error) {
341 |         throw new Error(`Failed to extract description from ${relativePath}: ${error}`);
342 |       }
343 |     } else {
344 |       description = '(Re-exported from shared workflow)';
345 |       reExportCount++;
346 |     }
347 | 
348 |     const toolInfo: ToolInfo = {
349 |       name: toolName,
350 |       workflow: workflowDir,
351 |       path: filePath,
352 |       relativePath,
353 |       description,
354 |       isCanonical: !isReExport,
355 |     };
356 | 
357 |     allTools.push(toolInfo);
358 | 
359 |     const workflowTools = workflowMap.get(workflowDir);
360 |     if (workflowTools) {
361 |       workflowTools.push(toolInfo);
362 |     }
363 |   }
364 | 
365 |   // Build workflow information
366 |   const workflows: WorkflowInfo[] = [];
367 | 
368 |   for (const workflowDir of workflowDirs) {
369 |     const workflowTools = workflowMap.get(workflowDir) ?? [];
370 |     const canonicalTools = workflowTools.filter((t) => t.isCanonical);
371 |     const reExportTools = workflowTools.filter((t) => !t.isCanonical);
372 | 
373 |     // Try to get metadata from index.ts, fall back to hardcoded names/descriptions
374 |     const metadata = await getWorkflowMetadata(workflowDir);
375 | 
376 |     const workflowInfo: WorkflowInfo = {
377 |       name: workflowDir,
378 |       displayName: metadata?.displayName ?? getWorkflowDisplayName(workflowDir),
379 |       description: metadata?.description ?? getWorkflowDescription(workflowDir),
380 |       tools: workflowTools.sort((a, b) => a.name.localeCompare(b.name)),
381 |       toolCount: workflowTools.length,
382 |       canonicalCount: canonicalTools.length,
383 |       reExportCount: reExportTools.length,
384 |     };
385 | 
386 |     workflows.push(workflowInfo);
387 |   }
388 | 
389 |   const stats: AnalysisStats = {
390 |     totalTools: allTools.length,
391 |     canonicalTools: canonicalCount,
392 |     reExportTools: reExportCount,
393 |     workflowCount: workflows.length,
394 |   };
395 | 
396 |   return {
397 |     workflows: workflows.sort((a, b) => a.displayName.localeCompare(b.displayName)),
398 |     tools: allTools.sort((a, b) => a.name.localeCompare(b.name)),
399 |     stats,
400 |   };
401 | }
402 | 
403 | /**
404 |  * Get only canonical tools (excluding re-exports) for documentation generation
405 |  */
406 | export async function getCanonicalTools(): Promise<ToolInfo[]> {
407 |   const analysis = await getStaticToolAnalysis();
408 |   return analysis.tools.filter((tool) => tool.isCanonical);
409 | }
410 | 
411 | /**
412 |  * Get tools grouped by workflow for documentation generation
413 |  */
414 | export async function getToolsByWorkflow(): Promise<Map<string, ToolInfo[]>> {
415 |   const analysis = await getStaticToolAnalysis();
416 |   const workflowMap = new Map<string, ToolInfo[]>();
417 | 
418 |   for (const workflow of analysis.workflows) {
419 |     // Only include canonical tools for documentation
420 |     const canonicalTools = workflow.tools.filter((tool) => tool.isCanonical);
421 |     if (canonicalTools.length > 0) {
422 |       workflowMap.set(workflow.name, canonicalTools);
423 |     }
424 |   }
425 | 
426 |   return workflowMap;
427 | }
428 | 
429 | // CLI support - if run directly, perform analysis and output results
430 | if (import.meta.url === `file://${process.argv[1]}`) {
431 |   async function main(): Promise<void> {
432 |     try {
433 |       console.log('🔍 Performing static analysis...');
434 |       const analysis = await getStaticToolAnalysis();
435 | 
436 |       console.log('\n📊 Analysis Results:');
437 |       console.log(`   Workflows: ${analysis.stats.workflowCount}`);
438 |       console.log(`   Total tools: ${analysis.stats.totalTools}`);
439 |       console.log(`   Canonical tools: ${analysis.stats.canonicalTools}`);
440 |       console.log(`   Re-export tools: ${analysis.stats.reExportTools}`);
441 | 
442 |       if (process.argv.includes('--json')) {
443 |         console.log('\n' + JSON.stringify(analysis, null, 2));
444 |       } else {
445 |         console.log('\n📂 Workflows:');
446 |         for (const workflow of analysis.workflows) {
447 |           console.log(
448 |             `   • ${workflow.displayName} (${workflow.canonicalCount} canonical, ${workflow.reExportCount} re-exports)`,
449 |           );
450 |         }
451 |       }
452 |     } catch (error) {
453 |       console.error('❌ Analysis failed:', error);
454 |       process.exit(1);
455 |     }
456 |   }
457 | 
458 |   main();
459 | }
460 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/project-scaffolding/__tests__/scaffold_macos_project.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Test for scaffold_macos_project plugin - Dependency Injection Architecture
  3 |  *
  4 |  * Tests the plugin structure and exported components for scaffold_macos_project tool.
  5 |  * Uses pure dependency injection with createMockFileSystemExecutor.
  6 |  * NO VITEST MOCKING ALLOWED - Only createMockExecutor/createMockFileSystemExecutor
  7 |  *
  8 |  * Plugin location: plugins/utilities/scaffold_macos_project.js
  9 |  */
 10 | 
 11 | import { describe, it, expect, beforeEach } from 'vitest';
 12 | import { z } from 'zod';
 13 | import {
 14 |   createMockFileSystemExecutor,
 15 |   createNoopExecutor,
 16 |   createMockExecutor,
 17 | } from '../../../../test-utils/mock-executors.ts';
 18 | import plugin, { scaffold_macos_projectLogic } from '../scaffold_macos_project.ts';
 19 | import { TemplateManager } from '../../../../utils/template/index.ts';
 20 | 
 21 | // ONLY ALLOWED MOCKING: createMockFileSystemExecutor
 22 | 
 23 | describe('scaffold_macos_project plugin', () => {
 24 |   let mockFileSystemExecutor: ReturnType<typeof createMockFileSystemExecutor>;
 25 |   let templateManagerStub: {
 26 |     getTemplatePath: (
 27 |       platform: string,
 28 |       commandExecutor?: unknown,
 29 |       fileSystemExecutor?: unknown,
 30 |     ) => Promise<string>;
 31 |     cleanup: (path: string) => Promise<void>;
 32 |     setError: (error: Error | string | null) => void;
 33 |     getCalls: () => string;
 34 |     resetCalls: () => void;
 35 |   };
 36 | 
 37 |   beforeEach(async () => {
 38 |     // Create template manager stub using pure JavaScript approach
 39 |     let templateManagerCall = '';
 40 |     let templateManagerError: Error | string | null = null;
 41 | 
 42 |     templateManagerStub = {
 43 |       getTemplatePath: async (
 44 |         platform: string,
 45 |         commandExecutor?: unknown,
 46 |         fileSystemExecutor?: unknown,
 47 |       ) => {
 48 |         templateManagerCall = `getTemplatePath(${platform})`;
 49 |         if (templateManagerError) {
 50 |           throw templateManagerError;
 51 |         }
 52 |         return '/tmp/test-templates/macos';
 53 |       },
 54 |       cleanup: async (path: string) => {
 55 |         templateManagerCall += `,cleanup(${path})`;
 56 |         return undefined;
 57 |       },
 58 |       // Test helpers
 59 |       setError: (error: Error | string | null) => {
 60 |         templateManagerError = error;
 61 |       },
 62 |       getCalls: () => templateManagerCall,
 63 |       resetCalls: () => {
 64 |         templateManagerCall = '';
 65 |       },
 66 |     };
 67 | 
 68 |     // Create fresh mock file system executor for each test
 69 |     mockFileSystemExecutor = createMockFileSystemExecutor({
 70 |       existsSync: () => false,
 71 |       mkdir: async () => {},
 72 |       cp: async () => {},
 73 |       readFile: async () => 'template content with MyProject placeholder',
 74 |       writeFile: async () => {},
 75 |       readdir: async () => [
 76 |         { name: 'Package.swift', isDirectory: () => false, isFile: () => true },
 77 |         { name: 'MyProject.swift', isDirectory: () => false, isFile: () => true },
 78 |       ],
 79 |     });
 80 | 
 81 |     // Replace the real TemplateManager with our stub for most tests
 82 |     (TemplateManager as any).getTemplatePath = templateManagerStub.getTemplatePath;
 83 |     (TemplateManager as any).cleanup = templateManagerStub.cleanup;
 84 |   });
 85 | 
 86 |   describe('Export Field Validation (Literal)', () => {
 87 |     it('should have correct name field', () => {
 88 |       expect(plugin.name).toBe('scaffold_macos_project');
 89 |     });
 90 | 
 91 |     it('should have correct description field', () => {
 92 |       expect(plugin.description).toBe(
 93 |         'Scaffold a new macOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper macOS configuration.',
 94 |       );
 95 |     });
 96 | 
 97 |     it('should have handler as function', () => {
 98 |       expect(typeof plugin.handler).toBe('function');
 99 |     });
100 | 
101 |     it('should have valid schema with required fields', () => {
102 |       // Test the schema object exists
103 |       expect(plugin.schema).toBeDefined();
104 |       expect(plugin.schema.projectName).toBeDefined();
105 |       expect(plugin.schema.outputPath).toBeDefined();
106 |       expect(plugin.schema.bundleIdentifier).toBeDefined();
107 |       expect(plugin.schema.customizeNames).toBeDefined();
108 |       expect(plugin.schema.deploymentTarget).toBeDefined();
109 |     });
110 |   });
111 | 
112 |   describe('Command Generation', () => {
113 |     it('should generate correct curl command for macOS template download', async () => {
114 |       // This test validates that the curl command would be generated correctly
115 |       // by verifying the URL construction logic
116 |       const expectedUrl =
117 |         'https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template/releases/download/';
118 | 
119 |       // The curl command should be structured correctly for macOS template
120 |       expect(expectedUrl).toContain('XcodeBuildMCP-macOS-Template');
121 |       expect(expectedUrl).toContain('releases/download');
122 | 
123 |       // The template zip file should follow the expected pattern
124 |       const expectedFilename = 'template.zip';
125 |       expect(expectedFilename).toMatch(/template\.zip$/);
126 | 
127 |       // The curl command flags should be correct
128 |       const expectedCurlFlags = ['-L', '-f', '-o'];
129 |       expect(expectedCurlFlags).toContain('-L'); // Follow redirects
130 |       expect(expectedCurlFlags).toContain('-f'); // Fail on HTTP errors
131 |       expect(expectedCurlFlags).toContain('-o'); // Output to file
132 |     });
133 | 
134 |     it('should generate correct unzip command for template extraction', async () => {
135 |       // This test validates that the unzip command would be generated correctly
136 |       // by verifying the command structure
137 |       const expectedUnzipCommand = ['unzip', '-q', 'template.zip'];
138 | 
139 |       // The unzip command should use the quiet flag
140 |       expect(expectedUnzipCommand).toContain('-q');
141 | 
142 |       // The unzip command should target the template zip file
143 |       expect(expectedUnzipCommand).toContain('template.zip');
144 | 
145 |       // The unzip command should be structured correctly
146 |       expect(expectedUnzipCommand[0]).toBe('unzip');
147 |       expect(expectedUnzipCommand[1]).toBe('-q');
148 |       expect(expectedUnzipCommand[2]).toMatch(/template\.zip$/);
149 |     });
150 | 
151 |     it('should generate correct commands for template with version', async () => {
152 |       // This test validates that the curl command would be generated correctly with version
153 |       const testVersion = 'v1.0.0';
154 |       const expectedUrlWithVersion = `https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template/releases/download/${testVersion}/`;
155 | 
156 |       // The URL should contain the specific version
157 |       expect(expectedUrlWithVersion).toContain(testVersion);
158 |       expect(expectedUrlWithVersion).toContain('XcodeBuildMCP-macOS-Template');
159 |       expect(expectedUrlWithVersion).toContain('releases/download');
160 | 
161 |       // The version should be in the correct format
162 |       expect(testVersion).toMatch(/^v\d+\.\d+\.\d+$/);
163 | 
164 |       // The full URL should be correctly constructed
165 |       expect(expectedUrlWithVersion).toBe(
166 |         `https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template/releases/download/${testVersion}/`,
167 |       );
168 |     });
169 | 
170 |     it('should not generate commands when using local template path', async () => {
171 |       let capturedCommands: string[][] = [];
172 |       const trackingExecutor = async (command: string[]) => {
173 |         capturedCommands.push(command);
174 |         return {
175 |           success: true,
176 |           output: 'Command successful',
177 |           error: undefined,
178 |           process: { pid: 12345 },
179 |         };
180 |       };
181 | 
182 |       // Store original environment variable
183 |       const originalEnv = process.env.XCODEBUILDMCP_MACOS_TEMPLATE_PATH;
184 | 
185 |       // Mock local template path exists
186 |       mockFileSystemExecutor.existsSync = (path: string) => {
187 |         return path === '/local/template/path' || path === '/local/template/path/template';
188 |       };
189 | 
190 |       // Set environment variable for local template path
191 |       process.env.XCODEBUILDMCP_MACOS_TEMPLATE_PATH = '/local/template/path';
192 | 
193 |       // Restore original TemplateManager for command generation tests
194 |       const { TemplateManager: OriginalTemplateManager } = await import(
195 |         '../../../../utils/template/index.ts'
196 |       );
197 |       (TemplateManager as any).getTemplatePath = OriginalTemplateManager.getTemplatePath;
198 |       (TemplateManager as any).cleanup = OriginalTemplateManager.cleanup;
199 | 
200 |       await scaffold_macos_projectLogic(
201 |         {
202 |           projectName: 'TestMacApp',
203 |           outputPath: '/tmp/test-projects',
204 |         },
205 |         trackingExecutor,
206 |         mockFileSystemExecutor,
207 |       );
208 | 
209 |       // Should not generate any curl or unzip commands when using local template
210 |       expect(capturedCommands).not.toContainEqual(
211 |         expect.arrayContaining(['curl', expect.anything(), expect.anything()]),
212 |       );
213 |       expect(capturedCommands).not.toContainEqual(
214 |         expect.arrayContaining(['unzip', expect.anything(), expect.anything()]),
215 |       );
216 | 
217 |       // Clean up environment variable
218 |       process.env.XCODEBUILDMCP_MACOS_TEMPLATE_PATH = originalEnv;
219 | 
220 |       // Restore stub after test
221 |       (TemplateManager as any).getTemplatePath = templateManagerStub.getTemplatePath;
222 |       (TemplateManager as any).cleanup = templateManagerStub.cleanup;
223 |     });
224 |   });
225 | 
226 |   describe('Handler Behavior (Complete Literal Returns)', () => {
227 |     it('should return success response for valid scaffold macOS project request', async () => {
228 |       const result = await scaffold_macos_projectLogic(
229 |         {
230 |           projectName: 'TestMacApp',
231 |           outputPath: '/tmp/test-projects',
232 |           bundleIdentifier: 'com.test.macapp',
233 |           customizeNames: false,
234 |         },
235 |         createNoopExecutor(),
236 |         mockFileSystemExecutor,
237 |       );
238 | 
239 |       expect(result).toEqual({
240 |         content: [
241 |           {
242 |             type: 'text',
243 |             text: JSON.stringify(
244 |               {
245 |                 success: true,
246 |                 projectPath: '/tmp/test-projects',
247 |                 platform: 'macOS',
248 |                 message: 'Successfully scaffolded macOS project "TestMacApp" in /tmp/test-projects',
249 |                 nextSteps: [
250 |                   'Important: Before working on the project make sure to read the README.md file in the workspace root directory.',
251 |                   'Build for macOS: build_macos({ workspacePath: "/tmp/test-projects/MyProject.xcworkspace", scheme: "MyProject" })',
252 |                   'Build & Run on macOS: build_run_macos({ workspacePath: "/tmp/test-projects/MyProject.xcworkspace", scheme: "MyProject" })',
253 |                 ],
254 |               },
255 |               null,
256 |               2,
257 |             ),
258 |           },
259 |         ],
260 |       });
261 | 
262 |       // Verify template manager calls using manual tracking
263 |       expect(templateManagerStub.getCalls()).toBe(
264 |         'getTemplatePath(macOS),cleanup(/tmp/test-templates/macos)',
265 |       );
266 |     });
267 | 
268 |     it('should return success response with customizeNames false', async () => {
269 |       const result = await scaffold_macos_projectLogic(
270 |         {
271 |           projectName: 'TestMacApp',
272 |           outputPath: '/tmp/test-projects',
273 |           customizeNames: false,
274 |         },
275 |         createNoopExecutor(),
276 |         mockFileSystemExecutor,
277 |       );
278 | 
279 |       expect(result).toEqual({
280 |         content: [
281 |           {
282 |             type: 'text',
283 |             text: JSON.stringify(
284 |               {
285 |                 success: true,
286 |                 projectPath: '/tmp/test-projects',
287 |                 platform: 'macOS',
288 |                 message: 'Successfully scaffolded macOS project "TestMacApp" in /tmp/test-projects',
289 |                 nextSteps: [
290 |                   'Important: Before working on the project make sure to read the README.md file in the workspace root directory.',
291 |                   'Build for macOS: build_macos({ workspacePath: "/tmp/test-projects/MyProject.xcworkspace", scheme: "MyProject" })',
292 |                   'Build & Run on macOS: build_run_macos({ workspacePath: "/tmp/test-projects/MyProject.xcworkspace", scheme: "MyProject" })',
293 |                 ],
294 |               },
295 |               null,
296 |               2,
297 |             ),
298 |           },
299 |         ],
300 |       });
301 |     });
302 | 
303 |     it('should return error response for invalid project name', async () => {
304 |       const result = await scaffold_macos_projectLogic(
305 |         {
306 |           projectName: '123InvalidName',
307 |           outputPath: '/tmp/test-projects',
308 |         },
309 |         createNoopExecutor(),
310 |         mockFileSystemExecutor,
311 |       );
312 | 
313 |       expect(result).toEqual({
314 |         content: [
315 |           {
316 |             type: 'text',
317 |             text: JSON.stringify(
318 |               {
319 |                 success: false,
320 |                 error:
321 |                   'Project name must start with a letter and contain only letters, numbers, and underscores',
322 |               },
323 |               null,
324 |               2,
325 |             ),
326 |           },
327 |         ],
328 |         isError: true,
329 |       });
330 |     });
331 | 
332 |     it('should return error response for existing project files', async () => {
333 |       // Override existsSync to return true for workspace file
334 |       mockFileSystemExecutor.existsSync = () => true;
335 | 
336 |       const result = await scaffold_macos_projectLogic(
337 |         {
338 |           projectName: 'TestMacApp',
339 |           outputPath: '/tmp/test-projects',
340 |         },
341 |         createNoopExecutor(),
342 |         mockFileSystemExecutor,
343 |       );
344 | 
345 |       expect(result).toEqual({
346 |         content: [
347 |           {
348 |             type: 'text',
349 |             text: JSON.stringify(
350 |               {
351 |                 success: false,
352 |                 error: 'Xcode project files already exist in /tmp/test-projects',
353 |               },
354 |               null,
355 |               2,
356 |             ),
357 |           },
358 |         ],
359 |         isError: true,
360 |       });
361 |     });
362 | 
363 |     it('should return error response for template manager failure', async () => {
364 |       templateManagerStub.setError(new Error('Template not found'));
365 | 
366 |       const result = await scaffold_macos_projectLogic(
367 |         {
368 |           projectName: 'TestMacApp',
369 |           outputPath: '/tmp/test-projects',
370 |         },
371 |         createNoopExecutor(),
372 |         mockFileSystemExecutor,
373 |       );
374 | 
375 |       expect(result).toEqual({
376 |         content: [
377 |           {
378 |             type: 'text',
379 |             text: JSON.stringify(
380 |               {
381 |                 success: false,
382 |                 error: 'Failed to get template for macOS: Template not found',
383 |               },
384 |               null,
385 |               2,
386 |             ),
387 |           },
388 |         ],
389 |         isError: true,
390 |       });
391 |     });
392 |   });
393 | 
394 |   describe('File System Operations', () => {
395 |     it('should create directories and process files correctly', async () => {
396 |       await scaffold_macos_projectLogic(
397 |         {
398 |           projectName: 'TestApp',
399 |           outputPath: '/tmp/test',
400 |           customizeNames: true,
401 |         },
402 |         createNoopExecutor(),
403 |         mockFileSystemExecutor,
404 |       );
405 | 
406 |       // Verify template manager calls using manual tracking
407 |       expect(templateManagerStub.getCalls()).toBe(
408 |         'getTemplatePath(macOS),cleanup(/tmp/test-templates/macos)',
409 |       );
410 | 
411 |       // File system operations are called by the mock implementation
412 |       // but we can't verify them without vitest mocking patterns
413 |       // This test validates the integration works correctly
414 |     });
415 |   });
416 | });
417 | 
```

--------------------------------------------------------------------------------
/docs/RELOADEROO_FOR_XCODEBUILDMCP.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Reloaderoo Usage Guide for XcodeBuildMCP
  2 | 
  3 | This guide explains how to use Reloaderoo for interacting with XcodeBuildMCP as a CLI to save context window space.
  4 | 
  5 | You can use this guide to prompt your agent, but providing the entire document will give you no actual benefits. You will end up using more context than just using MCP server directly. So it's recommended that you curate this document by removing the example commands that you don't need and just keeping the ones that are right for your project. You'll then want to keep this file within your project workspace and then include it in the context window when you need to interact your agent to use XcodeBuildMCP tools.
  6 | 
  7 | > [!IMPORTANT]
  8 | > Please remove this introduction before you prompt your agent with this file or any derrived version of it.
  9 | 
 10 | ## Installation
 11 | 
 12 | Reloaderoo is available via npm and can be used with npx for universal compatibility.
 13 | 
 14 | ```bash
 15 | # Use npx to run reloaderoo
 16 | npx reloaderoo@latest --help
 17 | ```
 18 | 
 19 | **Example Tool Calls:**
 20 | 
 21 | ### Dynamic Tool Discovery
 22 | 
 23 | - **`discover_tools`**: Analyzes a task description to enable relevant tools.
 24 |   ```bash
 25 |   npx reloaderoo@latest inspect call-tool discover_tools --params '{"task_description": "I want to build and run my iOS app on a simulator."}' -- node build/index.js
 26 |   ```
 27 | 
 28 | ### iOS Device Development
 29 | 
 30 | - **`build_device`**: Builds an app for a physical device.
 31 |   ```bash
 32 |   npx reloaderoo@latest inspect call-tool build_device --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
 33 |   ```
 34 | - **`get_device_app_path`**: Gets the `.app` bundle path for a device build.
 35 |   ```bash
 36 |   npx reloaderoo@latest inspect call-tool get_device_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
 37 |   ```
 38 | - **`install_app_device`**: Installs an app on a physical device.
 39 |   ```bash
 40 |   npx reloaderoo@latest inspect call-tool install_app_device --params '{"deviceId": "DEVICE_UDID", "appPath": "/path/to/MyApp.app"}' -- node build/index.js
 41 |   ```
 42 | - **`launch_app_device`**: Launches an app on a physical device.
 43 |   ```bash
 44 |   npx reloaderoo@latest inspect call-tool launch_app_device --params '{"deviceId": "DEVICE_UDID", "bundleId": "com.example.MyApp"}' -- node build/index.js
 45 |   ```
 46 | - **`list_devices`**: Lists connected physical devices.
 47 |   ```bash
 48 |   npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/index.js
 49 |   ```
 50 | - **`stop_app_device`**: Stops an app on a physical device.
 51 |   ```bash
 52 |   npx reloaderoo@latest inspect call-tool stop_app_device --params '{"deviceId": "DEVICE_UDID", "processId": 12345}' -- node build/index.js
 53 |   ```
 54 | - **`test_device`**: Runs tests on a physical device.
 55 |   ```bash
 56 |   npx reloaderoo@latest inspect call-tool test_device --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "deviceId": "DEVICE_UDID"}' -- node build/index.js
 57 |   ```
 58 | 
 59 | ### iOS Simulator Development
 60 | 
 61 | - **`boot_sim`**: Boots a simulator.
 62 |   ```bash
 63 |   npx reloaderoo@latest inspect call-tool boot_sim --params '{"simulatorId": "SIMULATOR_UUID"}' -- node build/index.js
 64 |   ```
 65 | - **`build_run_sim`**: Builds and runs an app on a simulator.
 66 |   ```bash
 67 |   npx reloaderoo@latest inspect call-tool build_run_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -- node build/index.js
 68 |   ```
 69 | - **`build_sim`**: Builds an app for a simulator.
 70 |   ```bash
 71 |   npx reloaderoo@latest inspect call-tool build_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -- node build/index.js
 72 |   ```
 73 | - **`get_sim_app_path`**: Gets the `.app` bundle path for a simulator build.
 74 |   ```bash
 75 |   npx reloaderoo@latest inspect call-tool get_sim_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "platform": "iOS Simulator", "simulatorName": "iPhone 16"}' -- node build/index.js
 76 |   ```
 77 | - **`install_app_sim`**: Installs an app on a simulator.
 78 |   ```bash
 79 |   npx reloaderoo@latest inspect call-tool install_app_sim --params '{"simulatorId": "SIMULATOR_UUID", "appPath": "/path/to/MyApp.app"}' -- node build/index.js
 80 |   ```
 81 | - **`launch_app_logs_sim`**: Launches an app on a simulator with log capture.
 82 |   ```bash
 83 |   npx reloaderoo@latest inspect call-tool launch_app_logs_sim --params '{"simulatorId": "SIMULATOR_UUID", "bundleId": "com.example.MyApp"}' -- node build/index.js
 84 |   ```
 85 | - **`launch_app_sim`**: Launches an app on a simulator.
 86 |   ```bash
 87 |   npx reloaderoo@latest inspect call-tool launch_app_sim --params '{"simulatorName": "iPhone 16", "bundleId": "com.example.MyApp"}' -- node build/index.js
 88 |   ```
 89 | - **`list_sims`**: Lists available simulators.
 90 |   ```bash
 91 |   npx reloaderoo@latest inspect call-tool list_sims --params '{}' -- node build/index.js
 92 |   ```
 93 | - **`open_sim`**: Opens the Simulator application.
 94 |   ```bash
 95 |   npx reloaderoo@latest inspect call-tool open_sim --params '{}' -- node build/index.js
 96 |   ```
 97 | - **`stop_app_sim`**: Stops an app on a simulator.
 98 |   ```bash
 99 |   npx reloaderoo@latest inspect call-tool stop_app_sim --params '{"simulatorName": "iPhone 16", "bundleId": "com.example.MyApp"}' -- node build/index.js
100 |   ```
101 | - **`test_sim`**: Runs tests on a simulator.
102 |   ```bash
103 |   npx reloaderoo@latest inspect call-tool test_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -- node build/index.js
104 |   ```
105 | 
106 | ### Log Capture & Management
107 | 
108 | - **`start_device_log_cap`**: Starts log capture for a physical device.
109 |   ```bash
110 |   npx reloaderoo@latest inspect call-tool start_device_log_cap --params '{"deviceId": "DEVICE_UDID", "bundleId": "com.example.MyApp"}' -- node build/index.js
111 |   ```
112 | - **`start_sim_log_cap`**: Starts log capture for a simulator.
113 |   ```bash
114 |   npx reloaderoo@latest inspect call-tool start_sim_log_cap --params '{"simulatorUuid": "SIMULATOR_UUID", "bundleId": "com.example.MyApp"}' -- node build/index.js
115 |   ```
116 | - **`stop_device_log_cap`**: Stops log capture for a physical device.
117 |   ```bash
118 |   npx reloaderoo@latest inspect call-tool stop_device_log_cap --params '{"logSessionId": "SESSION_ID"}' -- node build/index.js
119 |   ```
120 | - **`stop_sim_log_cap`**: Stops log capture for a simulator.
121 |   ```bash
122 |   npx reloaderoo@latest inspect call-tool stop_sim_log_cap --params '{"logSessionId": "SESSION_ID"}' -- node build/index.js
123 |   ```
124 | 
125 | ### macOS Development
126 | 
127 | - **`build_macos`**: Builds a macOS app.
128 |   ```bash
129 |   npx reloaderoo@latest inspect call-tool build_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
130 |   ```
131 | - **`build_run_macos`**: Builds and runs a macOS app.
132 |   ```bash
133 |   npx reloaderoo@latest inspect call-tool build_run_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
134 |   ```
135 | - **`get_mac_app_path`**: Gets the `.app` bundle path for a macOS build.
136 |   ```bash
137 |   npx reloaderoo@latest inspect call-tool get_mac_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
138 |   ```
139 | - **`launch_mac_app`**: Launches a macOS app.
140 |   ```bash
141 |   npx reloaderoo@latest inspect call-tool launch_mac_app --params '{"appPath": "/Applications/Calculator.app"}' -- node build/index.js
142 |   ```
143 | - **`stop_mac_app`**: Stops a macOS app.
144 |   ```bash
145 |   npx reloaderoo@latest inspect call-tool stop_mac_app --params '{"appName": "Calculator"}' -- node build/index.js
146 |   ```
147 | - **`test_macos`**: Runs tests for a macOS project.
148 |   ```bash
149 |   npx reloaderoo@latest inspect call-tool test_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
150 |   ```
151 | 
152 | ### Project Discovery
153 | 
154 | - **`discover_projs`**: Discovers Xcode projects and workspaces.
155 |   ```bash
156 |   npx reloaderoo@latest inspect call-tool discover_projs --params '{"workspaceRoot": "/path/to/workspace"}' -- node build/index.js
157 |   ```
158 | - **`get_app_bundle_id`**: Gets an app's bundle identifier.
159 |   ```bash
160 |   npx reloaderoo@latest inspect call-tool get_app_bundle_id --params '{"appPath": "/path/to/MyApp.app"}' -- node build/index.js
161 |   ```
162 | - **`get_mac_bundle_id`**: Gets a macOS app's bundle identifier.
163 |   ```bash
164 |   npx reloaderoo@latest inspect call-tool get_mac_bundle_id --params '{"appPath": "/Applications/Calculator.app"}' -- node build/index.js
165 |   ```
166 | - **`list_schemes`**: Lists schemes in a project or workspace.
167 |   ```bash
168 |   npx reloaderoo@latest inspect call-tool list_schemes --params '{"projectPath": "/path/to/MyProject.xcodeproj"}' -- node build/index.js
169 |   ```
170 | - **`show_build_settings`**: Shows build settings for a scheme.
171 |   ```bash
172 |   npx reloaderoo@latest inspect call-tool show_build_settings --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
173 |   ```
174 | 
175 | ### Project Scaffolding
176 | 
177 | - **`scaffold_ios_project`**: Scaffolds a new iOS project.
178 |   ```bash
179 |   npx reloaderoo@latest inspect call-tool scaffold_ios_project --params '{"projectName": "MyNewApp", "outputPath": "/path/to/projects"}' -- node build/index.js
180 |   ```
181 | - **`scaffold_macos_project`**: Scaffolds a new macOS project.
182 |   ```bash
183 |   npx reloaderoo@latest inspect call-tool scaffold_macos_project --params '{"projectName": "MyNewMacApp", "outputPath": "/path/to/projects"}' -- node build/index.js
184 |   ```
185 | 
186 | ### Project Utilities
187 | 
188 | - **`clean`**: Cleans build artifacts.
189 |   ```bash
190 |   # For a project
191 |   npx reloaderoo@latest inspect call-tool clean --params '{"projectPath": "/path/to/MyProject.xcodeproj"}' -- node build/index.js
192 |   # For a workspace
193 |   npx reloaderoo@latest inspect call-tool clean --params '{"workspacePath": "/path/to/MyWorkspace.xcworkspace", "scheme": "MyScheme"}' -- node build/index.js
194 |   ```
195 | 
196 | ### Simulator Management
197 | 
198 | - **`reset_sim_location`**: Resets a simulator's location.
199 |   ```bash
200 |   npx reloaderoo@latest inspect call-tool reset_sim_location --params '{"simulatorUuid": "SIMULATOR_UUID"}' -- node build/index.js
201 |   ```
202 | - **`set_sim_appearance`**: Sets a simulator's appearance (dark/light mode).
203 |   ```bash
204 |   npx reloaderoo@latest inspect call-tool set_sim_appearance --params '{"simulatorUuid": "SIMULATOR_UUID", "mode": "dark"}' -- node build/index.js
205 |   ```
206 | - **`set_sim_location`**: Sets a simulator's GPS location.
207 |   ```bash
208 |   npx reloaderoo@latest inspect call-tool set_sim_location --params '{"simulatorUuid": "SIMULATOR_UUID", "latitude": 37.7749, "longitude": -122.4194}' -- node build/index.js
209 |   ```
210 | - **`sim_statusbar`**: Overrides a simulator's status bar.
211 |   ```bash
212 |   npx reloaderoo@latest inspect call-tool sim_statusbar --params '{"simulatorUuid": "SIMULATOR_UUID", "dataNetwork": "wifi"}' -- node build/index.js
213 |   ```
214 | 
215 | ### Swift Package Manager
216 | 
217 | - **`swift_package_build`**: Builds a Swift package.
218 |   ```bash
219 |   npx reloaderoo@latest inspect call-tool swift_package_build --params '{"packagePath": "/path/to/package"}' -- node build/index.js
220 |   ```
221 | - **`swift_package_clean`**: Cleans a Swift package.
222 |   ```bash
223 |   npx reloaderoo@latest inspect call-tool swift_package_clean --params '{"packagePath": "/path/to/package"}' -- node build/index.js
224 |   ```
225 | - **`swift_package_list`**: Lists running Swift package processes.
226 |   ```bash
227 |   npx reloaderoo@latest inspect call-tool swift_package_list --params '{}' -- node build/index.js
228 |   ```
229 | - **`swift_package_run`**: Runs a Swift package executable.
230 |   ```bash
231 |   npx reloaderoo@latest inspect call-tool swift_package_run --params '{"packagePath": "/path/to/package"}' -- node build/index.js
232 |   ```
233 | - **`swift_package_stop`**: Stops a running Swift package process.
234 |   ```bash
235 |   npx reloaderoo@latest inspect call-tool swift_package_stop --params '{"pid": 12345}' -- node build/index.js
236 |   ```
237 | - **`swift_package_test`**: Tests a Swift package.
238 |   ```bash
239 |   npx reloaderoo@latest inspect call-tool swift_package_test --params '{"packagePath": "/path/to/package"}' -- node build/index.js
240 |   ```
241 | 
242 | ### System Doctor
243 | 
244 | - **`doctor`**: Runs system diagnostics.
245 |   ```bash
246 |   npx reloaderoo@latest inspect call-tool doctor --params '{}' -- node build/index.js
247 |   ```
248 | 
249 | ### UI Testing & Automation
250 | 
251 | - **`button`**: Simulates a hardware button press.
252 |   ```bash
253 |   npx reloaderoo@latest inspect call-tool button --params '{"simulatorUuid": "SIMULATOR_UUID", "buttonType": "home"}' -- node build/index.js
254 |   ```
255 | - **`describe_ui`**: Gets the UI hierarchy of the current screen.
256 |   ```bash
257 |   npx reloaderoo@latest inspect call-tool describe_ui --params '{"simulatorUuid": "SIMULATOR_UUID"}' -- node build/index.js
258 |   ```
259 | - **`gesture`**: Performs a pre-defined gesture.
260 |   ```bash
261 |   npx reloaderoo@latest inspect call-tool gesture --params '{"simulatorUuid": "SIMULATOR_UUID", "preset": "scroll-up"}' -- node build/index.js
262 |   ```
263 | - **`key_press`**: Simulates a key press.
264 |   ```bash
265 |   npx reloaderoo@latest inspect call-tool key_press --params '{"simulatorUuid": "SIMULATOR_UUID", "keyCode": 40}' -- node build/index.js
266 |   ```
267 | - **`key_sequence`**: Simulates a sequence of key presses.
268 |   ```bash
269 |   npx reloaderoo@latest inspect call-tool key_sequence --params '{"simulatorUuid": "SIMULATOR_UUID", "keyCodes": [40, 42, 44]}' -- node build/index.js
270 |   ```
271 | - **`long_press`**: Performs a long press at coordinates.
272 |   ```bash
273 |   npx reloaderoo@latest inspect call-tool long_press --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200, "duration": 1500}' -- node build/index.js
274 |   ```
275 | - **`screenshot`**: Takes a screenshot.
276 |   ```bash
277 |   npx reloaderoo@latest inspect call-tool screenshot --params '{"simulatorUuid": "SIMULATOR_UUID"}' -- node build/index.js
278 |   ```
279 | - **`swipe`**: Performs a swipe gesture.
280 |   ```bash
281 |   npx reloaderoo@latest inspect call-tool swipe --params '{"simulatorUuid": "SIMULATOR_UUID", "x1": 100, "y1": 200, "x2": 100, "y2": 400}' -- node build/index.js
282 |   ```
283 | - **`tap`**: Performs a tap at coordinates.
284 |   ```bash
285 |   npx reloaderoo@latest inspect call-tool tap --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200}' -- node build/index.js
286 |   ```
287 | - **`touch`**: Simulates a touch down or up event.
288 |   ```bash
289 |   npx reloaderoo@latest inspect call-tool touch --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200, "down": true}' -- node build/index.js
290 |   ```
291 | - **`type_text`**: Types text into the focused element.
292 |   ```bash
293 |   npx reloaderoo@latest inspect call-tool type_text --params '{"simulatorUuid": "SIMULATOR_UUID", "text": "Hello, World!"}' -- node build/index.js
294 |   ```
295 | 
296 | ### Resources
297 | 
298 | - **Read devices resource**:
299 |   ```bash
300 |   npx reloaderoo@latest inspect read-resource "xcodebuildmcp://devices" -- node build/index.js
301 |   ```
302 | - **Read simulators resource**:
303 |   ```bash
304 |   npx reloaderoo@latest inspect read-resource "xcodebuildmcp://simulators" -- node build/index.js
305 |   ```
306 | - **Read doctor resource**:
307 |   ```bash
308 |   npx reloaderoo@latest inspect read-resource "xcodebuildmcp://doctor" -- node build/index.js
309 |   ```
310 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for build_macos plugin (unified)
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using pure dependency injection for deterministic testing
  5 |  * NO VITEST MOCKING ALLOWED - Only createMockExecutor and createMockFileSystemExecutor
  6 |  */
  7 | 
  8 | import { describe, it, expect, beforeEach } from 'vitest';
  9 | import { z } from 'zod';
 10 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
 11 | import { sessionStore } from '../../../../utils/session-store.ts';
 12 | import buildMacOS, { buildMacOSLogic } from '../build_macos.ts';
 13 | 
 14 | describe('build_macos plugin', () => {
 15 |   beforeEach(() => {
 16 |     sessionStore.clear();
 17 |   });
 18 | 
 19 |   describe('Export Field Validation (Literal)', () => {
 20 |     it('should have correct name', () => {
 21 |       expect(buildMacOS.name).toBe('build_macos');
 22 |     });
 23 | 
 24 |     it('should have correct description', () => {
 25 |       expect(buildMacOS.description).toBe('Builds a macOS app.');
 26 |     });
 27 | 
 28 |     it('should have handler function', () => {
 29 |       expect(typeof buildMacOS.handler).toBe('function');
 30 |     });
 31 | 
 32 |     it('should validate schema correctly', () => {
 33 |       const schema = z.object(buildMacOS.schema);
 34 | 
 35 |       expect(schema.safeParse({}).success).toBe(true);
 36 |       expect(
 37 |         schema.safeParse({
 38 |           derivedDataPath: '/path/to/derived-data',
 39 |           extraArgs: ['--arg1', '--arg2'],
 40 |           preferXcodebuild: true,
 41 |         }).success,
 42 |       ).toBe(true);
 43 | 
 44 |       expect(schema.safeParse({ derivedDataPath: 42 }).success).toBe(false);
 45 |       expect(schema.safeParse({ extraArgs: ['--ok', 1] }).success).toBe(false);
 46 |       expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false);
 47 | 
 48 |       const schemaKeys = Object.keys(buildMacOS.schema).sort();
 49 |       expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs', 'preferXcodebuild'].sort());
 50 |     });
 51 |   });
 52 | 
 53 |   describe('Handler Requirements', () => {
 54 |     it('should require scheme when no defaults provided', async () => {
 55 |       const result = await buildMacOS.handler({});
 56 | 
 57 |       expect(result.isError).toBe(true);
 58 |       expect(result.content[0].text).toContain('scheme is required');
 59 |       expect(result.content[0].text).toContain('session-set-defaults');
 60 |     });
 61 | 
 62 |     it('should require project or workspace once scheme default exists', async () => {
 63 |       sessionStore.setDefaults({ scheme: 'MyScheme' });
 64 | 
 65 |       const result = await buildMacOS.handler({});
 66 | 
 67 |       expect(result.isError).toBe(true);
 68 |       expect(result.content[0].text).toContain('Provide a project or workspace');
 69 |     });
 70 | 
 71 |     it('should reject when both projectPath and workspacePath provided explicitly', async () => {
 72 |       sessionStore.setDefaults({ scheme: 'MyScheme' });
 73 | 
 74 |       const result = await buildMacOS.handler({
 75 |         projectPath: '/path/to/project.xcodeproj',
 76 |         workspacePath: '/path/to/workspace.xcworkspace',
 77 |       });
 78 | 
 79 |       expect(result.isError).toBe(true);
 80 |       expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
 81 |       expect(result.content[0].text).toContain('projectPath');
 82 |       expect(result.content[0].text).toContain('workspacePath');
 83 |     });
 84 |   });
 85 | 
 86 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 87 |     it('should return exact successful build response', async () => {
 88 |       const mockExecutor = createMockExecutor({
 89 |         success: true,
 90 |         output: 'BUILD SUCCEEDED',
 91 |       });
 92 | 
 93 |       const result = await buildMacOSLogic(
 94 |         {
 95 |           projectPath: '/path/to/MyProject.xcodeproj',
 96 |           scheme: 'MyScheme',
 97 |         },
 98 |         mockExecutor,
 99 |       );
100 | 
101 |       expect(result).toEqual({
102 |         content: [
103 |           {
104 |             type: 'text',
105 |             text: '✅ macOS Build build succeeded for scheme MyScheme.',
106 |           },
107 |           {
108 |             type: 'text',
109 |             text: "Next Steps:\n1. Get app path: get_mac_app_path({ scheme: 'MyScheme' })\n2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })\n3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })",
110 |           },
111 |         ],
112 |       });
113 |     });
114 | 
115 |     it('should return exact build failure response', async () => {
116 |       const mockExecutor = createMockExecutor({
117 |         success: false,
118 |         error: 'error: Compilation error in main.swift',
119 |       });
120 | 
121 |       const result = await buildMacOSLogic(
122 |         {
123 |           projectPath: '/path/to/MyProject.xcodeproj',
124 |           scheme: 'MyScheme',
125 |         },
126 |         mockExecutor,
127 |       );
128 | 
129 |       expect(result).toEqual({
130 |         content: [
131 |           {
132 |             type: 'text',
133 |             text: '❌ [stderr] error: Compilation error in main.swift',
134 |           },
135 |           {
136 |             type: 'text',
137 |             text: '❌ macOS Build build failed for scheme MyScheme.',
138 |           },
139 |         ],
140 |         isError: true,
141 |       });
142 |     });
143 | 
144 |     it('should return exact successful build response with optional parameters', async () => {
145 |       const mockExecutor = createMockExecutor({
146 |         success: true,
147 |         output: 'BUILD SUCCEEDED',
148 |       });
149 | 
150 |       const result = await buildMacOSLogic(
151 |         {
152 |           projectPath: '/path/to/MyProject.xcodeproj',
153 |           scheme: 'MyScheme',
154 |           configuration: 'Release',
155 |           arch: 'arm64',
156 |           derivedDataPath: '/path/to/derived-data',
157 |           extraArgs: ['--verbose'],
158 |           preferXcodebuild: true,
159 |         },
160 |         mockExecutor,
161 |       );
162 | 
163 |       expect(result).toEqual({
164 |         content: [
165 |           {
166 |             type: 'text',
167 |             text: '✅ macOS Build build succeeded for scheme MyScheme.',
168 |           },
169 |           {
170 |             type: 'text',
171 |             text: "Next Steps:\n1. Get app path: get_mac_app_path({ scheme: 'MyScheme' })\n2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })\n3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })",
172 |           },
173 |         ],
174 |       });
175 |     });
176 | 
177 |     it('should return exact exception handling response', async () => {
178 |       // Create executor that throws error during command execution
179 |       // This will be caught by executeXcodeBuildCommand's try-catch block
180 |       const mockExecutor = async () => {
181 |         throw new Error('Network error');
182 |       };
183 | 
184 |       const result = await buildMacOSLogic(
185 |         {
186 |           projectPath: '/path/to/MyProject.xcodeproj',
187 |           scheme: 'MyScheme',
188 |         },
189 |         mockExecutor,
190 |       );
191 | 
192 |       expect(result).toEqual({
193 |         content: [
194 |           {
195 |             type: 'text',
196 |             text: 'Error during macOS Build build: Network error',
197 |           },
198 |         ],
199 |         isError: true,
200 |       });
201 |     });
202 | 
203 |     it('should return exact spawn error handling response', async () => {
204 |       // Create executor that throws spawn error during command execution
205 |       // This will be caught by executeXcodeBuildCommand's try-catch block
206 |       const mockExecutor = async () => {
207 |         throw new Error('Spawn error');
208 |       };
209 | 
210 |       const result = await buildMacOSLogic(
211 |         {
212 |           projectPath: '/path/to/MyProject.xcodeproj',
213 |           scheme: 'MyScheme',
214 |         },
215 |         mockExecutor,
216 |       );
217 | 
218 |       expect(result).toEqual({
219 |         content: [
220 |           {
221 |             type: 'text',
222 |             text: 'Error during macOS Build build: Spawn error',
223 |           },
224 |         ],
225 |         isError: true,
226 |       });
227 |     });
228 |   });
229 | 
230 |   describe('Command Generation', () => {
231 |     it('should generate correct xcodebuild command with minimal parameters', async () => {
232 |       let capturedCommand: string[] = [];
233 |       const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });
234 | 
235 |       // Override the executor to capture the command
236 |       const spyExecutor = async (command: string[]) => {
237 |         capturedCommand = command;
238 |         return mockExecutor(command);
239 |       };
240 | 
241 |       const result = await buildMacOSLogic(
242 |         {
243 |           projectPath: '/path/to/project.xcodeproj',
244 |           scheme: 'MyScheme',
245 |         },
246 |         spyExecutor,
247 |       );
248 | 
249 |       expect(capturedCommand).toEqual([
250 |         'xcodebuild',
251 |         '-project',
252 |         '/path/to/project.xcodeproj',
253 |         '-scheme',
254 |         'MyScheme',
255 |         '-configuration',
256 |         'Debug',
257 |         '-skipMacroValidation',
258 |         '-destination',
259 |         'platform=macOS',
260 |         'build',
261 |       ]);
262 |     });
263 | 
264 |     it('should generate correct xcodebuild command with all parameters', async () => {
265 |       let capturedCommand: string[] = [];
266 |       const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });
267 | 
268 |       // Override the executor to capture the command
269 |       const spyExecutor = async (command: string[]) => {
270 |         capturedCommand = command;
271 |         return mockExecutor(command);
272 |       };
273 | 
274 |       const result = await buildMacOSLogic(
275 |         {
276 |           projectPath: '/path/to/project.xcodeproj',
277 |           scheme: 'MyScheme',
278 |           configuration: 'Release',
279 |           arch: 'x86_64',
280 |           derivedDataPath: '/custom/derived',
281 |           extraArgs: ['--verbose'],
282 |           preferXcodebuild: true,
283 |         },
284 |         spyExecutor,
285 |       );
286 | 
287 |       expect(capturedCommand).toEqual([
288 |         'xcodebuild',
289 |         '-project',
290 |         '/path/to/project.xcodeproj',
291 |         '-scheme',
292 |         'MyScheme',
293 |         '-configuration',
294 |         'Release',
295 |         '-skipMacroValidation',
296 |         '-destination',
297 |         'platform=macOS,arch=x86_64',
298 |         '-derivedDataPath',
299 |         '/custom/derived',
300 |         '--verbose',
301 |         'build',
302 |       ]);
303 |     });
304 | 
305 |     it('should generate correct xcodebuild command with only derivedDataPath', async () => {
306 |       let capturedCommand: string[] = [];
307 |       const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });
308 | 
309 |       // Override the executor to capture the command
310 |       const spyExecutor = async (command: string[]) => {
311 |         capturedCommand = command;
312 |         return mockExecutor(command);
313 |       };
314 | 
315 |       const result = await buildMacOSLogic(
316 |         {
317 |           projectPath: '/path/to/project.xcodeproj',
318 |           scheme: 'MyScheme',
319 |           derivedDataPath: '/custom/derived/data',
320 |         },
321 |         spyExecutor,
322 |       );
323 | 
324 |       expect(capturedCommand).toEqual([
325 |         'xcodebuild',
326 |         '-project',
327 |         '/path/to/project.xcodeproj',
328 |         '-scheme',
329 |         'MyScheme',
330 |         '-configuration',
331 |         'Debug',
332 |         '-skipMacroValidation',
333 |         '-destination',
334 |         'platform=macOS',
335 |         '-derivedDataPath',
336 |         '/custom/derived/data',
337 |         'build',
338 |       ]);
339 |     });
340 | 
341 |     it('should generate correct xcodebuild command with arm64 architecture only', async () => {
342 |       let capturedCommand: string[] = [];
343 |       const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });
344 | 
345 |       // Override the executor to capture the command
346 |       const spyExecutor = async (command: string[]) => {
347 |         capturedCommand = command;
348 |         return mockExecutor(command);
349 |       };
350 | 
351 |       const result = await buildMacOSLogic(
352 |         {
353 |           projectPath: '/path/to/project.xcodeproj',
354 |           scheme: 'MyScheme',
355 |           arch: 'arm64',
356 |         },
357 |         spyExecutor,
358 |       );
359 | 
360 |       expect(capturedCommand).toEqual([
361 |         'xcodebuild',
362 |         '-project',
363 |         '/path/to/project.xcodeproj',
364 |         '-scheme',
365 |         'MyScheme',
366 |         '-configuration',
367 |         'Debug',
368 |         '-skipMacroValidation',
369 |         '-destination',
370 |         'platform=macOS,arch=arm64',
371 |         'build',
372 |       ]);
373 |     });
374 | 
375 |     it('should handle paths with spaces in command generation', async () => {
376 |       let capturedCommand: string[] = [];
377 |       const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });
378 | 
379 |       // Override the executor to capture the command
380 |       const spyExecutor = async (command: string[]) => {
381 |         capturedCommand = command;
382 |         return mockExecutor(command);
383 |       };
384 | 
385 |       const result = await buildMacOSLogic(
386 |         {
387 |           projectPath: '/Users/dev/My Project/MyProject.xcodeproj',
388 |           scheme: 'MyScheme',
389 |         },
390 |         spyExecutor,
391 |       );
392 | 
393 |       expect(capturedCommand).toEqual([
394 |         'xcodebuild',
395 |         '-project',
396 |         '/Users/dev/My Project/MyProject.xcodeproj',
397 |         '-scheme',
398 |         'MyScheme',
399 |         '-configuration',
400 |         'Debug',
401 |         '-skipMacroValidation',
402 |         '-destination',
403 |         'platform=macOS',
404 |         'build',
405 |       ]);
406 |     });
407 | 
408 |     it('should generate correct xcodebuild workspace command with minimal parameters', async () => {
409 |       let capturedCommand: string[] = [];
410 |       const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });
411 | 
412 |       // Override the executor to capture the command
413 |       const spyExecutor = async (command: string[]) => {
414 |         capturedCommand = command;
415 |         return mockExecutor(command);
416 |       };
417 | 
418 |       const result = await buildMacOSLogic(
419 |         {
420 |           workspacePath: '/path/to/workspace.xcworkspace',
421 |           scheme: 'MyScheme',
422 |         },
423 |         spyExecutor,
424 |       );
425 | 
426 |       expect(capturedCommand).toEqual([
427 |         'xcodebuild',
428 |         '-workspace',
429 |         '/path/to/workspace.xcworkspace',
430 |         '-scheme',
431 |         'MyScheme',
432 |         '-configuration',
433 |         'Debug',
434 |         '-skipMacroValidation',
435 |         '-destination',
436 |         'platform=macOS',
437 |         'build',
438 |       ]);
439 |     });
440 |   });
441 | 
442 |   describe('XOR Validation', () => {
443 |     it('should error when neither projectPath nor workspacePath provided', async () => {
444 |       const result = await buildMacOS.handler({ scheme: 'MyScheme' });
445 |       expect(result.isError).toBe(true);
446 |       expect(result.content[0].text).toContain('Provide a project or workspace');
447 |     });
448 | 
449 |     it('should error when both projectPath and workspacePath provided', async () => {
450 |       const result = await buildMacOS.handler({
451 |         projectPath: '/path/to/project.xcodeproj',
452 |         workspacePath: '/path/to/workspace.xcworkspace',
453 |         scheme: 'MyScheme',
454 |       });
455 |       expect(result.isError).toBe(true);
456 |       expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
457 |     });
458 | 
459 |     it('should succeed with valid projectPath', async () => {
460 |       const mockExecutor = createMockExecutor({
461 |         success: true,
462 |         output: 'BUILD SUCCEEDED',
463 |       });
464 | 
465 |       const result = await buildMacOSLogic(
466 |         {
467 |           projectPath: '/path/to/project.xcodeproj',
468 |           scheme: 'MyScheme',
469 |         },
470 |         mockExecutor,
471 |       );
472 | 
473 |       expect(result.isError).toBeUndefined();
474 |     });
475 | 
476 |     it('should succeed with valid workspacePath', async () => {
477 |       const mockExecutor = createMockExecutor({
478 |         success: true,
479 |         output: 'BUILD SUCCEEDED',
480 |       });
481 | 
482 |       const result = await buildMacOSLogic(
483 |         {
484 |           workspacePath: '/path/to/workspace.xcworkspace',
485 |           scheme: 'MyScheme',
486 |         },
487 |         mockExecutor,
488 |       );
489 | 
490 |       expect(result.isError).toBeUndefined();
491 |     });
492 |   });
493 | });
494 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/discovery/discover_tools.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from 'zod';
  2 | import { createTextResponse } from '../../../utils/responses/index.ts';
  3 | import { log } from '../../../utils/logging/index.ts';
  4 | // Removed CreateMessageResultSchema import as it's no longer used
  5 | import { ToolResponse } from '../../../types/common.ts';
  6 | import {
  7 |   enableWorkflows,
  8 |   getAvailableWorkflows,
  9 |   generateWorkflowDescriptions,
 10 | } from '../../../core/dynamic-tools.ts';
 11 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
 12 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
 13 | import { McpServer } from '@camsoft/mcp-sdk/server/mcp.js';
 14 | 
 15 | // Using McpServer type from SDK instead of custom interface
 16 | 
 17 | // Configuration for LLM parameters - made configurable instead of hardcoded
 18 | interface LLMConfig {
 19 |   maxTokens: number;
 20 |   temperature?: number;
 21 | }
 22 | 
 23 | // Default LLM configuration with environment variable overrides
 24 | const getLLMConfig = (): LLMConfig => {
 25 |   let maxTokens = 200; // default
 26 |   if (process.env.XCODEBUILDMCP_LLM_MAX_TOKENS) {
 27 |     const parsed = parseInt(process.env.XCODEBUILDMCP_LLM_MAX_TOKENS, 10);
 28 |     if (!isNaN(parsed) && parsed > 0) {
 29 |       maxTokens = parsed;
 30 |     }
 31 |   }
 32 | 
 33 |   let temperature: number | undefined;
 34 |   if (process.env.XCODEBUILDMCP_LLM_TEMPERATURE) {
 35 |     const parsed = parseFloat(process.env.XCODEBUILDMCP_LLM_TEMPERATURE);
 36 |     if (!isNaN(parsed) && parsed >= 0 && parsed <= 2) {
 37 |       temperature = parsed;
 38 |     }
 39 |   }
 40 | 
 41 |   return {
 42 |     maxTokens,
 43 |     temperature,
 44 |   };
 45 | };
 46 | 
 47 | /**
 48 |  * Sanitizes user input to prevent injection attacks and ensure safe LLM usage
 49 |  * @param input The raw user input to sanitize
 50 |  * @returns Sanitized input safe for LLM processing
 51 |  */
 52 | function sanitizeTaskDescription(input: string): string {
 53 |   if (!input || typeof input !== 'string') {
 54 |     throw new Error('Task description must be a non-empty string');
 55 |   }
 56 | 
 57 |   // Remove control characters and normalize whitespace
 58 |   let sanitized = input
 59 |     // eslint-disable-next-line no-control-regex -- Intentional control character removal for security
 60 |     .replace(/[\x00-\x1F\x7F-\x9F]/g, '') // Remove control characters
 61 |     .replace(/\s+/g, ' ') // Normalize whitespace
 62 |     .trim();
 63 | 
 64 |   // Length validation - prevent excessively long inputs
 65 |   if (sanitized.length === 0) {
 66 |     throw new Error('Task description cannot be empty after sanitization');
 67 |   }
 68 | 
 69 |   if (sanitized.length > 2000) {
 70 |     sanitized = sanitized.substring(0, 2000);
 71 |     log('warn', 'Task description truncated to 2000 characters for safety');
 72 |   }
 73 | 
 74 |   // Basic injection prevention - remove potential prompt injection patterns
 75 |   const suspiciousPatterns = [
 76 |     /ignore\s+previous\s+instructions/gi,
 77 |     /forget\s+everything/gi,
 78 |     /system\s*:/gi,
 79 |     /assistant\s*:/gi,
 80 |     /you\s+are\s+now/gi,
 81 |     /act\s+as/gi,
 82 |   ];
 83 | 
 84 |   for (const pattern of suspiciousPatterns) {
 85 |     if (pattern.test(sanitized)) {
 86 |       log('warn', 'Potentially suspicious pattern detected in task description');
 87 |       sanitized = sanitized.replace(pattern, '[filtered]');
 88 |     }
 89 |   }
 90 | 
 91 |   return sanitized;
 92 | }
 93 | 
 94 | // Define schema as ZodObject
 95 | const discoverToolsSchema = z.object({
 96 |   task_description: z
 97 |     .string()
 98 |     .describe(
 99 |       'A detailed description of the development task you want to accomplish. ' +
100 |         "For example: 'I need to build my iOS app and run it on the iPhone 16 simulator.' " +
101 |         'If working with Xcode projects, explicitly state whether you are using a .xcworkspace (workspace) or a .xcodeproj (project).',
102 |     ),
103 |   additive: z
104 |     .boolean()
105 |     .optional()
106 |     .describe(
107 |       'If true, add the discovered tools to existing enabled workflows. ' +
108 |         'If false (default), replace all existing workflows with the newly discovered one. ' +
109 |         'Use additive mode when you need tools from multiple workflows simultaneously.',
110 |     ),
111 | });
112 | 
113 | // Use z.infer for type safety
114 | type DiscoverToolsParams = z.infer<typeof discoverToolsSchema>;
115 | 
116 | // Dependencies interface for dependency injection
117 | interface Dependencies {
118 |   getAvailableWorkflows?: () => string[];
119 |   generateWorkflowDescriptions?: () => string;
120 |   enableWorkflows?: (server: McpServer, workflows: string[], additive?: boolean) => Promise<void>;
121 | }
122 | 
123 | export async function discover_toolsLogic(
124 |   args: DiscoverToolsParams,
125 |   _executor?: unknown,
126 |   deps?: Dependencies,
127 | ): Promise<ToolResponse> {
128 |   // Enhanced null safety checks
129 |   if (!args || typeof args !== 'object') {
130 |     return createTextResponse('Invalid arguments provided to discover_tools', true);
131 |   }
132 | 
133 |   const { task_description, additive } = args;
134 | 
135 |   // Sanitize the task description to prevent injection attacks
136 |   let sanitizedTaskDescription: string;
137 |   try {
138 |     sanitizedTaskDescription = sanitizeTaskDescription(task_description);
139 |     log('info', `Discovering tools for task: ${sanitizedTaskDescription}`);
140 |   } catch (error) {
141 |     const errorMessage = error instanceof Error ? error.message : 'Invalid task description';
142 |     log('error', `Task description sanitization failed: ${errorMessage}`);
143 |     return createTextResponse(`Invalid task description: ${errorMessage}`, true);
144 |   }
145 | 
146 |   try {
147 |     // Get the server instance from the global context
148 |     const server = (globalThis as { mcpServer?: McpServer }).mcpServer;
149 |     if (!server) {
150 |       throw new Error('Server instance not available');
151 |     }
152 | 
153 |     // 1. Check for sampling capability
154 |     const clientCapabilities = server.server?.getClientCapabilities?.();
155 |     if (!clientCapabilities?.sampling) {
156 |       log('warn', 'Client does not support sampling capability');
157 |       return createTextResponse(
158 |         'Your client does not support the sampling feature required for dynamic tool discovery. ' +
159 |           'Please use XCODEBUILDMCP_DYNAMIC_TOOLS=false to use the standard tool set.',
160 |         true,
161 |       );
162 |     }
163 | 
164 |     // 2. Get available workflows using generated metadata
165 |     const workflowNames = (deps?.getAvailableWorkflows ?? getAvailableWorkflows)();
166 |     const workflowDescriptions = (
167 |       deps?.generateWorkflowDescriptions ?? generateWorkflowDescriptions
168 |     )();
169 | 
170 |     // 3. Construct the prompt for the LLM using sanitized input
171 |     const userPrompt = `You are an expert assistant for the XcodeBuildMCP server. Your task is to select the most relevant workflow for a user's Apple development request.
172 | 
173 | The user wants to perform the following task: "${sanitizedTaskDescription}"
174 | 
175 | IMPORTANT: Select EXACTLY ONE workflow that best matches the user's task. In most cases, users are working with a project or workspace. Use this selection guide:
176 | 
177 | Primary (project/workspace-based) workflows:
178 | - iOS simulator (supports both .xcworkspace and .xcodeproj): choose "simulator"
179 | - iOS physical device (supports both .xcworkspace and .xcodeproj): choose "device"
180 | - macOS (supports both .xcworkspace and .xcodeproj): choose "macos"
181 | - Swift Package Manager (no Xcode project): choose "swift-package"
182 | 
183 | Secondary (task-based, no project/workspace needed):
184 | - Simulator management (boot, list, open, status bar, appearance, GPS/location): choose "simulator-management"
185 | - Logging or log capture (simulator or device): choose "logging"
186 | - UI automation/gestures/screenshots on a simulator app: choose "ui-testing"
187 | - System/environment diagnostics or validation: choose "doctor"
188 | - Create new iOS/macOS projects from templates: choose "project-scaffolding"
189 | - Project discovery and analysis: choose "project-discovery"
190 | - General utilities: choose "utilities"
191 | 
192 | All available workflows:
193 | ${workflowDescriptions}
194 | 
195 | Respond with ONLY a JSON array containing ONE workflow name that best matches the task (e.g., ["simulator"]).`;
196 | 
197 |     // 4. Send sampling request with configurable parameters
198 |     const llmConfig = getLLMConfig();
199 |     log('debug', `Sending sampling request to client LLM with maxTokens: ${llmConfig.maxTokens}`);
200 |     if (!server.server?.createMessage) {
201 |       throw new Error('Server does not support message creation');
202 |     }
203 | 
204 |     const samplingOptions: {
205 |       messages: Array<{ role: 'user'; content: { type: 'text'; text: string } }>;
206 |       maxTokens: number;
207 |       temperature?: number;
208 |     } = {
209 |       messages: [{ role: 'user', content: { type: 'text', text: userPrompt } }],
210 |       maxTokens: llmConfig.maxTokens,
211 |     };
212 | 
213 |     // Only add temperature if configured
214 |     if (llmConfig.temperature !== undefined) {
215 |       samplingOptions.temperature = llmConfig.temperature;
216 |     }
217 | 
218 |     const samplingResult = await server.server.createMessage(samplingOptions);
219 | 
220 |     // 5. Parse the response with enhanced null safety checks
221 |     let selectedWorkflows: string[] = [];
222 |     try {
223 |       // Enhanced null safety - check if samplingResult exists and has expected structure
224 |       if (!samplingResult || typeof samplingResult !== 'object') {
225 |         throw new Error('Invalid sampling result: null or not an object');
226 |       }
227 | 
228 |       const content = (
229 |         samplingResult as {
230 |           content?: Array<{ type: 'text'; text: string }> | { type: 'text'; text: string } | null;
231 |         }
232 |       ).content;
233 | 
234 |       if (!content) {
235 |         throw new Error('No content in sampling response');
236 |       }
237 | 
238 |       let responseText = '';
239 | 
240 |       // Handle both array and single object content formats with enhanced null checks
241 |       if (Array.isArray(content)) {
242 |         if (content.length === 0) {
243 |           throw new Error('Empty content array in sampling response');
244 |         }
245 |         const firstItem = content[0];
246 |         if (!firstItem || typeof firstItem !== 'object' || firstItem.type !== 'text') {
247 |           throw new Error('Invalid first content item in array');
248 |         }
249 |         if (!firstItem.text || typeof firstItem.text !== 'string') {
250 |           throw new Error('Invalid text content in first array item');
251 |         }
252 |         responseText = firstItem.text.trim();
253 |       } else if (
254 |         content &&
255 |         typeof content === 'object' &&
256 |         'type' in content &&
257 |         content.type === 'text' &&
258 |         'text' in content &&
259 |         typeof content.text === 'string'
260 |       ) {
261 |         responseText = content.text.trim();
262 |       } else {
263 |         throw new Error('Invalid content format in sampling response');
264 |       }
265 | 
266 |       if (!responseText) {
267 |         throw new Error('Empty response text after parsing');
268 |       }
269 | 
270 |       log('debug', `LLM response: ${responseText}`);
271 | 
272 |       const parsedResponse: unknown = JSON.parse(responseText);
273 | 
274 |       if (!Array.isArray(parsedResponse)) {
275 |         throw new Error('Response is not an array');
276 |       }
277 | 
278 |       // Validate that all items are strings
279 |       if (!parsedResponse.every((item): item is string => typeof item === 'string')) {
280 |         throw new Error('Response array contains non-string items');
281 |       }
282 | 
283 |       selectedWorkflows = parsedResponse;
284 | 
285 |       // Validate that all selected workflows are valid
286 |       const validWorkflows = selectedWorkflows.filter((workflow) =>
287 |         workflowNames.includes(workflow),
288 |       );
289 |       if (validWorkflows.length !== selectedWorkflows.length) {
290 |         const invalidWorkflows = selectedWorkflows.filter(
291 |           (workflow) => !workflowNames.includes(workflow),
292 |         );
293 |         log('warn', `LLM selected invalid workflows: ${invalidWorkflows.join(', ')}`);
294 |         selectedWorkflows = validWorkflows;
295 |       }
296 |     } catch (error) {
297 |       log('error', `Failed to parse LLM response: ${error}`);
298 |       // Extract the response text for error reporting with enhanced null safety
299 |       let errorResponseText = 'Unknown response format';
300 |       try {
301 |         if (samplingResult && typeof samplingResult === 'object') {
302 |           const content = (
303 |             samplingResult as {
304 |               content?:
305 |                 | Array<{ type: 'text'; text: string }>
306 |                 | { type: 'text'; text: string }
307 |                 | null;
308 |             }
309 |           ).content;
310 | 
311 |           if (content && Array.isArray(content) && content.length > 0) {
312 |             const firstItem = content[0];
313 |             if (
314 |               firstItem &&
315 |               typeof firstItem === 'object' &&
316 |               firstItem.type === 'text' &&
317 |               typeof firstItem.text === 'string'
318 |             ) {
319 |               errorResponseText = firstItem.text;
320 |             }
321 |           } else if (
322 |             content &&
323 |             typeof content === 'object' &&
324 |             'type' in content &&
325 |             content.type === 'text' &&
326 |             'text' in content &&
327 |             typeof content.text === 'string'
328 |           ) {
329 |             errorResponseText = content.text;
330 |           }
331 |         }
332 |       } catch {
333 |         // Keep default error message
334 |       }
335 | 
336 |       return createTextResponse(
337 |         `I was unable to determine the right tools for your task. The AI model returned: "${errorResponseText}". ` +
338 |           `Could you please rephrase your request or try a more specific description?`,
339 |         true,
340 |       );
341 |     }
342 | 
343 |     // 6. Handle empty selection
344 |     if (selectedWorkflows.length === 0) {
345 |       log('info', 'LLM returned empty workflow selection');
346 |       return createTextResponse(
347 |         "No specific Xcode tools seem necessary for that task. Could you provide more details about what you'd like to accomplish with Xcode?",
348 |       );
349 |     }
350 | 
351 |     // 7. Enable the selected workflows
352 |     const isAdditive = Boolean(additive);
353 |     log(
354 |       'info',
355 |       `${isAdditive ? 'Adding' : 'Replacing with'} workflows: ${selectedWorkflows.join(', ')}`,
356 |     );
357 |     await (deps?.enableWorkflows ?? enableWorkflows)(server, selectedWorkflows, isAdditive);
358 | 
359 |     // 8. Return success response - we can't easily get tool count ahead of time with dynamic loading
360 |     // but that's okay since the user will see the tools when they're loaded
361 | 
362 |     const actionWord = isAdditive ? 'Added' : 'Enabled';
363 |     const modeDescription = isAdditive
364 |       ? `Added tools from ${selectedWorkflows.join(', ')} to your existing workflow tools.`
365 |       : `Replaced previous tools with ${selectedWorkflows.join(', ')} workflow tools.`;
366 | 
367 |     return createTextResponse(
368 |       `✅ ${actionWord} XcodeBuildMCP tools for: ${selectedWorkflows.join(', ')}.\n\n` +
369 |         `${modeDescription}\n\n` +
370 |         `Use XcodeBuildMCP tools for all Apple platform development tasks from now on. ` +
371 |         `Call tools/list to see all available tools for your workflow.`,
372 |     );
373 |   } catch (error) {
374 |     log('error', `Error in discoverTools: ${error}`);
375 |     return createTextResponse(
376 |       `An error occurred while discovering tools: ${error instanceof Error ? error.message : 'Unknown error'}`,
377 |       true,
378 |     );
379 |   }
380 | }
381 | 
382 | export default {
383 |   name: 'discover_tools',
384 |   description:
385 |     'Analyzes a natural language task description and enables the most relevant development workflow. Prioritizes project/workspace workflows (simulator/device/macOS) and also supports task-based workflows (simulator-management, logging) and Swift packages.',
386 |   schema: discoverToolsSchema.shape, // MCP SDK compatibility
387 |   handler: createTypedTool(
388 |     discoverToolsSchema,
389 |     (params: DiscoverToolsParams, executor) => {
390 |       return discover_toolsLogic(params, executor);
391 |     },
392 |     getDefaultCommandExecutor,
393 |   ),
394 | };
395 | 
```

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

```typescript
  1 | import { describe, it, expect, beforeEach } from 'vitest';
  2 | import { z } from 'zod';
  3 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
  4 | import { sessionStore } from '../../../../utils/session-store.ts';
  5 | import tool, { buildRunMacOSLogic } from '../build_run_macos.ts';
  6 | 
  7 | describe('build_run_macos', () => {
  8 |   beforeEach(() => {
  9 |     sessionStore.clear();
 10 |   });
 11 | 
 12 |   describe('Export Field Validation (Literal)', () => {
 13 |     it('should export the correct name', () => {
 14 |       expect(tool.name).toBe('build_run_macos');
 15 |     });
 16 | 
 17 |     it('should export the correct description', () => {
 18 |       expect(tool.description).toBe('Builds and runs a macOS app.');
 19 |     });
 20 | 
 21 |     it('should export a handler function', () => {
 22 |       expect(typeof tool.handler).toBe('function');
 23 |     });
 24 | 
 25 |     it('should expose only non-session fields in schema', () => {
 26 |       const schema = z.object(tool.schema);
 27 | 
 28 |       expect(schema.safeParse({}).success).toBe(true);
 29 |       expect(
 30 |         schema.safeParse({
 31 |           derivedDataPath: '/tmp/derived',
 32 |           extraArgs: ['--verbose'],
 33 |           preferXcodebuild: true,
 34 |         }).success,
 35 |       ).toBe(true);
 36 | 
 37 |       expect(schema.safeParse({ derivedDataPath: 1 }).success).toBe(false);
 38 |       expect(schema.safeParse({ extraArgs: ['--ok', 2] }).success).toBe(false);
 39 |       expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false);
 40 | 
 41 |       const schemaKeys = Object.keys(tool.schema).sort();
 42 |       expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs', 'preferXcodebuild'].sort());
 43 |     });
 44 |   });
 45 | 
 46 |   describe('Handler Requirements', () => {
 47 |     it('should require scheme before executing', async () => {
 48 |       const result = await tool.handler({});
 49 | 
 50 |       expect(result.isError).toBe(true);
 51 |       expect(result.content[0].text).toContain('scheme is required');
 52 |     });
 53 | 
 54 |     it('should require project or workspace once scheme is set', async () => {
 55 |       sessionStore.setDefaults({ scheme: 'MyApp' });
 56 | 
 57 |       const result = await tool.handler({});
 58 | 
 59 |       expect(result.isError).toBe(true);
 60 |       expect(result.content[0].text).toContain('Provide a project or workspace');
 61 |     });
 62 | 
 63 |     it('should fail when both project and workspace provided explicitly', async () => {
 64 |       sessionStore.setDefaults({ scheme: 'MyApp' });
 65 | 
 66 |       const result = await tool.handler({
 67 |         projectPath: '/path/to/project.xcodeproj',
 68 |         workspacePath: '/path/to/workspace.xcworkspace',
 69 |       });
 70 | 
 71 |       expect(result.isError).toBe(true);
 72 |       expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
 73 |     });
 74 |   });
 75 | 
 76 |   describe('Command Generation and Response Logic', () => {
 77 |     it('should successfully build and run macOS app from project', async () => {
 78 |       // Track executor calls manually
 79 |       let callCount = 0;
 80 |       const executorCalls: any[] = [];
 81 |       const mockExecutor = (
 82 |         command: string[],
 83 |         description: string,
 84 |         logOutput: boolean,
 85 |         timeout?: number,
 86 |       ) => {
 87 |         callCount++;
 88 |         executorCalls.push({ command, description, logOutput, timeout });
 89 | 
 90 |         if (callCount === 1) {
 91 |           // First call for build
 92 |           return Promise.resolve({
 93 |             success: true,
 94 |             output: 'BUILD SUCCEEDED',
 95 |             error: '',
 96 |           });
 97 |         } else if (callCount === 2) {
 98 |           // Second call for build settings
 99 |           return Promise.resolve({
100 |             success: true,
101 |             output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app',
102 |             error: '',
103 |           });
104 |         }
105 |         return Promise.resolve({ success: true, output: '', error: '' });
106 |       };
107 | 
108 |       const args = {
109 |         projectPath: '/path/to/project.xcodeproj',
110 |         scheme: 'MyApp',
111 |         configuration: 'Debug',
112 |         preferXcodebuild: false,
113 |       };
114 | 
115 |       const result = await buildRunMacOSLogic(args, mockExecutor);
116 | 
117 |       // Verify build command was called
118 |       expect(executorCalls[0]).toEqual({
119 |         command: [
120 |           'xcodebuild',
121 |           '-project',
122 |           '/path/to/project.xcodeproj',
123 |           '-scheme',
124 |           'MyApp',
125 |           '-configuration',
126 |           'Debug',
127 |           '-skipMacroValidation',
128 |           '-destination',
129 |           'platform=macOS',
130 |           'build',
131 |         ],
132 |         description: 'macOS Build',
133 |         logOutput: true,
134 |         timeout: undefined,
135 |       });
136 | 
137 |       // Verify build settings command was called
138 |       expect(executorCalls[1]).toEqual({
139 |         command: [
140 |           'xcodebuild',
141 |           '-showBuildSettings',
142 |           '-project',
143 |           '/path/to/project.xcodeproj',
144 |           '-scheme',
145 |           'MyApp',
146 |           '-configuration',
147 |           'Debug',
148 |         ],
149 |         description: 'Get Build Settings for Launch',
150 |         logOutput: true,
151 |         timeout: undefined,
152 |       });
153 | 
154 |       expect(result).toEqual({
155 |         content: [
156 |           {
157 |             type: 'text',
158 |             text: '✅ macOS Build build succeeded for scheme MyApp.',
159 |           },
160 |           {
161 |             type: 'text',
162 |             text: "Next Steps:\n1. Get app path: get_mac_app_path({ scheme: 'MyApp' })\n2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })\n3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })",
163 |           },
164 |           {
165 |             type: 'text',
166 |             text: '✅ macOS build and run succeeded for scheme MyApp. App launched: /path/to/build/MyApp.app',
167 |           },
168 |         ],
169 |         isError: false,
170 |       });
171 |     });
172 | 
173 |     it('should successfully build and run macOS app from workspace', async () => {
174 |       // Track executor calls manually
175 |       let callCount = 0;
176 |       const executorCalls: any[] = [];
177 |       const mockExecutor = (
178 |         command: string[],
179 |         description: string,
180 |         logOutput: boolean,
181 |         timeout?: number,
182 |       ) => {
183 |         callCount++;
184 |         executorCalls.push({ command, description, logOutput, timeout });
185 | 
186 |         if (callCount === 1) {
187 |           // First call for build
188 |           return Promise.resolve({
189 |             success: true,
190 |             output: 'BUILD SUCCEEDED',
191 |             error: '',
192 |           });
193 |         } else if (callCount === 2) {
194 |           // Second call for build settings
195 |           return Promise.resolve({
196 |             success: true,
197 |             output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app',
198 |             error: '',
199 |           });
200 |         }
201 |         return Promise.resolve({ success: true, output: '', error: '' });
202 |       };
203 | 
204 |       const args = {
205 |         workspacePath: '/path/to/workspace.xcworkspace',
206 |         scheme: 'MyApp',
207 |         configuration: 'Debug',
208 |         preferXcodebuild: false,
209 |       };
210 | 
211 |       const result = await buildRunMacOSLogic(args, mockExecutor);
212 | 
213 |       // Verify build command was called
214 |       expect(executorCalls[0]).toEqual({
215 |         command: [
216 |           'xcodebuild',
217 |           '-workspace',
218 |           '/path/to/workspace.xcworkspace',
219 |           '-scheme',
220 |           'MyApp',
221 |           '-configuration',
222 |           'Debug',
223 |           '-skipMacroValidation',
224 |           '-destination',
225 |           'platform=macOS',
226 |           'build',
227 |         ],
228 |         description: 'macOS Build',
229 |         logOutput: true,
230 |         timeout: undefined,
231 |       });
232 | 
233 |       // Verify build settings command was called
234 |       expect(executorCalls[1]).toEqual({
235 |         command: [
236 |           'xcodebuild',
237 |           '-showBuildSettings',
238 |           '-workspace',
239 |           '/path/to/workspace.xcworkspace',
240 |           '-scheme',
241 |           'MyApp',
242 |           '-configuration',
243 |           'Debug',
244 |         ],
245 |         description: 'Get Build Settings for Launch',
246 |         logOutput: true,
247 |         timeout: undefined,
248 |       });
249 | 
250 |       expect(result).toEqual({
251 |         content: [
252 |           {
253 |             type: 'text',
254 |             text: '✅ macOS Build build succeeded for scheme MyApp.',
255 |           },
256 |           {
257 |             type: 'text',
258 |             text: "Next Steps:\n1. Get app path: get_mac_app_path({ scheme: 'MyApp' })\n2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })\n3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })",
259 |           },
260 |           {
261 |             type: 'text',
262 |             text: '✅ macOS build and run succeeded for scheme MyApp. App launched: /path/to/build/MyApp.app',
263 |           },
264 |         ],
265 |         isError: false,
266 |       });
267 |     });
268 | 
269 |     it('should handle build failure', async () => {
270 |       const mockExecutor = createMockExecutor({
271 |         success: false,
272 |         output: '',
273 |         error: 'error: Build failed',
274 |       });
275 | 
276 |       const args = {
277 |         projectPath: '/path/to/project.xcodeproj',
278 |         scheme: 'MyApp',
279 |         configuration: 'Debug',
280 |         preferXcodebuild: false,
281 |       };
282 | 
283 |       const result = await buildRunMacOSLogic(args, mockExecutor);
284 | 
285 |       expect(result).toEqual({
286 |         content: [
287 |           { type: 'text', text: '❌ [stderr] error: Build failed' },
288 |           { type: 'text', text: '❌ macOS Build build failed for scheme MyApp.' },
289 |         ],
290 |         isError: true,
291 |       });
292 |     });
293 | 
294 |     it('should handle build settings failure', async () => {
295 |       // Track executor calls manually
296 |       let callCount = 0;
297 |       const mockExecutor = (
298 |         command: string[],
299 |         description: string,
300 |         logOutput: boolean,
301 |         timeout?: number,
302 |       ) => {
303 |         callCount++;
304 |         if (callCount === 1) {
305 |           // First call for build succeeds
306 |           return Promise.resolve({
307 |             success: true,
308 |             output: 'BUILD SUCCEEDED',
309 |             error: '',
310 |           });
311 |         } else if (callCount === 2) {
312 |           // Second call for build settings fails
313 |           return Promise.resolve({
314 |             success: false,
315 |             output: '',
316 |             error: 'error: Failed to get settings',
317 |           });
318 |         }
319 |         return Promise.resolve({ success: true, output: '', error: '' });
320 |       };
321 | 
322 |       const args = {
323 |         projectPath: '/path/to/project.xcodeproj',
324 |         scheme: 'MyApp',
325 |         configuration: 'Debug',
326 |         preferXcodebuild: false,
327 |       };
328 | 
329 |       const result = await buildRunMacOSLogic(args, mockExecutor);
330 | 
331 |       expect(result).toEqual({
332 |         content: [
333 |           {
334 |             type: 'text',
335 |             text: '✅ macOS Build build succeeded for scheme MyApp.',
336 |           },
337 |           {
338 |             type: 'text',
339 |             text: "Next Steps:\n1. Get app path: get_mac_app_path({ scheme: 'MyApp' })\n2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })\n3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })",
340 |           },
341 |           {
342 |             type: 'text',
343 |             text: '✅ Build succeeded, but failed to get app path to launch: error: Failed to get settings',
344 |           },
345 |         ],
346 |         isError: false,
347 |       });
348 |     });
349 | 
350 |     it('should handle app launch failure', async () => {
351 |       // Track executor calls manually
352 |       let callCount = 0;
353 |       const mockExecutor = (
354 |         command: string[],
355 |         description: string,
356 |         logOutput: boolean,
357 |         timeout?: number,
358 |       ) => {
359 |         callCount++;
360 |         if (callCount === 1) {
361 |           // First call for build succeeds
362 |           return Promise.resolve({
363 |             success: true,
364 |             output: 'BUILD SUCCEEDED',
365 |             error: '',
366 |           });
367 |         } else if (callCount === 2) {
368 |           // Second call for build settings succeeds
369 |           return Promise.resolve({
370 |             success: true,
371 |             output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app',
372 |             error: '',
373 |           });
374 |         } else if (callCount === 3) {
375 |           // Third call for open command fails
376 |           return Promise.resolve({
377 |             success: false,
378 |             output: '',
379 |             error: 'Failed to launch',
380 |           });
381 |         }
382 |         return Promise.resolve({ success: true, output: '', error: '' });
383 |       };
384 | 
385 |       const args = {
386 |         projectPath: '/path/to/project.xcodeproj',
387 |         scheme: 'MyApp',
388 |         configuration: 'Debug',
389 |         preferXcodebuild: false,
390 |       };
391 | 
392 |       const result = await buildRunMacOSLogic(args, mockExecutor);
393 | 
394 |       expect(result).toEqual({
395 |         content: [
396 |           {
397 |             type: 'text',
398 |             text: '✅ macOS Build build succeeded for scheme MyApp.',
399 |           },
400 |           {
401 |             type: 'text',
402 |             text: "Next Steps:\n1. Get app path: get_mac_app_path({ scheme: 'MyApp' })\n2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })\n3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })",
403 |           },
404 |           {
405 |             type: 'text',
406 |             text: '✅ Build succeeded, but failed to launch app /path/to/build/MyApp.app. Error: Failed to launch',
407 |           },
408 |         ],
409 |         isError: false,
410 |       });
411 |     });
412 | 
413 |     it('should handle spawn error', async () => {
414 |       const mockExecutor = (
415 |         command: string[],
416 |         description: string,
417 |         logOutput: boolean,
418 |         timeout?: number,
419 |       ) => {
420 |         return Promise.reject(new Error('spawn xcodebuild ENOENT'));
421 |       };
422 | 
423 |       const args = {
424 |         projectPath: '/path/to/project.xcodeproj',
425 |         scheme: 'MyApp',
426 |         configuration: 'Debug',
427 |         preferXcodebuild: false,
428 |       };
429 | 
430 |       const result = await buildRunMacOSLogic(args, mockExecutor);
431 | 
432 |       expect(result).toEqual({
433 |         content: [
434 |           { type: 'text', text: 'Error during macOS Build build: spawn xcodebuild ENOENT' },
435 |         ],
436 |         isError: true,
437 |       });
438 |     });
439 | 
440 |     it('should use default configuration when not provided', async () => {
441 |       // Track executor calls manually
442 |       let callCount = 0;
443 |       const executorCalls: any[] = [];
444 |       const mockExecutor = (
445 |         command: string[],
446 |         description: string,
447 |         logOutput: boolean,
448 |         timeout?: number,
449 |       ) => {
450 |         callCount++;
451 |         executorCalls.push({ command, description, logOutput, timeout });
452 | 
453 |         if (callCount === 1) {
454 |           // First call for build
455 |           return Promise.resolve({
456 |             success: true,
457 |             output: 'BUILD SUCCEEDED',
458 |             error: '',
459 |           });
460 |         } else if (callCount === 2) {
461 |           // Second call for build settings
462 |           return Promise.resolve({
463 |             success: true,
464 |             output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app',
465 |             error: '',
466 |           });
467 |         }
468 |         return Promise.resolve({ success: true, output: '', error: '' });
469 |       };
470 | 
471 |       const args = {
472 |         projectPath: '/path/to/project.xcodeproj',
473 |         scheme: 'MyApp',
474 |         configuration: 'Debug',
475 |         preferXcodebuild: false,
476 |       };
477 | 
478 |       await buildRunMacOSLogic(args, mockExecutor);
479 | 
480 |       expect(executorCalls[0]).toEqual({
481 |         command: [
482 |           'xcodebuild',
483 |           '-project',
484 |           '/path/to/project.xcodeproj',
485 |           '-scheme',
486 |           'MyApp',
487 |           '-configuration',
488 |           'Debug',
489 |           '-skipMacroValidation',
490 |           '-destination',
491 |           'platform=macOS',
492 |           'build',
493 |         ],
494 |         description: 'macOS Build',
495 |         logOutput: true,
496 |         timeout: undefined,
497 |       });
498 |     });
499 |   });
500 | });
501 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for get_mac_app_path plugin (unified project/workspace)
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using dependency injection for deterministic testing
  5 |  */
  6 | import { describe, it, expect, beforeEach } from 'vitest';
  7 | import { z } from 'zod';
  8 | import { createMockExecutor, type CommandExecutor } from '../../../../test-utils/mock-executors.ts';
  9 | import { sessionStore } from '../../../../utils/session-store.ts';
 10 | import getMacAppPath, { get_mac_app_pathLogic } from '../get_mac_app_path.ts';
 11 | 
 12 | describe('get_mac_app_path plugin', () => {
 13 |   beforeEach(() => {
 14 |     sessionStore.clear();
 15 |   });
 16 | 
 17 |   describe('Export Field Validation (Literal)', () => {
 18 |     it('should have correct name', () => {
 19 |       expect(getMacAppPath.name).toBe('get_mac_app_path');
 20 |     });
 21 | 
 22 |     it('should have correct description', () => {
 23 |       expect(getMacAppPath.description).toBe('Retrieves the built macOS app bundle path.');
 24 |     });
 25 | 
 26 |     it('should have handler function', () => {
 27 |       expect(typeof getMacAppPath.handler).toBe('function');
 28 |     });
 29 | 
 30 |     it('should validate schema correctly', () => {
 31 |       const schema = z.object(getMacAppPath.schema);
 32 | 
 33 |       expect(schema.safeParse({}).success).toBe(true);
 34 |       expect(
 35 |         schema.safeParse({
 36 |           derivedDataPath: '/path/to/derived',
 37 |           extraArgs: ['--verbose'],
 38 |         }).success,
 39 |       ).toBe(true);
 40 | 
 41 |       expect(schema.safeParse({ derivedDataPath: 7 }).success).toBe(false);
 42 |       expect(schema.safeParse({ extraArgs: ['--bad', 1] }).success).toBe(false);
 43 | 
 44 |       const schemaKeys = Object.keys(getMacAppPath.schema).sort();
 45 |       expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs'].sort());
 46 |     });
 47 |   });
 48 | 
 49 |   describe('Handler Requirements', () => {
 50 |     it('should require scheme before running', async () => {
 51 |       const result = await getMacAppPath.handler({});
 52 | 
 53 |       expect(result.isError).toBe(true);
 54 |       expect(result.content[0].text).toContain('scheme is required');
 55 |     });
 56 | 
 57 |     it('should require project or workspace when scheme default exists', async () => {
 58 |       sessionStore.setDefaults({ scheme: 'MyScheme' });
 59 | 
 60 |       const result = await getMacAppPath.handler({});
 61 | 
 62 |       expect(result.isError).toBe(true);
 63 |       expect(result.content[0].text).toContain('Provide a project or workspace');
 64 |     });
 65 | 
 66 |     it('should reject when both projectPath and workspacePath provided explicitly', async () => {
 67 |       sessionStore.setDefaults({ scheme: 'MyScheme' });
 68 | 
 69 |       const result = await getMacAppPath.handler({
 70 |         projectPath: '/path/to/project.xcodeproj',
 71 |         workspacePath: '/path/to/workspace.xcworkspace',
 72 |       });
 73 | 
 74 |       expect(result.isError).toBe(true);
 75 |       expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
 76 |     });
 77 |   });
 78 | 
 79 |   describe('XOR Validation', () => {
 80 |     it('should error when neither projectPath nor workspacePath provided', async () => {
 81 |       const result = await getMacAppPath.handler({
 82 |         scheme: 'MyScheme',
 83 |       });
 84 | 
 85 |       expect(result.isError).toBe(true);
 86 |       expect(result.content[0].text).toContain('Provide a project or workspace');
 87 |     });
 88 | 
 89 |     it('should error when both projectPath and workspacePath provided', async () => {
 90 |       const result = await getMacAppPath.handler({
 91 |         projectPath: '/path/to/project.xcodeproj',
 92 |         workspacePath: '/path/to/workspace.xcworkspace',
 93 |         scheme: 'MyScheme',
 94 |       });
 95 | 
 96 |       expect(result.isError).toBe(true);
 97 |       expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
 98 |     });
 99 |   });
100 | 
101 |   describe('Command Generation', () => {
102 |     it('should generate correct command with workspace minimal parameters', async () => {
103 |       // Manual call tracking for command verification
104 |       const calls: any[] = [];
105 |       const mockExecutor: CommandExecutor = async (...args) => {
106 |         calls.push(args);
107 |         return {
108 |           success: true,
109 |           output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app',
110 |           error: undefined,
111 |           process: { pid: 12345 },
112 |         };
113 |       };
114 | 
115 |       const args = {
116 |         workspacePath: '/path/to/MyProject.xcworkspace',
117 |         scheme: 'MyScheme',
118 |       };
119 | 
120 |       await get_mac_app_pathLogic(args, mockExecutor);
121 | 
122 |       // Verify command generation with manual call tracking
123 |       expect(calls).toHaveLength(1);
124 |       expect(calls[0]).toEqual([
125 |         [
126 |           'xcodebuild',
127 |           '-showBuildSettings',
128 |           '-workspace',
129 |           '/path/to/MyProject.xcworkspace',
130 |           '-scheme',
131 |           'MyScheme',
132 |           '-configuration',
133 |           'Debug',
134 |         ],
135 |         'Get App Path',
136 |         true,
137 |         undefined,
138 |       ]);
139 |     });
140 | 
141 |     it('should generate correct command with project minimal parameters', async () => {
142 |       // Manual call tracking for command verification
143 |       const calls: any[] = [];
144 |       const mockExecutor: CommandExecutor = async (...args) => {
145 |         calls.push(args);
146 |         return {
147 |           success: true,
148 |           output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app',
149 |           error: undefined,
150 |           process: { pid: 12345 },
151 |         };
152 |       };
153 | 
154 |       const args = {
155 |         projectPath: '/path/to/MyProject.xcodeproj',
156 |         scheme: 'MyScheme',
157 |       };
158 | 
159 |       await get_mac_app_pathLogic(args, mockExecutor);
160 | 
161 |       // Verify command generation with manual call tracking
162 |       expect(calls).toHaveLength(1);
163 |       expect(calls[0]).toEqual([
164 |         [
165 |           'xcodebuild',
166 |           '-showBuildSettings',
167 |           '-project',
168 |           '/path/to/MyProject.xcodeproj',
169 |           '-scheme',
170 |           'MyScheme',
171 |           '-configuration',
172 |           'Debug',
173 |         ],
174 |         'Get App Path',
175 |         true,
176 |         undefined,
177 |       ]);
178 |     });
179 | 
180 |     it('should generate correct command with workspace all parameters', async () => {
181 |       // Manual call tracking for command verification
182 |       const calls: any[] = [];
183 |       const mockExecutor: CommandExecutor = async (...args) => {
184 |         calls.push(args);
185 |         return {
186 |           success: true,
187 |           output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app',
188 |           error: undefined,
189 |           process: { pid: 12345 },
190 |         };
191 |       };
192 | 
193 |       const args = {
194 |         workspacePath: '/path/to/MyProject.xcworkspace',
195 |         scheme: 'MyScheme',
196 |         configuration: 'Release',
197 |         arch: 'arm64',
198 |       };
199 | 
200 |       await get_mac_app_pathLogic(args, mockExecutor);
201 | 
202 |       // Verify command generation with manual call tracking
203 |       expect(calls).toHaveLength(1);
204 |       expect(calls[0]).toEqual([
205 |         [
206 |           'xcodebuild',
207 |           '-showBuildSettings',
208 |           '-workspace',
209 |           '/path/to/MyProject.xcworkspace',
210 |           '-scheme',
211 |           'MyScheme',
212 |           '-configuration',
213 |           'Release',
214 |           '-destination',
215 |           'platform=macOS,arch=arm64',
216 |         ],
217 |         'Get App Path',
218 |         true,
219 |         undefined,
220 |       ]);
221 |     });
222 | 
223 |     it('should generate correct command with x86_64 architecture', async () => {
224 |       // Manual call tracking for command verification
225 |       const calls: any[] = [];
226 |       const mockExecutor: CommandExecutor = async (...args) => {
227 |         calls.push(args);
228 |         return {
229 |           success: true,
230 |           output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app',
231 |           error: undefined,
232 |           process: { pid: 12345 },
233 |         };
234 |       };
235 | 
236 |       const args = {
237 |         workspacePath: '/path/to/MyProject.xcworkspace',
238 |         scheme: 'MyScheme',
239 |         configuration: 'Debug',
240 |         arch: 'x86_64',
241 |       };
242 | 
243 |       await get_mac_app_pathLogic(args, mockExecutor);
244 | 
245 |       // Verify command generation with manual call tracking
246 |       expect(calls).toHaveLength(1);
247 |       expect(calls[0]).toEqual([
248 |         [
249 |           'xcodebuild',
250 |           '-showBuildSettings',
251 |           '-workspace',
252 |           '/path/to/MyProject.xcworkspace',
253 |           '-scheme',
254 |           'MyScheme',
255 |           '-configuration',
256 |           'Debug',
257 |           '-destination',
258 |           'platform=macOS,arch=x86_64',
259 |         ],
260 |         'Get App Path',
261 |         true,
262 |         undefined,
263 |       ]);
264 |     });
265 | 
266 |     it('should generate correct command with project all parameters', async () => {
267 |       // Manual call tracking for command verification
268 |       const calls: any[] = [];
269 |       const mockExecutor: CommandExecutor = async (...args) => {
270 |         calls.push(args);
271 |         return {
272 |           success: true,
273 |           output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app',
274 |           error: undefined,
275 |           process: { pid: 12345 },
276 |         };
277 |       };
278 | 
279 |       const args = {
280 |         projectPath: '/path/to/MyProject.xcodeproj',
281 |         scheme: 'MyScheme',
282 |         configuration: 'Release',
283 |         derivedDataPath: '/path/to/derived',
284 |         extraArgs: ['--verbose'],
285 |       };
286 | 
287 |       await get_mac_app_pathLogic(args, mockExecutor);
288 | 
289 |       // Verify command generation with manual call tracking
290 |       expect(calls).toHaveLength(1);
291 |       expect(calls[0]).toEqual([
292 |         [
293 |           'xcodebuild',
294 |           '-showBuildSettings',
295 |           '-project',
296 |           '/path/to/MyProject.xcodeproj',
297 |           '-scheme',
298 |           'MyScheme',
299 |           '-configuration',
300 |           'Release',
301 |           '-derivedDataPath',
302 |           '/path/to/derived',
303 |           '--verbose',
304 |         ],
305 |         'Get App Path',
306 |         true,
307 |         undefined,
308 |       ]);
309 |     });
310 | 
311 |     it('should use default configuration when not provided', async () => {
312 |       // Manual call tracking for command verification
313 |       const calls: any[] = [];
314 |       const mockExecutor: CommandExecutor = async (...args) => {
315 |         calls.push(args);
316 |         return {
317 |           success: true,
318 |           output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app',
319 |           error: undefined,
320 |           process: { pid: 12345 },
321 |         };
322 |       };
323 | 
324 |       const args = {
325 |         workspacePath: '/path/to/MyProject.xcworkspace',
326 |         scheme: 'MyScheme',
327 |         arch: 'arm64',
328 |       };
329 | 
330 |       await get_mac_app_pathLogic(args, mockExecutor);
331 | 
332 |       // Verify command generation with manual call tracking
333 |       expect(calls).toHaveLength(1);
334 |       expect(calls[0]).toEqual([
335 |         [
336 |           'xcodebuild',
337 |           '-showBuildSettings',
338 |           '-workspace',
339 |           '/path/to/MyProject.xcworkspace',
340 |           '-scheme',
341 |           'MyScheme',
342 |           '-configuration',
343 |           'Debug',
344 |           '-destination',
345 |           'platform=macOS,arch=arm64',
346 |         ],
347 |         'Get App Path',
348 |         true,
349 |         undefined,
350 |       ]);
351 |     });
352 |   });
353 | 
354 |   describe('Handler Behavior (Complete Literal Returns)', () => {
355 |     it('should return Zod validation error for missing scheme', async () => {
356 |       const result = await getMacAppPath.handler({
357 |         workspacePath: '/path/to/MyProject.xcworkspace',
358 |       });
359 | 
360 |       expect(result.isError).toBe(true);
361 |       expect(result.content[0].text).toContain('scheme is required');
362 |       expect(result.content[0].text).toContain('session-set-defaults');
363 |     });
364 | 
365 |     it('should return exact successful app path response with workspace', async () => {
366 |       const mockExecutor = createMockExecutor({
367 |         success: true,
368 |         output: `
369 | BUILT_PRODUCTS_DIR = /Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug
370 | FULL_PRODUCT_NAME = MyApp.app
371 |         `,
372 |       });
373 | 
374 |       const result = await get_mac_app_pathLogic(
375 |         {
376 |           workspacePath: '/path/to/MyProject.xcworkspace',
377 |           scheme: 'MyScheme',
378 |         },
379 |         mockExecutor,
380 |       );
381 | 
382 |       expect(result).toEqual({
383 |         content: [
384 |           {
385 |             type: 'text',
386 |             text: '✅ App path retrieved successfully: /Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug/MyApp.app',
387 |           },
388 |           {
389 |             type: 'text',
390 |             text: 'Next Steps:\n1. Get bundle ID: get_app_bundle_id({ appPath: "/Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug/MyApp.app" })\n2. Launch app: launch_mac_app({ appPath: "/Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug/MyApp.app" })',
391 |           },
392 |         ],
393 |       });
394 |     });
395 | 
396 |     it('should return exact successful app path response with project', async () => {
397 |       const mockExecutor = createMockExecutor({
398 |         success: true,
399 |         output: `
400 | BUILT_PRODUCTS_DIR = /Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug
401 | FULL_PRODUCT_NAME = MyApp.app
402 |         `,
403 |       });
404 | 
405 |       const result = await get_mac_app_pathLogic(
406 |         {
407 |           projectPath: '/path/to/MyProject.xcodeproj',
408 |           scheme: 'MyScheme',
409 |         },
410 |         mockExecutor,
411 |       );
412 | 
413 |       expect(result).toEqual({
414 |         content: [
415 |           {
416 |             type: 'text',
417 |             text: '✅ App path retrieved successfully: /Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug/MyApp.app',
418 |           },
419 |           {
420 |             type: 'text',
421 |             text: 'Next Steps:\n1. Get bundle ID: get_app_bundle_id({ appPath: "/Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug/MyApp.app" })\n2. Launch app: launch_mac_app({ appPath: "/Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug/MyApp.app" })',
422 |           },
423 |         ],
424 |       });
425 |     });
426 | 
427 |     it('should return exact build settings failure response', async () => {
428 |       const mockExecutor = createMockExecutor({
429 |         success: false,
430 |         error: 'error: No such scheme',
431 |       });
432 | 
433 |       const result = await get_mac_app_pathLogic(
434 |         {
435 |           workspacePath: '/path/to/MyProject.xcworkspace',
436 |           scheme: 'MyScheme',
437 |         },
438 |         mockExecutor,
439 |       );
440 | 
441 |       expect(result).toEqual({
442 |         content: [
443 |           {
444 |             type: 'text',
445 |             text: 'Error: Failed to get macOS app path\nDetails: error: No such scheme',
446 |           },
447 |         ],
448 |         isError: true,
449 |       });
450 |     });
451 | 
452 |     it('should return exact missing build settings response', async () => {
453 |       const mockExecutor = createMockExecutor({
454 |         success: true,
455 |         output: 'OTHER_SETTING = value',
456 |       });
457 | 
458 |       const result = await get_mac_app_pathLogic(
459 |         {
460 |           workspacePath: '/path/to/MyProject.xcworkspace',
461 |           scheme: 'MyScheme',
462 |         },
463 |         mockExecutor,
464 |       );
465 | 
466 |       expect(result).toEqual({
467 |         content: [
468 |           {
469 |             type: 'text',
470 |             text: 'Error: Failed to get macOS app path\nDetails: Could not extract app path from build settings',
471 |           },
472 |         ],
473 |         isError: true,
474 |       });
475 |     });
476 | 
477 |     it('should return exact exception handling response', async () => {
478 |       const mockExecutor = async () => {
479 |         throw new Error('Network error');
480 |       };
481 | 
482 |       const result = await get_mac_app_pathLogic(
483 |         {
484 |           workspacePath: '/path/to/MyProject.xcworkspace',
485 |           scheme: 'MyScheme',
486 |         },
487 |         mockExecutor,
488 |       );
489 | 
490 |       expect(result).toEqual({
491 |         content: [
492 |           {
493 |             type: 'text',
494 |             text: 'Error: Failed to get macOS app path\nDetails: Network error',
495 |           },
496 |         ],
497 |         isError: true,
498 |       });
499 |     });
500 |   });
501 | });
502 | 
```
Page 10/14FirstPrevNextLast