#
tokens: 49052/50000 23/337 files (page 4/14)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 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/screenshot.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Screenshot tool plugin - Capture screenshots from iOS Simulator
  3 |  */
  4 | import * as path from 'path';
  5 | import { tmpdir } from 'os';
  6 | import { z } from 'zod';
  7 | import { v4 as uuidv4 } from 'uuid';
  8 | import { ToolResponse, createImageContent } from '../../../types/common.ts';
  9 | import { log } from '../../../utils/logging/index.ts';
 10 | import { createErrorResponse, SystemError } from '../../../utils/responses/index.ts';
 11 | import type { CommandExecutor, FileSystemExecutor } from '../../../utils/execution/index.ts';
 12 | import {
 13 |   getDefaultFileSystemExecutor,
 14 |   getDefaultCommandExecutor,
 15 | } from '../../../utils/execution/index.ts';
 16 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
 17 | 
 18 | const LOG_PREFIX = '[Screenshot]';
 19 | 
 20 | // Define schema as ZodObject
 21 | const screenshotSchema = z.object({
 22 |   simulatorUuid: z.string().uuid('Invalid Simulator UUID format'),
 23 | });
 24 | 
 25 | // Use z.infer for type safety
 26 | type ScreenshotParams = z.infer<typeof screenshotSchema>;
 27 | 
 28 | export async function screenshotLogic(
 29 |   params: ScreenshotParams,
 30 |   executor: CommandExecutor,
 31 |   fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(),
 32 |   pathUtils: { tmpdir: () => string; join: (...paths: string[]) => string } = { ...path, tmpdir },
 33 |   uuidUtils: { v4: () => string } = { v4: uuidv4 },
 34 | ): Promise<ToolResponse> {
 35 |   const { simulatorUuid } = params;
 36 |   const tempDir = pathUtils.tmpdir();
 37 |   const screenshotFilename = `screenshot_${uuidUtils.v4()}.png`;
 38 |   const screenshotPath = pathUtils.join(tempDir, screenshotFilename);
 39 |   const optimizedFilename = `screenshot_optimized_${uuidUtils.v4()}.jpg`;
 40 |   const optimizedPath = pathUtils.join(tempDir, optimizedFilename);
 41 |   // Use xcrun simctl to take screenshot
 42 |   const commandArgs: string[] = [
 43 |     'xcrun',
 44 |     'simctl',
 45 |     'io',
 46 |     simulatorUuid,
 47 |     'screenshot',
 48 |     screenshotPath,
 49 |   ];
 50 | 
 51 |   log(
 52 |     'info',
 53 |     `${LOG_PREFIX}/screenshot: Starting capture to ${screenshotPath} on ${simulatorUuid}`,
 54 |   );
 55 | 
 56 |   try {
 57 |     // Execute the screenshot command
 58 |     const result = await executor(commandArgs, `${LOG_PREFIX}: screenshot`, false);
 59 | 
 60 |     if (!result.success) {
 61 |       throw new SystemError(`Failed to capture screenshot: ${result.error ?? result.output}`);
 62 |     }
 63 | 
 64 |     log('info', `${LOG_PREFIX}/screenshot: Success for ${simulatorUuid}`);
 65 | 
 66 |     try {
 67 |       // Optimize the image for LLM consumption: resize to max 800px width and convert to JPEG
 68 |       const optimizeArgs = [
 69 |         'sips',
 70 |         '-Z',
 71 |         '800', // Resize to max 800px (maintains aspect ratio)
 72 |         '-s',
 73 |         'format',
 74 |         'jpeg', // Convert to JPEG
 75 |         '-s',
 76 |         'formatOptions',
 77 |         '75', // 75% quality compression
 78 |         screenshotPath,
 79 |         '--out',
 80 |         optimizedPath,
 81 |       ];
 82 | 
 83 |       const optimizeResult = await executor(optimizeArgs, `${LOG_PREFIX}: optimize image`, false);
 84 | 
 85 |       if (!optimizeResult.success) {
 86 |         log('warning', `${LOG_PREFIX}/screenshot: Image optimization failed, using original PNG`);
 87 |         // Fallback to original PNG if optimization fails
 88 |         const base64Image = await fileSystemExecutor.readFile(screenshotPath, 'base64');
 89 | 
 90 |         // Clean up
 91 |         try {
 92 |           await fileSystemExecutor.rm(screenshotPath);
 93 |         } catch (err) {
 94 |           log('warning', `${LOG_PREFIX}/screenshot: Failed to delete temp file: ${err}`);
 95 |         }
 96 | 
 97 |         return {
 98 |           content: [createImageContent(base64Image, 'image/png')],
 99 |           isError: false,
100 |         };
101 |       }
102 | 
103 |       log('info', `${LOG_PREFIX}/screenshot: Image optimized successfully`);
104 | 
105 |       // Read the optimized image file as base64
106 |       const base64Image = await fileSystemExecutor.readFile(optimizedPath, 'base64');
107 | 
108 |       log('info', `${LOG_PREFIX}/screenshot: Successfully encoded image as Base64`);
109 | 
110 |       // Clean up both temporary files
111 |       try {
112 |         await fileSystemExecutor.rm(screenshotPath);
113 |         await fileSystemExecutor.rm(optimizedPath);
114 |       } catch (err) {
115 |         log('warning', `${LOG_PREFIX}/screenshot: Failed to delete temporary files: ${err}`);
116 |       }
117 | 
118 |       // Return the optimized image (JPEG format, smaller size)
119 |       return {
120 |         content: [createImageContent(base64Image, 'image/jpeg')],
121 |         isError: false,
122 |       };
123 |     } catch (fileError) {
124 |       log('error', `${LOG_PREFIX}/screenshot: Failed to process image file: ${fileError}`);
125 |       return createErrorResponse(
126 |         `Screenshot captured but failed to process image file: ${fileError instanceof Error ? fileError.message : String(fileError)}`,
127 |       );
128 |     }
129 |   } catch (_error) {
130 |     log('error', `${LOG_PREFIX}/screenshot: Failed - ${_error}`);
131 |     if (_error instanceof SystemError) {
132 |       return createErrorResponse(
133 |         `System error executing screenshot: ${_error.message}`,
134 |         _error.originalError?.stack,
135 |       );
136 |     }
137 |     return createErrorResponse(
138 |       `An unexpected error occurred: ${_error instanceof Error ? _error.message : String(_error)}`,
139 |     );
140 |   }
141 | }
142 | 
143 | export default {
144 |   name: 'screenshot',
145 |   description:
146 |     "Captures screenshot for visual verification. For UI coordinates, use describe_ui instead (don't determine coordinates from screenshots).",
147 |   schema: screenshotSchema.shape, // MCP SDK compatibility
148 |   handler: createTypedTool(
149 |     screenshotSchema,
150 |     (params: ScreenshotParams, executor: CommandExecutor) => {
151 |       return screenshotLogic(params, executor);
152 |     },
153 |     getDefaultCommandExecutor,
154 |   ),
155 | };
156 | 
```

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

```typescript
  1 | /**
  2 |  * Simulator Test Plugin: Test Simulator (Unified)
  3 |  *
  4 |  * Runs tests for a project or workspace on a simulator by UUID or name.
  5 |  * Accepts mutually exclusive `projectPath` or `workspacePath`.
  6 |  * Accepts mutually exclusive `simulatorId` or `simulatorName`.
  7 |  */
  8 | 
  9 | import { z } from 'zod';
 10 | import { handleTestLogic } from '../../../utils/test/index.ts';
 11 | import { log } from '../../../utils/logging/index.ts';
 12 | import { XcodePlatform } from '../../../types/common.ts';
 13 | import { ToolResponse } from '../../../types/common.ts';
 14 | import type { CommandExecutor } from '../../../utils/execution/index.ts';
 15 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
 16 | import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
 17 | import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
 18 | 
 19 | // Define base schema object with all fields
 20 | const baseSchemaObject = z.object({
 21 |   projectPath: z
 22 |     .string()
 23 |     .optional()
 24 |     .describe('Path to .xcodeproj file. Provide EITHER this OR workspacePath, not both'),
 25 |   workspacePath: z
 26 |     .string()
 27 |     .optional()
 28 |     .describe('Path to .xcworkspace file. Provide EITHER this OR projectPath, not both'),
 29 |   scheme: z.string().describe('The scheme to use (Required)'),
 30 |   simulatorId: z
 31 |     .string()
 32 |     .optional()
 33 |     .describe(
 34 |       'UUID of the simulator (from list_sims). Provide EITHER this OR simulatorName, not both',
 35 |     ),
 36 |   simulatorName: z
 37 |     .string()
 38 |     .optional()
 39 |     .describe(
 40 |       "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both",
 41 |     ),
 42 |   configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
 43 |   derivedDataPath: z
 44 |     .string()
 45 |     .optional()
 46 |     .describe('Path where build products and other derived data will go'),
 47 |   extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'),
 48 |   useLatestOS: z
 49 |     .boolean()
 50 |     .optional()
 51 |     .describe('Whether to use the latest OS version for the named simulator'),
 52 |   preferXcodebuild: z
 53 |     .boolean()
 54 |     .optional()
 55 |     .describe(
 56 |       'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.',
 57 |     ),
 58 |   testRunnerEnv: z
 59 |     .record(z.string(), z.string())
 60 |     .optional()
 61 |     .describe(
 62 |       'Environment variables to pass to the test runner (TEST_RUNNER_ prefix added automatically)',
 63 |     ),
 64 | });
 65 | 
 66 | // Apply preprocessor to handle empty strings
 67 | const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);
 68 | 
 69 | // Apply XOR validation: exactly one of projectPath OR workspacePath, and exactly one of simulatorId OR simulatorName required
 70 | const testSimulatorSchema = baseSchema
 71 |   .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
 72 |     message: 'Either projectPath or workspacePath is required.',
 73 |   })
 74 |   .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
 75 |     message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
 76 |   })
 77 |   .refine((val) => val.simulatorId !== undefined || val.simulatorName !== undefined, {
 78 |     message: 'Either simulatorId or simulatorName is required.',
 79 |   })
 80 |   .refine((val) => !(val.simulatorId !== undefined && val.simulatorName !== undefined), {
 81 |     message: 'simulatorId and simulatorName are mutually exclusive. Provide only one.',
 82 |   });
 83 | 
 84 | // Use z.infer for type safety
 85 | type TestSimulatorParams = z.infer<typeof testSimulatorSchema>;
 86 | 
 87 | export async function test_simLogic(
 88 |   params: TestSimulatorParams,
 89 |   executor: CommandExecutor,
 90 | ): Promise<ToolResponse> {
 91 |   // Log warning if useLatestOS is provided with simulatorId
 92 |   if (params.simulatorId && params.useLatestOS !== undefined) {
 93 |     log(
 94 |       'warning',
 95 |       `useLatestOS parameter is ignored when using simulatorId (UUID implies exact device/OS)`,
 96 |     );
 97 |   }
 98 | 
 99 |   return handleTestLogic(
100 |     {
101 |       projectPath: params.projectPath,
102 |       workspacePath: params.workspacePath,
103 |       scheme: params.scheme,
104 |       simulatorId: params.simulatorId,
105 |       simulatorName: params.simulatorName,
106 |       configuration: params.configuration ?? 'Debug',
107 |       derivedDataPath: params.derivedDataPath,
108 |       extraArgs: params.extraArgs,
109 |       useLatestOS: params.simulatorId ? false : (params.useLatestOS ?? false),
110 |       preferXcodebuild: params.preferXcodebuild ?? false,
111 |       platform: XcodePlatform.iOSSimulator,
112 |       testRunnerEnv: params.testRunnerEnv,
113 |     },
114 |     executor,
115 |   );
116 | }
117 | 
118 | const publicSchemaObject = baseSchemaObject.omit({
119 |   projectPath: true,
120 |   workspacePath: true,
121 |   scheme: true,
122 |   simulatorId: true,
123 |   simulatorName: true,
124 |   configuration: true,
125 |   useLatestOS: true,
126 | } as const);
127 | 
128 | export default {
129 |   name: 'test_sim',
130 |   description: 'Runs tests on an iOS simulator.',
131 |   schema: publicSchemaObject.shape,
132 |   handler: createSessionAwareTool<TestSimulatorParams>({
133 |     internalSchema: testSimulatorSchema as unknown as z.ZodType<TestSimulatorParams>,
134 |     logicFunction: test_simLogic,
135 |     getExecutor: getDefaultCommandExecutor,
136 |     requirements: [
137 |       { allOf: ['scheme'], message: 'scheme is required' },
138 |       { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
139 |       { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
140 |     ],
141 |     exclusivePairs: [
142 |       ['projectPath', 'workspacePath'],
143 |       ['simulatorId', 'simulatorName'],
144 |     ],
145 |   }),
146 | };
147 | 
```

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

```typescript
  1 | import { describe, it, expect, beforeEach } from 'vitest';
  2 | import { z } from 'zod';
  3 | import tool, { cleanLogic } from '../clean.ts';
  4 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
  5 | import { sessionStore } from '../../../../utils/session-store.ts';
  6 | 
  7 | describe('clean (unified) tool', () => {
  8 |   beforeEach(() => {
  9 |     sessionStore.clear();
 10 |   });
 11 | 
 12 |   it('exports correct name/description/schema/handler', () => {
 13 |     expect(tool.name).toBe('clean');
 14 |     expect(tool.description).toBe('Cleans build products with xcodebuild.');
 15 |     expect(typeof tool.handler).toBe('function');
 16 | 
 17 |     const schema = z.object(tool.schema).strict();
 18 |     expect(schema.safeParse({}).success).toBe(true);
 19 |     expect(
 20 |       schema.safeParse({
 21 |         derivedDataPath: '/tmp/Derived',
 22 |         extraArgs: ['--quiet'],
 23 |         preferXcodebuild: true,
 24 |         platform: 'iOS Simulator',
 25 |       }).success,
 26 |     ).toBe(true);
 27 |     expect(schema.safeParse({ configuration: 'Debug' }).success).toBe(false);
 28 | 
 29 |     const schemaKeys = Object.keys(tool.schema).sort();
 30 |     expect(schemaKeys).toEqual(
 31 |       ['derivedDataPath', 'extraArgs', 'platform', 'preferXcodebuild'].sort(),
 32 |     );
 33 |   });
 34 | 
 35 |   it('handler validation: error when neither projectPath nor workspacePath provided', async () => {
 36 |     const result = await (tool as any).handler({});
 37 |     expect(result.isError).toBe(true);
 38 |     const text = String(result.content?.[0]?.text ?? '');
 39 |     expect(text).toContain('Missing required session defaults');
 40 |     expect(text).toContain('Provide a project or workspace');
 41 |   });
 42 | 
 43 |   it('handler validation: error when both projectPath and workspacePath provided', async () => {
 44 |     const result = await (tool as any).handler({
 45 |       projectPath: '/p.xcodeproj',
 46 |       workspacePath: '/w.xcworkspace',
 47 |     });
 48 |     expect(result.isError).toBe(true);
 49 |     const text = String(result.content?.[0]?.text ?? '');
 50 |     expect(text).toContain('Mutually exclusive parameters provided');
 51 |   });
 52 | 
 53 |   it('runs project-path flow via logic', async () => {
 54 |     const mock = createMockExecutor({ success: true, output: 'ok' });
 55 |     const result = await cleanLogic({ projectPath: '/p.xcodeproj', scheme: 'App' } as any, mock);
 56 |     expect(result.isError).not.toBe(true);
 57 |   });
 58 | 
 59 |   it('runs workspace-path flow via logic', async () => {
 60 |     const mock = createMockExecutor({ success: true, output: 'ok' });
 61 |     const result = await cleanLogic(
 62 |       { workspacePath: '/w.xcworkspace', scheme: 'App' } as any,
 63 |       mock,
 64 |     );
 65 |     expect(result.isError).not.toBe(true);
 66 |   });
 67 | 
 68 |   it('handler validation: requires scheme when workspacePath is provided', async () => {
 69 |     const result = await (tool as any).handler({ workspacePath: '/w.xcworkspace' });
 70 |     expect(result.isError).toBe(true);
 71 |     const text = String(result.content?.[0]?.text ?? '');
 72 |     expect(text).toContain('Parameter validation failed');
 73 |     expect(text).toContain('scheme is required when workspacePath is provided');
 74 |   });
 75 | 
 76 |   it('uses iOS platform by default', async () => {
 77 |     let capturedCommand: string[] = [];
 78 |     const mockExecutor = async (command: string[]) => {
 79 |       capturedCommand = command;
 80 |       return { success: true, output: 'clean success' };
 81 |     };
 82 | 
 83 |     const result = await cleanLogic(
 84 |       { projectPath: '/p.xcodeproj', scheme: 'App' } as any,
 85 |       mockExecutor,
 86 |     );
 87 |     expect(result.isError).not.toBe(true);
 88 | 
 89 |     // Check that the command contains iOS platform destination
 90 |     const commandStr = capturedCommand.join(' ');
 91 |     expect(commandStr).toContain('-destination');
 92 |     expect(commandStr).toContain('platform=iOS');
 93 |   });
 94 | 
 95 |   it('accepts custom platform parameter', async () => {
 96 |     let capturedCommand: string[] = [];
 97 |     const mockExecutor = async (command: string[]) => {
 98 |       capturedCommand = command;
 99 |       return { success: true, output: 'clean success' };
100 |     };
101 | 
102 |     const result = await cleanLogic(
103 |       {
104 |         projectPath: '/p.xcodeproj',
105 |         scheme: 'App',
106 |         platform: 'macOS',
107 |       } as any,
108 |       mockExecutor,
109 |     );
110 |     expect(result.isError).not.toBe(true);
111 | 
112 |     // Check that the command contains macOS platform destination
113 |     const commandStr = capturedCommand.join(' ');
114 |     expect(commandStr).toContain('-destination');
115 |     expect(commandStr).toContain('platform=macOS');
116 |   });
117 | 
118 |   it('accepts iOS Simulator platform parameter (maps to iOS for clean)', async () => {
119 |     let capturedCommand: string[] = [];
120 |     const mockExecutor = async (command: string[]) => {
121 |       capturedCommand = command;
122 |       return { success: true, output: 'clean success' };
123 |     };
124 | 
125 |     const result = await cleanLogic(
126 |       {
127 |         projectPath: '/p.xcodeproj',
128 |         scheme: 'App',
129 |         platform: 'iOS Simulator',
130 |       } as any,
131 |       mockExecutor,
132 |     );
133 |     expect(result.isError).not.toBe(true);
134 | 
135 |     // For clean operations, iOS Simulator should be mapped to iOS platform
136 |     const commandStr = capturedCommand.join(' ');
137 |     expect(commandStr).toContain('-destination');
138 |     expect(commandStr).toContain('platform=iOS');
139 |   });
140 | 
141 |   it('handler validation: rejects invalid platform values', async () => {
142 |     const result = await (tool as any).handler({
143 |       projectPath: '/p.xcodeproj',
144 |       scheme: 'App',
145 |       platform: 'InvalidPlatform',
146 |     });
147 |     expect(result.isError).toBe(true);
148 |     const text = String(result.content?.[0]?.text ?? '');
149 |     expect(text).toContain('Parameter validation failed');
150 |     expect(text).toContain('platform');
151 |   });
152 | });
153 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for active-processes module
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  */
  5 | 
  6 | import { describe, it, expect, beforeEach } from 'vitest';
  7 | import {
  8 |   activeProcesses,
  9 |   getProcess,
 10 |   addProcess,
 11 |   removeProcess,
 12 |   clearAllProcesses,
 13 |   type ProcessInfo,
 14 | } from '../active-processes.ts';
 15 | 
 16 | describe('active-processes module', () => {
 17 |   // Clear the map before each test
 18 |   beforeEach(() => {
 19 |     clearAllProcesses();
 20 |   });
 21 | 
 22 |   describe('activeProcesses Map', () => {
 23 |     it('should be a Map instance', () => {
 24 |       expect(activeProcesses instanceof Map).toBe(true);
 25 |     });
 26 | 
 27 |     it('should start empty after clearing', () => {
 28 |       expect(activeProcesses.size).toBe(0);
 29 |     });
 30 |   });
 31 | 
 32 |   describe('getProcess function', () => {
 33 |     it('should return undefined for non-existent process', () => {
 34 |       const result = getProcess(12345);
 35 |       expect(result).toBe(undefined);
 36 |     });
 37 | 
 38 |     it('should return process info for existing process', () => {
 39 |       const mockProcess = {
 40 |         kill: () => {},
 41 |         on: () => {},
 42 |         pid: 12345,
 43 |       };
 44 |       const startedAt = new Date('2023-01-01T10:00:00.000Z');
 45 |       const processInfo: ProcessInfo = {
 46 |         process: mockProcess,
 47 |         startedAt: startedAt,
 48 |       };
 49 | 
 50 |       activeProcesses.set(12345, processInfo);
 51 |       const result = getProcess(12345);
 52 | 
 53 |       expect(result).toEqual({
 54 |         process: mockProcess,
 55 |         startedAt: startedAt,
 56 |       });
 57 |     });
 58 |   });
 59 | 
 60 |   describe('addProcess function', () => {
 61 |     it('should add process to the map', () => {
 62 |       const mockProcess = {
 63 |         kill: () => {},
 64 |         on: () => {},
 65 |         pid: 67890,
 66 |       };
 67 |       const startedAt = new Date('2023-02-15T14:30:00.000Z');
 68 |       const processInfo: ProcessInfo = {
 69 |         process: mockProcess,
 70 |         startedAt: startedAt,
 71 |       };
 72 | 
 73 |       addProcess(67890, processInfo);
 74 | 
 75 |       expect(activeProcesses.size).toBe(1);
 76 |       expect(activeProcesses.get(67890)).toEqual(processInfo);
 77 |     });
 78 | 
 79 |     it('should overwrite existing process with same pid', () => {
 80 |       const mockProcess1 = {
 81 |         kill: () => {},
 82 |         on: () => {},
 83 |         pid: 11111,
 84 |       };
 85 |       const mockProcess2 = {
 86 |         kill: () => {},
 87 |         on: () => {},
 88 |         pid: 11111,
 89 |       };
 90 |       const startedAt1 = new Date('2023-01-01T10:00:00.000Z');
 91 |       const startedAt2 = new Date('2023-01-01T11:00:00.000Z');
 92 | 
 93 |       addProcess(11111, { process: mockProcess1, startedAt: startedAt1 });
 94 |       addProcess(11111, { process: mockProcess2, startedAt: startedAt2 });
 95 | 
 96 |       expect(activeProcesses.size).toBe(1);
 97 |       expect(activeProcesses.get(11111)).toEqual({
 98 |         process: mockProcess2,
 99 |         startedAt: startedAt2,
100 |       });
101 |     });
102 |   });
103 | 
104 |   describe('removeProcess function', () => {
105 |     it('should return false for non-existent process', () => {
106 |       const result = removeProcess(99999);
107 |       expect(result).toBe(false);
108 |     });
109 | 
110 |     it('should return true and remove existing process', () => {
111 |       const mockProcess = {
112 |         kill: () => {},
113 |         on: () => {},
114 |         pid: 54321,
115 |       };
116 |       const processInfo: ProcessInfo = {
117 |         process: mockProcess,
118 |         startedAt: new Date('2023-03-20T09:15:00.000Z'),
119 |       };
120 | 
121 |       addProcess(54321, processInfo);
122 |       expect(activeProcesses.size).toBe(1);
123 | 
124 |       const result = removeProcess(54321);
125 | 
126 |       expect(result).toBe(true);
127 |       expect(activeProcesses.size).toBe(0);
128 |       expect(activeProcesses.get(54321)).toBe(undefined);
129 |     });
130 |   });
131 | 
132 |   describe('clearAllProcesses function', () => {
133 |     it('should clear all processes from the map', () => {
134 |       const mockProcess1 = {
135 |         kill: () => {},
136 |         on: () => {},
137 |         pid: 1111,
138 |       };
139 |       const mockProcess2 = {
140 |         kill: () => {},
141 |         on: () => {},
142 |         pid: 2222,
143 |       };
144 | 
145 |       addProcess(1111, { process: mockProcess1, startedAt: new Date() });
146 |       addProcess(2222, { process: mockProcess2, startedAt: new Date() });
147 | 
148 |       expect(activeProcesses.size).toBe(2);
149 | 
150 |       clearAllProcesses();
151 | 
152 |       expect(activeProcesses.size).toBe(0);
153 |     });
154 | 
155 |     it('should work on already empty map', () => {
156 |       expect(activeProcesses.size).toBe(0);
157 |       clearAllProcesses();
158 |       expect(activeProcesses.size).toBe(0);
159 |     });
160 |   });
161 | 
162 |   describe('ProcessInfo interface', () => {
163 |     it('should work with complete process object', () => {
164 |       const mockProcess = {
165 |         kill: () => {},
166 |         on: () => {},
167 |         pid: 12345,
168 |       };
169 |       const startedAt = new Date('2023-01-01T10:00:00.000Z');
170 |       const processInfo: ProcessInfo = {
171 |         process: mockProcess,
172 |         startedAt: startedAt,
173 |       };
174 | 
175 |       addProcess(12345, processInfo);
176 |       const retrieved = getProcess(12345);
177 | 
178 |       expect(retrieved).toEqual({
179 |         process: {
180 |           kill: expect.any(Function),
181 |           on: expect.any(Function),
182 |           pid: 12345,
183 |         },
184 |         startedAt: startedAt,
185 |       });
186 |     });
187 | 
188 |     it('should work with minimal process object', () => {
189 |       const mockProcess = {
190 |         kill: () => {},
191 |         on: () => {},
192 |       };
193 |       const startedAt = new Date('2023-01-01T10:00:00.000Z');
194 |       const processInfo: ProcessInfo = {
195 |         process: mockProcess,
196 |         startedAt: startedAt,
197 |       };
198 | 
199 |       addProcess(98765, processInfo);
200 |       const retrieved = getProcess(98765);
201 | 
202 |       expect(retrieved).toEqual({
203 |         process: {
204 |           kill: expect.any(Function),
205 |           on: expect.any(Function),
206 |         },
207 |         startedAt: startedAt,
208 |       });
209 |     });
210 |   });
211 | });
212 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for launch_app_logs_sim plugin (session-aware version)
  3 |  * Follows CLAUDE.md guidance with literal validation and DI.
  4 |  */
  5 | 
  6 | import { describe, it, expect, beforeEach } from 'vitest';
  7 | import { z } from 'zod';
  8 | import launchAppLogsSim, {
  9 |   launch_app_logs_simLogic,
 10 |   LogCaptureFunction,
 11 | } from '../launch_app_logs_sim.ts';
 12 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
 13 | import { sessionStore } from '../../../../utils/session-store.ts';
 14 | 
 15 | describe('launch_app_logs_sim tool', () => {
 16 |   beforeEach(() => {
 17 |     sessionStore.clear();
 18 |   });
 19 | 
 20 |   describe('Export Field Validation (Literal)', () => {
 21 |     it('should expose correct metadata', () => {
 22 |       expect(launchAppLogsSim.name).toBe('launch_app_logs_sim');
 23 |       expect(launchAppLogsSim.description).toBe(
 24 |         'Launches an app in an iOS simulator and captures its logs.',
 25 |       );
 26 |     });
 27 | 
 28 |     it('should expose only non-session fields in public schema', () => {
 29 |       const schema = z.object(launchAppLogsSim.schema);
 30 | 
 31 |       expect(schema.safeParse({ bundleId: 'com.example.app' }).success).toBe(true);
 32 |       expect(schema.safeParse({ bundleId: 'com.example.app', args: ['--debug'] }).success).toBe(
 33 |         true,
 34 |       );
 35 |       expect(schema.safeParse({}).success).toBe(false);
 36 |       expect(schema.safeParse({ bundleId: 42 }).success).toBe(false);
 37 | 
 38 |       expect(Object.keys(launchAppLogsSim.schema).sort()).toEqual(['args', 'bundleId'].sort());
 39 |     });
 40 |   });
 41 | 
 42 |   describe('Handler Requirements', () => {
 43 |     it('should require simulatorId when not provided', async () => {
 44 |       const result = await launchAppLogsSim.handler({ bundleId: 'com.example.testapp' });
 45 | 
 46 |       expect(result.isError).toBe(true);
 47 |       expect(result.content[0].text).toContain('Missing required session defaults');
 48 |       expect(result.content[0].text).toContain('simulatorId is required');
 49 |       expect(result.content[0].text).toContain('session-set-defaults');
 50 |     });
 51 | 
 52 |     it('should validate bundleId when simulatorId default exists', async () => {
 53 |       sessionStore.setDefaults({ simulatorId: 'SIM-UUID' });
 54 | 
 55 |       const result = await launchAppLogsSim.handler({});
 56 | 
 57 |       expect(result.isError).toBe(true);
 58 |       expect(result.content[0].text).toContain('Parameter validation failed');
 59 |       expect(result.content[0].text).toContain('bundleId: Required');
 60 |       expect(result.content[0].text).toContain(
 61 |         'Tip: set session defaults via session-set-defaults',
 62 |       );
 63 |     });
 64 |   });
 65 | 
 66 |   describe('Logic Behavior (Literal Returns)', () => {
 67 |     it('should handle successful app launch with log capture', async () => {
 68 |       let capturedParams: unknown = null;
 69 |       const logCaptureStub: LogCaptureFunction = async (params) => {
 70 |         capturedParams = params;
 71 |         return {
 72 |           sessionId: 'test-session-123',
 73 |           logFilePath: '/tmp/xcodemcp_sim_log_test-session-123.log',
 74 |           processes: [],
 75 |           error: undefined,
 76 |         };
 77 |       };
 78 | 
 79 |       const mockExecutor = createMockExecutor({ success: true, output: '' });
 80 | 
 81 |       const result = await launch_app_logs_simLogic(
 82 |         {
 83 |           simulatorId: 'test-uuid-123',
 84 |           bundleId: 'com.example.testapp',
 85 |         },
 86 |         mockExecutor,
 87 |         logCaptureStub,
 88 |       );
 89 | 
 90 |       expect(result).toEqual({
 91 |         content: [
 92 |           {
 93 |             type: 'text',
 94 |             text: `App launched successfully in simulator test-uuid-123 with log capture enabled.\n\nLog capture session ID: test-session-123\n\nNext Steps:\n1. Interact with your app in the simulator.\n2. Use 'stop_and_get_simulator_log({ logSessionId: "test-session-123" })' to stop capture and retrieve logs.`,
 95 |           },
 96 |         ],
 97 |         isError: false,
 98 |       });
 99 | 
100 |       expect(capturedParams).toEqual({
101 |         simulatorUuid: 'test-uuid-123',
102 |         bundleId: 'com.example.testapp',
103 |         captureConsole: true,
104 |       });
105 |     });
106 | 
107 |     it('should ignore args for log capture setup', async () => {
108 |       let capturedParams: unknown = null;
109 |       const logCaptureStub: LogCaptureFunction = async (params) => {
110 |         capturedParams = params;
111 |         return {
112 |           sessionId: 'test-session-456',
113 |           logFilePath: '/tmp/xcodemcp_sim_log_test-session-456.log',
114 |           processes: [],
115 |           error: undefined,
116 |         };
117 |       };
118 | 
119 |       const mockExecutor = createMockExecutor({ success: true, output: '' });
120 | 
121 |       await launch_app_logs_simLogic(
122 |         {
123 |           simulatorId: 'test-uuid-123',
124 |           bundleId: 'com.example.testapp',
125 |           args: ['--debug'],
126 |         },
127 |         mockExecutor,
128 |         logCaptureStub,
129 |       );
130 | 
131 |       expect(capturedParams).toEqual({
132 |         simulatorUuid: 'test-uuid-123',
133 |         bundleId: 'com.example.testapp',
134 |         captureConsole: true,
135 |         args: ['--debug'],
136 |       });
137 |     });
138 | 
139 |     it('should surface log capture failure', async () => {
140 |       const logCaptureStub: LogCaptureFunction = async () => ({
141 |         sessionId: '',
142 |         logFilePath: '',
143 |         processes: [],
144 |         error: 'Failed to start log capture',
145 |       });
146 | 
147 |       const mockExecutor = createMockExecutor({ success: true, output: '' });
148 | 
149 |       const result = await launch_app_logs_simLogic(
150 |         {
151 |           simulatorId: 'test-uuid-123',
152 |           bundleId: 'com.example.testapp',
153 |         },
154 |         mockExecutor,
155 |         logCaptureStub,
156 |       );
157 | 
158 |       expect(result).toEqual({
159 |         content: [
160 |           {
161 |             type: 'text',
162 |             text: 'App was launched but log capture failed: Failed to start log capture',
163 |           },
164 |         ],
165 |         isError: true,
166 |       });
167 |     });
168 |   });
169 | });
170 | 
```

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

```typescript
  1 | /**
  2 |  * Integration tests for TEST_RUNNER_ environment variable passing
  3 |  *
  4 |  * These tests verify that testRunnerEnv parameters are correctly processed
  5 |  * and passed through the execution chain. We focus on testing the core
  6 |  * functionality that matters most: environment variable normalization.
  7 |  */
  8 | 
  9 | import { describe, it, expect } from 'vitest';
 10 | import { normalizeTestRunnerEnv } from '../environment.ts';
 11 | 
 12 | describe('TEST_RUNNER_ Environment Variable Integration', () => {
 13 |   describe('Core normalization functionality', () => {
 14 |     it('should normalize environment variables correctly for real scenarios', () => {
 15 |       // Test the GitHub issue scenario: USE_DEV_MODE -> TEST_RUNNER_USE_DEV_MODE
 16 |       const gitHubIssueScenario = { USE_DEV_MODE: 'YES' };
 17 |       const normalized = normalizeTestRunnerEnv(gitHubIssueScenario);
 18 | 
 19 |       expect(normalized).toEqual({ TEST_RUNNER_USE_DEV_MODE: 'YES' });
 20 |     });
 21 | 
 22 |     it('should handle mixed prefixed and unprefixed variables', () => {
 23 |       const mixedVars = {
 24 |         USE_DEV_MODE: 'YES', // Should be prefixed
 25 |         TEST_RUNNER_SKIP_ANIMATIONS: '1', // Already prefixed, preserve
 26 |         DEBUG_MODE: 'true', // Should be prefixed
 27 |       };
 28 | 
 29 |       const normalized = normalizeTestRunnerEnv(mixedVars);
 30 | 
 31 |       expect(normalized).toEqual({
 32 |         TEST_RUNNER_USE_DEV_MODE: 'YES',
 33 |         TEST_RUNNER_SKIP_ANIMATIONS: '1',
 34 |         TEST_RUNNER_DEBUG_MODE: 'true',
 35 |       });
 36 |     });
 37 | 
 38 |     it('should filter out null and undefined values', () => {
 39 |       const varsWithNulls = {
 40 |         VALID_VAR: 'value1',
 41 |         NULL_VAR: null as any,
 42 |         UNDEFINED_VAR: undefined as any,
 43 |         ANOTHER_VALID: 'value2',
 44 |       };
 45 | 
 46 |       const normalized = normalizeTestRunnerEnv(varsWithNulls);
 47 | 
 48 |       expect(normalized).toEqual({
 49 |         TEST_RUNNER_VALID_VAR: 'value1',
 50 |         TEST_RUNNER_ANOTHER_VALID: 'value2',
 51 |       });
 52 | 
 53 |       // Ensure null/undefined vars are not present
 54 |       expect(normalized).not.toHaveProperty('TEST_RUNNER_NULL_VAR');
 55 |       expect(normalized).not.toHaveProperty('TEST_RUNNER_UNDEFINED_VAR');
 56 |     });
 57 | 
 58 |     it('should handle special characters in keys and values', () => {
 59 |       const specialChars = {
 60 |         'VAR_WITH-DASH': 'value-with-dash',
 61 |         'VAR.WITH.DOTS': 'value/with/slashes',
 62 |         VAR_WITH_SPACES: 'value with spaces',
 63 |         TEST_RUNNER_PRE_EXISTING: 'already=prefixed=value',
 64 |       };
 65 | 
 66 |       const normalized = normalizeTestRunnerEnv(specialChars);
 67 | 
 68 |       expect(normalized).toEqual({
 69 |         'TEST_RUNNER_VAR_WITH-DASH': 'value-with-dash',
 70 |         'TEST_RUNNER_VAR.WITH.DOTS': 'value/with/slashes',
 71 |         TEST_RUNNER_VAR_WITH_SPACES: 'value with spaces',
 72 |         TEST_RUNNER_PRE_EXISTING: 'already=prefixed=value',
 73 |       });
 74 |     });
 75 | 
 76 |     it('should handle empty values correctly', () => {
 77 |       const emptyValues = {
 78 |         EMPTY_STRING: '',
 79 |         NORMAL_VAR: 'normal_value',
 80 |       };
 81 | 
 82 |       const normalized = normalizeTestRunnerEnv(emptyValues);
 83 | 
 84 |       expect(normalized).toEqual({
 85 |         TEST_RUNNER_EMPTY_STRING: '',
 86 |         TEST_RUNNER_NORMAL_VAR: 'normal_value',
 87 |       });
 88 |     });
 89 | 
 90 |     it('should handle edge case prefix variations', () => {
 91 |       const prefixEdgeCases = {
 92 |         TEST_RUN: 'not_quite_prefixed', // Should get prefixed
 93 |         TEST_RUNNER: 'no_underscore', // Should get prefixed
 94 |         TEST_RUNNER_CORRECT: 'already_good', // Should stay as-is
 95 |         test_runner_lowercase: 'lowercase', // Should get prefixed (case sensitive)
 96 |       };
 97 | 
 98 |       const normalized = normalizeTestRunnerEnv(prefixEdgeCases);
 99 | 
100 |       expect(normalized).toEqual({
101 |         TEST_RUNNER_TEST_RUN: 'not_quite_prefixed',
102 |         TEST_RUNNER_TEST_RUNNER: 'no_underscore',
103 |         TEST_RUNNER_CORRECT: 'already_good',
104 |         TEST_RUNNER_test_runner_lowercase: 'lowercase',
105 |       });
106 |     });
107 | 
108 |     it('should preserve immutability of input object', () => {
109 |       const originalInput = { FOO: 'bar', BAZ: 'qux' };
110 |       const inputCopy = { ...originalInput };
111 | 
112 |       const normalized = normalizeTestRunnerEnv(originalInput);
113 | 
114 |       // Original should be unchanged
115 |       expect(originalInput).toEqual(inputCopy);
116 | 
117 |       // Result should be different
118 |       expect(normalized).not.toEqual(originalInput);
119 |       expect(normalized).toEqual({
120 |         TEST_RUNNER_FOO: 'bar',
121 |         TEST_RUNNER_BAZ: 'qux',
122 |       });
123 |     });
124 | 
125 |     it('should handle the complete test environment workflow', () => {
126 |       // Simulate a comprehensive test environment setup
127 |       const fullTestEnv = {
128 |         // Core testing flags
129 |         USE_DEV_MODE: 'YES',
130 |         SKIP_ANIMATIONS: '1',
131 |         FAST_MODE: 'true',
132 | 
133 |         // Already prefixed variables (user might provide these)
134 |         TEST_RUNNER_TIMEOUT: '30',
135 |         TEST_RUNNER_RETRIES: '3',
136 | 
137 |         // UI testing specific
138 |         UI_TESTING_MODE: 'enabled',
139 |         SCREENSHOT_MODE: 'disabled',
140 | 
141 |         // Performance testing
142 |         PERFORMANCE_TESTS: 'false',
143 |         MEMORY_TESTING: 'true',
144 | 
145 |         // Special values
146 |         EMPTY_VAR: '',
147 |         PATH_VAR: '/usr/local/bin:/usr/bin',
148 |       };
149 | 
150 |       const normalized = normalizeTestRunnerEnv(fullTestEnv);
151 | 
152 |       expect(normalized).toEqual({
153 |         TEST_RUNNER_USE_DEV_MODE: 'YES',
154 |         TEST_RUNNER_SKIP_ANIMATIONS: '1',
155 |         TEST_RUNNER_FAST_MODE: 'true',
156 |         TEST_RUNNER_TIMEOUT: '30',
157 |         TEST_RUNNER_RETRIES: '3',
158 |         TEST_RUNNER_UI_TESTING_MODE: 'enabled',
159 |         TEST_RUNNER_SCREENSHOT_MODE: 'disabled',
160 |         TEST_RUNNER_PERFORMANCE_TESTS: 'false',
161 |         TEST_RUNNER_MEMORY_TESTING: 'true',
162 |         TEST_RUNNER_EMPTY_VAR: '',
163 |         TEST_RUNNER_PATH_VAR: '/usr/local/bin:/usr/bin',
164 |       });
165 |     });
166 |   });
167 | });
168 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for swift_package_clean plugin
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using dependency injection for deterministic testing
  5 |  */
  6 | 
  7 | import { describe, it, expect } from 'vitest';
  8 | import {
  9 |   createMockExecutor,
 10 |   createMockFileSystemExecutor,
 11 |   createNoopExecutor,
 12 | } from '../../../../test-utils/mock-executors.ts';
 13 | import swiftPackageClean, { swift_package_cleanLogic } from '../swift_package_clean.ts';
 14 | 
 15 | describe('swift_package_clean plugin', () => {
 16 |   describe('Export Field Validation (Literal)', () => {
 17 |     it('should have correct name', () => {
 18 |       expect(swiftPackageClean.name).toBe('swift_package_clean');
 19 |     });
 20 | 
 21 |     it('should have correct description', () => {
 22 |       expect(swiftPackageClean.description).toBe(
 23 |         'Cleans Swift Package build artifacts and derived data',
 24 |       );
 25 |     });
 26 | 
 27 |     it('should have handler function', () => {
 28 |       expect(typeof swiftPackageClean.handler).toBe('function');
 29 |     });
 30 | 
 31 |     it('should validate schema correctly', () => {
 32 |       // Test required fields
 33 |       expect(swiftPackageClean.schema.packagePath.safeParse('/test/package').success).toBe(true);
 34 |       expect(swiftPackageClean.schema.packagePath.safeParse('').success).toBe(true);
 35 | 
 36 |       // Test invalid inputs
 37 |       expect(swiftPackageClean.schema.packagePath.safeParse(null).success).toBe(false);
 38 |       expect(swiftPackageClean.schema.packagePath.safeParse(undefined).success).toBe(false);
 39 |     });
 40 |   });
 41 | 
 42 |   describe('Command Generation Testing', () => {
 43 |     it('should build correct command for clean', async () => {
 44 |       const calls: Array<{
 45 |         command: string[];
 46 |         description: string;
 47 |         showOutput: boolean;
 48 |         workingDirectory: string | undefined;
 49 |       }> = [];
 50 | 
 51 |       const mockExecutor = async (
 52 |         command: string[],
 53 |         description: string,
 54 |         showOutput: boolean,
 55 |         workingDirectory?: string,
 56 |       ) => {
 57 |         calls.push({ command, description, showOutput, workingDirectory });
 58 |         return {
 59 |           success: true,
 60 |           output: 'Clean succeeded',
 61 |           error: undefined,
 62 |           process: { pid: 12345 },
 63 |         };
 64 |       };
 65 | 
 66 |       await swift_package_cleanLogic(
 67 |         {
 68 |           packagePath: '/test/package',
 69 |         },
 70 |         mockExecutor,
 71 |       );
 72 | 
 73 |       expect(calls).toHaveLength(1);
 74 |       expect(calls[0]).toEqual({
 75 |         command: ['swift', 'package', '--package-path', '/test/package', 'clean'],
 76 |         description: 'Swift Package Clean',
 77 |         showOutput: true,
 78 |         workingDirectory: undefined,
 79 |       });
 80 |     });
 81 |   });
 82 | 
 83 |   describe('Response Logic Testing', () => {
 84 |     it('should handle valid params without validation errors in logic function', async () => {
 85 |       // Note: The logic function assumes valid params since createTypedTool handles validation
 86 |       const mockExecutor = createMockExecutor({
 87 |         success: true,
 88 |         output: 'Package cleaned successfully',
 89 |       });
 90 | 
 91 |       const result = await swift_package_cleanLogic(
 92 |         {
 93 |           packagePath: '/test/package',
 94 |         },
 95 |         mockExecutor,
 96 |       );
 97 | 
 98 |       expect(result.isError).toBe(false);
 99 |       expect(result.content[0].text).toBe('✅ Swift package cleaned successfully.');
100 |     });
101 | 
102 |     it('should return successful clean response', async () => {
103 |       const mockExecutor = createMockExecutor({
104 |         success: true,
105 |         output: 'Package cleaned successfully',
106 |       });
107 | 
108 |       const result = await swift_package_cleanLogic(
109 |         {
110 |           packagePath: '/test/package',
111 |         },
112 |         mockExecutor,
113 |       );
114 | 
115 |       expect(result).toEqual({
116 |         content: [
117 |           { type: 'text', text: '✅ Swift package cleaned successfully.' },
118 |           {
119 |             type: 'text',
120 |             text: '💡 Build artifacts and derived data removed. Ready for fresh build.',
121 |           },
122 |           { type: 'text', text: 'Package cleaned successfully' },
123 |         ],
124 |         isError: false,
125 |       });
126 |     });
127 | 
128 |     it('should return successful clean response with no output', async () => {
129 |       const mockExecutor = createMockExecutor({
130 |         success: true,
131 |         output: '',
132 |       });
133 | 
134 |       const result = await swift_package_cleanLogic(
135 |         {
136 |           packagePath: '/test/package',
137 |         },
138 |         mockExecutor,
139 |       );
140 | 
141 |       expect(result).toEqual({
142 |         content: [
143 |           { type: 'text', text: '✅ Swift package cleaned successfully.' },
144 |           {
145 |             type: 'text',
146 |             text: '💡 Build artifacts and derived data removed. Ready for fresh build.',
147 |           },
148 |           { type: 'text', text: '(clean completed silently)' },
149 |         ],
150 |         isError: false,
151 |       });
152 |     });
153 | 
154 |     it('should return error response for clean failure', async () => {
155 |       const mockExecutor = createMockExecutor({
156 |         success: false,
157 |         error: 'Permission denied',
158 |       });
159 | 
160 |       const result = await swift_package_cleanLogic(
161 |         {
162 |           packagePath: '/test/package',
163 |         },
164 |         mockExecutor,
165 |       );
166 | 
167 |       expect(result).toEqual({
168 |         content: [
169 |           {
170 |             type: 'text',
171 |             text: 'Error: Swift package clean failed\nDetails: Permission denied',
172 |           },
173 |         ],
174 |         isError: true,
175 |       });
176 |     });
177 | 
178 |     it('should handle spawn error', async () => {
179 |       const mockExecutor = async () => {
180 |         throw new Error('spawn ENOENT');
181 |       };
182 | 
183 |       const result = await swift_package_cleanLogic(
184 |         {
185 |           packagePath: '/test/package',
186 |         },
187 |         mockExecutor,
188 |       );
189 | 
190 |       expect(result).toEqual({
191 |         content: [
192 |           {
193 |             type: 'text',
194 |             text: 'Error: Failed to execute swift package clean\nDetails: spawn ENOENT',
195 |           },
196 |         ],
197 |         isError: true,
198 |       });
199 |     });
200 |   });
201 | });
202 | 
```

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

```bash
  1 | #!/bin/bash
  2 | 
  3 | # Build script for AXe artifacts
  4 | # This script downloads pre-built AXe artifacts from GitHub releases and bundles them
  5 | 
  6 | set -e
  7 | 
  8 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  9 | PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
 10 | BUNDLED_DIR="$PROJECT_ROOT/bundled"
 11 | AXE_LOCAL_DIR="/Volumes/Developer/AXe"
 12 | AXE_TEMP_DIR="/tmp/axe-download-$$"
 13 | 
 14 | echo "🔨 Preparing AXe artifacts for bundling..."
 15 | 
 16 | # Single source of truth for AXe version (overridable)
 17 | # 1) Use $AXE_VERSION if provided in env
 18 | # 2) Else, use repo-level pin from .axe-version if present
 19 | # 3) Else, fall back to default below
 20 | DEFAULT_AXE_VERSION="1.1.1"
 21 | VERSION_FILE="$PROJECT_ROOT/.axe-version"
 22 | if [ -n "${AXE_VERSION}" ]; then
 23 |     PINNED_AXE_VERSION="${AXE_VERSION}"
 24 | elif [ -f "$VERSION_FILE" ]; then
 25 |     PINNED_AXE_VERSION="$(cat "$VERSION_FILE" | tr -d ' \n\r')"
 26 | else
 27 |     PINNED_AXE_VERSION="$DEFAULT_AXE_VERSION"
 28 | fi
 29 | echo "📌 Using AXe version: $PINNED_AXE_VERSION"
 30 | 
 31 | # Clean up any existing bundled directory
 32 | if [ -d "$BUNDLED_DIR" ]; then
 33 |     echo "🧹 Cleaning existing bundled directory..."
 34 |     rm -rf "$BUNDLED_DIR"
 35 | fi
 36 | 
 37 | # Create bundled directory
 38 | mkdir -p "$BUNDLED_DIR"
 39 | 
 40 | # Use local AXe build if available (unless AXE_FORCE_REMOTE=1), otherwise download from GitHub releases
 41 | if [ -z "${AXE_FORCE_REMOTE}" ] && [ -d "$AXE_LOCAL_DIR" ] && [ -f "$AXE_LOCAL_DIR/Package.swift" ]; then
 42 |     echo "🏠 Using local AXe source at $AXE_LOCAL_DIR"
 43 |     cd "$AXE_LOCAL_DIR"
 44 | 
 45 |     # Build AXe in release configuration
 46 |     echo "🔨 Building AXe in release configuration..."
 47 |     swift build --configuration release
 48 | 
 49 |     # Check if build succeeded
 50 |     if [ ! -f ".build/release/axe" ]; then
 51 |         echo "❌ AXe build failed - binary not found"
 52 |         exit 1
 53 |     fi
 54 | 
 55 |     echo "✅ AXe build completed successfully"
 56 | 
 57 |     # Copy binary to bundled directory
 58 |     echo "📦 Copying AXe binary..."
 59 |     cp ".build/release/axe" "$BUNDLED_DIR/"
 60 | 
 61 |     # Fix rpath to find frameworks in Frameworks/ subdirectory
 62 |     echo "🔧 Configuring AXe binary rpath for bundled frameworks..."
 63 |     install_name_tool -add_rpath "@executable_path/Frameworks" "$BUNDLED_DIR/axe"
 64 | 
 65 |     # Create Frameworks directory and copy frameworks
 66 |     echo "📦 Copying frameworks..."
 67 |     mkdir -p "$BUNDLED_DIR/Frameworks"
 68 | 
 69 |     # Copy frameworks with better error handling
 70 |     for framework in .build/release/*.framework; do
 71 |         if [ -d "$framework" ]; then
 72 |             echo "📦 Copying framework: $(basename "$framework")"
 73 |             cp -r "$framework" "$BUNDLED_DIR/Frameworks/"
 74 | 
 75 |             # Only copy nested frameworks if they exist
 76 |             if [ -d "$framework/Frameworks" ]; then
 77 |                 echo "📦 Found nested frameworks in $(basename "$framework")"
 78 |                 cp -r "$framework/Frameworks"/* "$BUNDLED_DIR/Frameworks/" 2>/dev/null || true
 79 |             fi
 80 |         fi
 81 |     done
 82 | else
 83 |     echo "📥 Downloading latest AXe release from GitHub..."
 84 | 
 85 |     # Construct release download URL from pinned version
 86 |     AXE_RELEASE_URL="https://github.com/cameroncooke/AXe/releases/download/v${PINNED_AXE_VERSION}/AXe-macOS-v${PINNED_AXE_VERSION}.tar.gz"
 87 | 
 88 |     # Create temp directory
 89 |     mkdir -p "$AXE_TEMP_DIR"
 90 |     cd "$AXE_TEMP_DIR"
 91 | 
 92 |     # Download and extract the release
 93 |     echo "📥 Downloading AXe release archive ($AXE_RELEASE_URL)..."
 94 |     curl -L -o "axe-release.tar.gz" "$AXE_RELEASE_URL"
 95 | 
 96 |     echo "📦 Extracting AXe release archive..."
 97 |     tar -xzf "axe-release.tar.gz"
 98 | 
 99 |     # Find the extracted directory (might be named differently)
100 |     EXTRACTED_DIR=$(find . -type d -name "*AXe*" -o -name "*axe*" | head -1)
101 |     if [ -z "$EXTRACTED_DIR" ]; then
102 |         # If no AXe directory found, assume files are in current directory
103 |         EXTRACTED_DIR="."
104 |     fi
105 | 
106 |     cd "$EXTRACTED_DIR"
107 | 
108 |     # Copy binary
109 |     if [ -f "axe" ]; then
110 |         echo "📦 Copying AXe binary..."
111 |         cp "axe" "$BUNDLED_DIR/"
112 |         chmod +x "$BUNDLED_DIR/axe"
113 |     elif [ -f "bin/axe" ]; then
114 |         echo "📦 Copying AXe binary from bin/..."
115 |         cp "bin/axe" "$BUNDLED_DIR/"
116 |         chmod +x "$BUNDLED_DIR/axe"
117 |     else
118 |         echo "❌ AXe binary not found in release archive"
119 |         ls -la
120 |         exit 1
121 |     fi
122 | 
123 |     # Copy frameworks if they exist
124 |     echo "📦 Copying frameworks..."
125 |     mkdir -p "$BUNDLED_DIR/Frameworks"
126 | 
127 |     if [ -d "Frameworks" ]; then
128 |         cp -r Frameworks/* "$BUNDLED_DIR/Frameworks/"
129 |     elif [ -d "lib" ]; then
130 |         # Look for frameworks in lib directory
131 |         find lib -name "*.framework" -exec cp -r {} "$BUNDLED_DIR/Frameworks/" \;
132 |     else
133 |         echo "⚠️  No frameworks directory found in release archive"
134 |         echo "📂 Contents of release archive:"
135 |         find . -type f -name "*.framework" -o -name "*.dylib" | head -10
136 |     fi
137 | fi
138 | 
139 | # Verify frameworks were copied
140 | FRAMEWORK_COUNT=$(find "$BUNDLED_DIR/Frameworks" -name "*.framework" | wc -l)
141 | echo "📦 Copied $FRAMEWORK_COUNT frameworks"
142 | 
143 | # List the frameworks for verification
144 | echo "🔍 Bundled frameworks:"
145 | ls -la "$BUNDLED_DIR/Frameworks/"
146 | 
147 | # Verify binary can run with bundled frameworks
148 | echo "🧪 Testing bundled AXe binary..."
149 | if DYLD_FRAMEWORK_PATH="$BUNDLED_DIR/Frameworks" "$BUNDLED_DIR/axe" --version > /dev/null 2>&1; then
150 |     echo "✅ Bundled AXe binary test passed"
151 | else
152 |     echo "❌ Bundled AXe binary test failed"
153 |     exit 1
154 | fi
155 | 
156 | # Get AXe version for logging
157 | AXE_VERSION=$(DYLD_FRAMEWORK_PATH="$BUNDLED_DIR/Frameworks" "$BUNDLED_DIR/axe" --version 2>/dev/null || echo "unknown")
158 | echo "📋 AXe version: $AXE_VERSION"
159 | 
160 | # Clean up temp directory if it was used
161 | if [ -d "$AXE_TEMP_DIR" ]; then
162 |     echo "🧹 Cleaning up temporary files..."
163 |     rm -rf "$AXE_TEMP_DIR"
164 | fi
165 | 
166 | # Show final bundle size
167 | BUNDLE_SIZE=$(du -sh "$BUNDLED_DIR" | cut -f1)
168 | echo "📊 Final bundle size: $BUNDLE_SIZE"
169 | 
170 | echo "🎉 AXe bundling completed successfully!"
171 | echo "📁 Bundled artifacts location: $BUNDLED_DIR"
172 | 
```

--------------------------------------------------------------------------------
/build-plugins/plugin-discovery.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Plugin } from 'esbuild';
  2 | import { readdirSync, readFileSync, existsSync } from 'fs';
  3 | import { join } from 'path';
  4 | import path from 'path';
  5 | 
  6 | export interface WorkflowMetadata {
  7 |   name: string;
  8 |   description: string;
  9 |   platforms?: string[];
 10 |   targets?: string[];
 11 |   projectTypes?: string[];
 12 |   capabilities?: string[];
 13 | }
 14 | 
 15 | export function createPluginDiscoveryPlugin(): Plugin {
 16 |   return {
 17 |     name: 'plugin-discovery',
 18 |     setup(build) {
 19 |       // Generate the workflow loaders file before build starts
 20 |       build.onStart(async () => {
 21 |         try {
 22 |           await generateWorkflowLoaders();
 23 |         } catch (error) {
 24 |           console.error('Failed to generate workflow loaders:', error);
 25 |           throw error;
 26 |         }
 27 |       });
 28 |     }
 29 |   };
 30 | }
 31 | 
 32 | async function generateWorkflowLoaders(): Promise<void> {
 33 |   const pluginsDir = path.resolve(process.cwd(), 'src/plugins');
 34 |   
 35 |   if (!existsSync(pluginsDir)) {
 36 |     throw new Error(`Plugins directory not found: ${pluginsDir}`);
 37 |   }
 38 | 
 39 |   // Scan for workflow directories
 40 |   const workflowDirs = readdirSync(pluginsDir, { withFileTypes: true })
 41 |     .filter(dirent => dirent.isDirectory())
 42 |     .map(dirent => dirent.name);
 43 | 
 44 |   const workflowLoaders: Record<string, string> = {};
 45 |   const workflowMetadata: Record<string, WorkflowMetadata> = {};
 46 | 
 47 |   for (const dirName of workflowDirs) {
 48 |     const indexPath = join(pluginsDir, dirName, 'index.ts');
 49 |     
 50 |     // Check if workflow has index.ts file
 51 |     if (!existsSync(indexPath)) {
 52 |       console.warn(`Skipping ${dirName}: no index.ts file found`);
 53 |       continue;
 54 |     }
 55 | 
 56 |     // Try to extract workflow metadata from index.ts
 57 |     try {
 58 |       const indexContent = readFileSync(indexPath, 'utf8');
 59 |       const metadata = extractWorkflowMetadata(indexContent);
 60 |       
 61 |       if (metadata) {
 62 |         // Generate dynamic import for this workflow
 63 |         workflowLoaders[dirName] = `() => import('../plugins/${dirName}/index.js')`;
 64 |         workflowMetadata[dirName] = metadata;
 65 |         
 66 |         console.log(`✅ Discovered workflow: ${dirName} - ${metadata.name}`);
 67 |       } else {
 68 |         console.warn(`⚠️  Skipping ${dirName}: invalid workflow metadata`);
 69 |       }
 70 |     } catch (error) {
 71 |       console.warn(`⚠️  Error processing ${dirName}:`, error);
 72 |     }
 73 |   }
 74 | 
 75 |   // Generate the content for generated-plugins.ts
 76 |   const generatedContent = generatePluginsFileContent(workflowLoaders, workflowMetadata);
 77 |   
 78 |   // Write to the generated file
 79 |   const outputPath = path.resolve(process.cwd(), 'src/core/generated-plugins.ts');
 80 |   
 81 |   const fs = await import('fs');
 82 |   await fs.promises.writeFile(outputPath, generatedContent, 'utf8');
 83 |   
 84 |   console.log(`🔧 Generated workflow loaders for ${Object.keys(workflowLoaders).length} workflows`);
 85 | }
 86 | 
 87 | function extractWorkflowMetadata(content: string): WorkflowMetadata | null {
 88 |   try {
 89 |     // Simple regex to extract workflow export object
 90 |     const workflowMatch = content.match(/export\s+const\s+workflow\s*=\s*({[\s\S]*?});/);
 91 |     
 92 |     if (!workflowMatch) {
 93 |       return null;
 94 |     }
 95 | 
 96 |     const workflowObj = workflowMatch[1];
 97 |     
 98 |     // Extract name
 99 |     const nameMatch = workflowObj.match(/name\s*:\s*['"`]([^'"`]+)['"`]/);
100 |     if (!nameMatch) return null;
101 |     
102 |     // Extract description
103 |     const descMatch = workflowObj.match(/description\s*:\s*['"`]([\s\S]*?)['"`]/);
104 |     if (!descMatch) return null;
105 | 
106 |     // Extract platforms (optional)
107 |     const platformsMatch = workflowObj.match(/platforms\s*:\s*\[([^\]]*)\]/);
108 |     let platforms: string[] | undefined;
109 |     if (platformsMatch) {
110 |       platforms = platformsMatch[1]
111 |         .split(',')
112 |         .map(p => p.trim().replace(/['"]/g, ''))
113 |         .filter(p => p.length > 0);
114 |     }
115 | 
116 |     // Extract targets (optional)
117 |     const targetsMatch = workflowObj.match(/targets\s*:\s*\[([^\]]*)\]/);
118 |     let targets: string[] | undefined;
119 |     if (targetsMatch) {
120 |       targets = targetsMatch[1]
121 |         .split(',')
122 |         .map(t => t.trim().replace(/['"]/g, ''))
123 |         .filter(t => t.length > 0);
124 |     }
125 | 
126 |     // Extract projectTypes (optional)
127 |     const projectTypesMatch = workflowObj.match(/projectTypes\s*:\s*\[([^\]]*)\]/);
128 |     let projectTypes: string[] | undefined;
129 |     if (projectTypesMatch) {
130 |       projectTypes = projectTypesMatch[1]
131 |         .split(',')
132 |         .map(pt => pt.trim().replace(/['"]/g, ''))
133 |         .filter(pt => pt.length > 0);
134 |     }
135 | 
136 |     // Extract capabilities (optional)
137 |     const capabilitiesMatch = workflowObj.match(/capabilities\s*:\s*\[([^\]]*)\]/);
138 |     let capabilities: string[] | undefined;
139 |     if (capabilitiesMatch) {
140 |       capabilities = capabilitiesMatch[1]
141 |         .split(',')
142 |         .map(c => c.trim().replace(/['"]/g, ''))
143 |         .filter(c => c.length > 0);
144 |     }
145 | 
146 |     return {
147 |       name: nameMatch[1],
148 |       description: descMatch[1],
149 |       platforms,
150 |       targets,
151 |       projectTypes,
152 |       capabilities
153 |     };
154 |   } catch (error) {
155 |     console.warn('Failed to extract workflow metadata:', error);
156 |     return null;
157 |   }
158 | }
159 | 
160 | function generatePluginsFileContent(
161 |   workflowLoaders: Record<string, string>,
162 |   workflowMetadata: Record<string, WorkflowMetadata>
163 | ): string {
164 |   const loaderEntries = Object.entries(workflowLoaders)
165 |     .map(([key, loader]) => `  '${key}': ${loader}`)
166 |     .join(',\n');
167 | 
168 |   const metadataEntries = Object.entries(workflowMetadata)
169 |     .map(([key, metadata]) => {
170 |       const metadataJson = JSON.stringify(metadata, null, 4)
171 |         .split('\n')
172 |         .map(line => `    ${line}`)
173 |         .join('\n');
174 |       return `  '${key}': ${metadataJson.trim()}`;
175 |     })
176 |     .join(',\n');
177 | 
178 |   return `// AUTO-GENERATED - DO NOT EDIT
179 | // This file is generated by the plugin discovery esbuild plugin
180 | 
181 | // Generated based on filesystem scan
182 | export const WORKFLOW_LOADERS = {
183 | ${loaderEntries}
184 | };
185 | 
186 | export type WorkflowName = keyof typeof WORKFLOW_LOADERS;
187 | 
188 | // Optional: Export workflow metadata for quick access
189 | export const WORKFLOW_METADATA = {
190 | ${metadataEntries}
191 | };
192 | `;
193 | }
```

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

```typescript
  1 | import { z } from 'zod';
  2 | import { ToolResponse } from '../../../types/common.ts';
  3 | import { log } from '../../../utils/logging/index.ts';
  4 | import { createErrorResponse } from '../../../utils/responses/index.ts';
  5 | import { DependencyError, AxeError, SystemError } from '../../../utils/errors.ts';
  6 | import type { CommandExecutor } from '../../../utils/execution/index.ts';
  7 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
  8 | import {
  9 |   createAxeNotAvailableResponse,
 10 |   getAxePath,
 11 |   getBundledAxeEnvironment,
 12 | } from '../../../utils/axe-helpers.ts';
 13 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
 14 | 
 15 | // Define schema as ZodObject
 16 | const describeUiSchema = z.object({
 17 |   simulatorUuid: z.string().uuid('Invalid Simulator UUID format'),
 18 | });
 19 | 
 20 | // Use z.infer for type safety
 21 | type DescribeUiParams = z.infer<typeof describeUiSchema>;
 22 | 
 23 | export interface AxeHelpers {
 24 |   getAxePath: () => string | null;
 25 |   getBundledAxeEnvironment: () => Record<string, string>;
 26 |   createAxeNotAvailableResponse: () => ToolResponse;
 27 | }
 28 | 
 29 | const LOG_PREFIX = '[AXe]';
 30 | 
 31 | // Session tracking for describe_ui warnings (shared across UI tools)
 32 | const describeUITimestamps = new Map();
 33 | 
 34 | function recordDescribeUICall(simulatorUuid: string): void {
 35 |   describeUITimestamps.set(simulatorUuid, {
 36 |     timestamp: Date.now(),
 37 |     simulatorUuid,
 38 |   });
 39 | }
 40 | 
 41 | /**
 42 |  * Core business logic for describe_ui functionality
 43 |  */
 44 | export async function describe_uiLogic(
 45 |   params: DescribeUiParams,
 46 |   executor: CommandExecutor,
 47 |   axeHelpers: AxeHelpers = {
 48 |     getAxePath,
 49 |     getBundledAxeEnvironment,
 50 |     createAxeNotAvailableResponse,
 51 |   },
 52 | ): Promise<ToolResponse> {
 53 |   const toolName = 'describe_ui';
 54 |   const { simulatorUuid } = params;
 55 |   const commandArgs = ['describe-ui'];
 56 | 
 57 |   log('info', `${LOG_PREFIX}/${toolName}: Starting for ${simulatorUuid}`);
 58 | 
 59 |   try {
 60 |     const responseText = await executeAxeCommand(
 61 |       commandArgs,
 62 |       simulatorUuid,
 63 |       'describe-ui',
 64 |       executor,
 65 |       axeHelpers,
 66 |     );
 67 | 
 68 |     // Record the describe_ui call for warning system
 69 |     recordDescribeUICall(simulatorUuid);
 70 | 
 71 |     log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`);
 72 |     return {
 73 |       content: [
 74 |         {
 75 |           type: 'text',
 76 |           text:
 77 |             'Accessibility hierarchy retrieved successfully:\n```json\n' + responseText + '\n```',
 78 |         },
 79 |         {
 80 |           type: 'text',
 81 |           text: `Next Steps:
 82 | - Use frame coordinates for tap/swipe (center: x+width/2, y+height/2)
 83 | - Re-run describe_ui after layout changes
 84 | - Screenshots are for visual verification only`,
 85 |         },
 86 |       ],
 87 |     };
 88 |   } catch (error) {
 89 |     log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
 90 |     if (error instanceof DependencyError) {
 91 |       return axeHelpers.createAxeNotAvailableResponse();
 92 |     } else if (error instanceof AxeError) {
 93 |       return createErrorResponse(
 94 |         `Failed to get accessibility hierarchy: ${error.message}`,
 95 |         error.axeOutput,
 96 |       );
 97 |     } else if (error instanceof SystemError) {
 98 |       return createErrorResponse(
 99 |         `System error executing axe: ${error.message}`,
100 |         error.originalError?.stack,
101 |       );
102 |     }
103 |     return createErrorResponse(
104 |       `An unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`,
105 |     );
106 |   }
107 | }
108 | 
109 | export default {
110 |   name: 'describe_ui',
111 |   description:
112 |     'Gets entire view hierarchy with precise frame coordinates (x, y, width, height) for all visible elements. Use this before UI interactions or after layout changes - do NOT guess coordinates from screenshots. Returns JSON tree with frame data for accurate automation.',
113 |   schema: describeUiSchema.shape, // MCP SDK compatibility
114 |   handler: createTypedTool(
115 |     describeUiSchema,
116 |     (params: DescribeUiParams, executor: CommandExecutor) => {
117 |       return describe_uiLogic(params, executor, {
118 |         getAxePath,
119 |         getBundledAxeEnvironment,
120 |         createAxeNotAvailableResponse,
121 |       });
122 |     },
123 |     getDefaultCommandExecutor,
124 |   ),
125 | };
126 | 
127 | // Helper function for executing axe commands (inlined from src/tools/axe/index.ts)
128 | async function executeAxeCommand(
129 |   commandArgs: string[],
130 |   simulatorUuid: string,
131 |   commandName: string,
132 |   executor: CommandExecutor = getDefaultCommandExecutor(),
133 |   axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse },
134 | ): Promise<string> {
135 |   // Get the appropriate axe binary path
136 |   const axeBinary = axeHelpers.getAxePath();
137 |   if (!axeBinary) {
138 |     throw new DependencyError('AXe binary not found');
139 |   }
140 | 
141 |   // Add --udid parameter to all commands
142 |   const fullArgs = [...commandArgs, '--udid', simulatorUuid];
143 | 
144 |   // Construct the full command array with the axe binary as the first element
145 |   const fullCommand = [axeBinary, ...fullArgs];
146 | 
147 |   try {
148 |     // Determine environment variables for bundled AXe
149 |     const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined;
150 | 
151 |     const result = await executor(fullCommand, `${LOG_PREFIX}: ${commandName}`, false, axeEnv);
152 | 
153 |     if (!result.success) {
154 |       throw new AxeError(
155 |         `axe command '${commandName}' failed.`,
156 |         commandName,
157 |         result.error ?? result.output,
158 |         simulatorUuid,
159 |       );
160 |     }
161 | 
162 |     // Check for stderr output in successful commands
163 |     if (result.error) {
164 |       log(
165 |         'warn',
166 |         `${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${result.error}`,
167 |       );
168 |     }
169 | 
170 |     return result.output.trim();
171 |   } catch (error) {
172 |     if (error instanceof Error) {
173 |       if (error instanceof AxeError) {
174 |         throw error;
175 |       }
176 | 
177 |       // Otherwise wrap it in a SystemError
178 |       throw new SystemError(`Failed to execute axe command: ${error.message}`, error);
179 |     }
180 | 
181 |     // For any other type of error
182 |     throw new SystemError(`Failed to execute axe command: ${String(error)}`);
183 |   }
184 | }
185 | 
```

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

```typescript
  1 | /**
  2 |  * Pure dependency injection test for stop_mac_app plugin
  3 |  *
  4 |  * Tests plugin structure and macOS app stopping functionality including parameter validation,
  5 |  * command generation, and response formatting.
  6 |  *
  7 |  * Uses manual call tracking instead of vitest mocking.
  8 |  * NO VITEST MOCKING ALLOWED - Only manual stubs
  9 |  */
 10 | 
 11 | import { describe, it, expect } from 'vitest';
 12 | import { z } from 'zod';
 13 | 
 14 | import stopMacApp, { stop_mac_appLogic } from '../stop_mac_app.ts';
 15 | 
 16 | describe('stop_mac_app plugin', () => {
 17 |   describe('Export Field Validation (Literal)', () => {
 18 |     it('should have correct name', () => {
 19 |       expect(stopMacApp.name).toBe('stop_mac_app');
 20 |     });
 21 | 
 22 |     it('should have correct description', () => {
 23 |       expect(stopMacApp.description).toBe(
 24 |         'Stops a running macOS application. Can stop by app name or process ID.',
 25 |       );
 26 |     });
 27 | 
 28 |     it('should have handler function', () => {
 29 |       expect(typeof stopMacApp.handler).toBe('function');
 30 |     });
 31 | 
 32 |     it('should validate schema correctly', () => {
 33 |       // Test optional fields
 34 |       expect(stopMacApp.schema.appName.safeParse('Calculator').success).toBe(true);
 35 |       expect(stopMacApp.schema.appName.safeParse(undefined).success).toBe(true);
 36 |       expect(stopMacApp.schema.processId.safeParse(1234).success).toBe(true);
 37 |       expect(stopMacApp.schema.processId.safeParse(undefined).success).toBe(true);
 38 | 
 39 |       // Test invalid inputs
 40 |       expect(stopMacApp.schema.appName.safeParse(null).success).toBe(false);
 41 |       expect(stopMacApp.schema.processId.safeParse('not-number').success).toBe(false);
 42 |       expect(stopMacApp.schema.processId.safeParse(null).success).toBe(false);
 43 |     });
 44 |   });
 45 | 
 46 |   describe('Input Validation', () => {
 47 |     it('should return exact validation error for missing parameters', async () => {
 48 |       const mockExecutor = async () => ({ success: true, output: '', process: {} as any });
 49 |       const result = await stop_mac_appLogic({}, mockExecutor);
 50 | 
 51 |       expect(result).toEqual({
 52 |         content: [
 53 |           {
 54 |             type: 'text',
 55 |             text: 'Either appName or processId must be provided.',
 56 |           },
 57 |         ],
 58 |         isError: true,
 59 |       });
 60 |     });
 61 |   });
 62 | 
 63 |   describe('Command Generation', () => {
 64 |     it('should generate correct command for process ID', async () => {
 65 |       const calls: any[] = [];
 66 |       const mockExecutor = async (command: string[]) => {
 67 |         calls.push({ command });
 68 |         return { success: true, output: '', process: {} as any };
 69 |       };
 70 | 
 71 |       await stop_mac_appLogic(
 72 |         {
 73 |           processId: 1234,
 74 |         },
 75 |         mockExecutor,
 76 |       );
 77 | 
 78 |       expect(calls).toHaveLength(1);
 79 |       expect(calls[0].command).toEqual(['kill', '1234']);
 80 |     });
 81 | 
 82 |     it('should generate correct command for app name', async () => {
 83 |       const calls: any[] = [];
 84 |       const mockExecutor = async (command: string[]) => {
 85 |         calls.push({ command });
 86 |         return { success: true, output: '', process: {} as any };
 87 |       };
 88 | 
 89 |       await stop_mac_appLogic(
 90 |         {
 91 |           appName: 'Calculator',
 92 |         },
 93 |         mockExecutor,
 94 |       );
 95 | 
 96 |       expect(calls).toHaveLength(1);
 97 |       expect(calls[0].command).toEqual([
 98 |         'sh',
 99 |         '-c',
100 |         'pkill -f "Calculator" || osascript -e \'tell application "Calculator" to quit\'',
101 |       ]);
102 |     });
103 | 
104 |     it('should prioritize processId over appName', async () => {
105 |       const calls: any[] = [];
106 |       const mockExecutor = async (command: string[]) => {
107 |         calls.push({ command });
108 |         return { success: true, output: '', process: {} as any };
109 |       };
110 | 
111 |       await stop_mac_appLogic(
112 |         {
113 |           appName: 'Calculator',
114 |           processId: 1234,
115 |         },
116 |         mockExecutor,
117 |       );
118 | 
119 |       expect(calls).toHaveLength(1);
120 |       expect(calls[0].command).toEqual(['kill', '1234']);
121 |     });
122 |   });
123 | 
124 |   describe('Response Processing', () => {
125 |     it('should return exact successful stop response by app name', async () => {
126 |       const mockExecutor = async () => ({ success: true, output: '', process: {} as any });
127 | 
128 |       const result = await stop_mac_appLogic(
129 |         {
130 |           appName: 'Calculator',
131 |         },
132 |         mockExecutor,
133 |       );
134 | 
135 |       expect(result).toEqual({
136 |         content: [
137 |           {
138 |             type: 'text',
139 |             text: '✅ macOS app stopped successfully: Calculator',
140 |           },
141 |         ],
142 |       });
143 |     });
144 | 
145 |     it('should return exact successful stop response by process ID', async () => {
146 |       const mockExecutor = async () => ({ success: true, output: '', process: {} as any });
147 | 
148 |       const result = await stop_mac_appLogic(
149 |         {
150 |           processId: 1234,
151 |         },
152 |         mockExecutor,
153 |       );
154 | 
155 |       expect(result).toEqual({
156 |         content: [
157 |           {
158 |             type: 'text',
159 |             text: '✅ macOS app stopped successfully: PID 1234',
160 |           },
161 |         ],
162 |       });
163 |     });
164 | 
165 |     it('should return exact successful stop response with both parameters (processId takes precedence)', async () => {
166 |       const mockExecutor = async () => ({ success: true, output: '', process: {} as any });
167 | 
168 |       const result = await stop_mac_appLogic(
169 |         {
170 |           appName: 'Calculator',
171 |           processId: 1234,
172 |         },
173 |         mockExecutor,
174 |       );
175 | 
176 |       expect(result).toEqual({
177 |         content: [
178 |           {
179 |             type: 'text',
180 |             text: '✅ macOS app stopped successfully: PID 1234',
181 |           },
182 |         ],
183 |       });
184 |     });
185 | 
186 |     it('should handle execution errors', async () => {
187 |       const mockExecutor = async () => {
188 |         throw new Error('Process not found');
189 |       };
190 | 
191 |       const result = await stop_mac_appLogic(
192 |         {
193 |           processId: 9999,
194 |         },
195 |         mockExecutor,
196 |       );
197 | 
198 |       expect(result).toEqual({
199 |         content: [
200 |           {
201 |             type: 'text',
202 |             text: '❌ Stop macOS app operation failed: Process not found',
203 |           },
204 |         ],
205 |         isError: true,
206 |       });
207 |     });
208 |   });
209 | });
210 | 
```

--------------------------------------------------------------------------------
/src/utils/tool-registry.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { McpServer, RegisteredTool } from '@camsoft/mcp-sdk/server/mcp.js';
  2 | import { loadPlugins } from '../core/plugin-registry.ts';
  3 | import { ToolResponse } from '../types/common.ts';
  4 | import { log } from './logger.ts';
  5 | 
  6 | // Global registry to track registered tools for cleanup
  7 | const toolRegistry = new Map<string, RegisteredTool>();
  8 | 
  9 | /**
 10 |  * Register a tool and track it for potential removal
 11 |  */
 12 | export function registerAndTrackTool(
 13 |   server: McpServer,
 14 |   name: string,
 15 |   config: Parameters<McpServer['registerTool']>[1],
 16 |   callback: Parameters<McpServer['registerTool']>[2],
 17 | ): RegisteredTool {
 18 |   const registeredTool = server.registerTool(name, config, callback);
 19 |   toolRegistry.set(name, registeredTool);
 20 |   return registeredTool;
 21 | }
 22 | 
 23 | /**
 24 |  * Register multiple tools and track them for potential removal
 25 |  */
 26 | export function registerAndTrackTools(
 27 |   server: McpServer,
 28 |   tools: Parameters<McpServer['registerTools']>[0],
 29 | ): RegisteredTool[] {
 30 |   const registeredTools = server.registerTools(tools);
 31 | 
 32 |   // Track each registered tool
 33 |   tools.forEach((tool, index) => {
 34 |     if (registeredTools[index]) {
 35 |       toolRegistry.set(tool.name, registeredTools[index]);
 36 |     }
 37 |   });
 38 | 
 39 |   return registeredTools;
 40 | }
 41 | 
 42 | /**
 43 |  * Check if a tool is already registered
 44 |  */
 45 | export function isToolRegistered(name: string): boolean {
 46 |   return toolRegistry.has(name);
 47 | }
 48 | 
 49 | /**
 50 |  * Remove a specific tracked tool by name
 51 |  */
 52 | export function removeTrackedTool(name: string): boolean {
 53 |   const tool = toolRegistry.get(name);
 54 |   if (!tool) {
 55 |     return false;
 56 |   }
 57 | 
 58 |   try {
 59 |     tool.remove();
 60 |     toolRegistry.delete(name);
 61 |     log('debug', `✅ Removed tool: ${name}`);
 62 |     return true;
 63 |   } catch (error) {
 64 |     log('error', `❌ Failed to remove tool ${name}: ${error}`);
 65 |     return false;
 66 |   }
 67 | }
 68 | 
 69 | /**
 70 |  * Remove multiple tracked tools by names
 71 |  */
 72 | export function removeTrackedTools(names: string[]): string[] {
 73 |   const removedTools: string[] = [];
 74 | 
 75 |   for (const name of names) {
 76 |     if (removeTrackedTool(name)) {
 77 |       removedTools.push(name);
 78 |     }
 79 |   }
 80 | 
 81 |   return removedTools;
 82 | }
 83 | 
 84 | /**
 85 |  * Remove all currently tracked tools
 86 |  */
 87 | export function removeAllTrackedTools(): void {
 88 |   const toolNames = Array.from(toolRegistry.keys());
 89 | 
 90 |   if (toolNames.length === 0) {
 91 |     return;
 92 |   }
 93 | 
 94 |   log('info', `Removing ${toolNames.length} tracked tools...`);
 95 | 
 96 |   const removedTools = removeTrackedTools(toolNames);
 97 |   log('info', `✅ Removed ${removedTools.length} tracked tools`);
 98 | }
 99 | 
100 | /**
101 |  * Get the number of currently tracked tools
102 |  */
103 | export function getTrackedToolCount(): number {
104 |   return toolRegistry.size;
105 | }
106 | 
107 | /**
108 |  * Get the names of currently tracked tools
109 |  */
110 | export function getTrackedToolNames(): string[] {
111 |   return Array.from(toolRegistry.keys());
112 | }
113 | 
114 | /**
115 |  * Register only discovery tools (discover_tools, discover_projs) with tracking
116 |  */
117 | export async function registerDiscoveryTools(server: McpServer): Promise<void> {
118 |   const plugins = await loadPlugins();
119 |   let registeredCount = 0;
120 | 
121 |   // Only register discovery tools initially
122 |   const discoveryTools = [];
123 |   for (const plugin of plugins.values()) {
124 |     // Only load discover_tools and discover_projs initially - other tools will be loaded via workflows
125 |     if (plugin.name === 'discover_tools' || plugin.name === 'discover_projs') {
126 |       discoveryTools.push({
127 |         name: plugin.name,
128 |         config: {
129 |           description: plugin.description ?? '',
130 |           inputSchema: plugin.schema,
131 |         },
132 |         // Adapt callback to match SDK's expected signature
133 |         callback: (args: unknown): Promise<ToolResponse> =>
134 |           plugin.handler(args as Record<string, unknown>),
135 |       });
136 |       registeredCount++;
137 |     }
138 |   }
139 | 
140 |   // Register discovery tools using bulk registration with tracking
141 |   if (discoveryTools.length > 0) {
142 |     registerAndTrackTools(server, discoveryTools);
143 |   }
144 | 
145 |   log('info', `✅ Registered ${registeredCount} discovery tools in dynamic mode.`);
146 | }
147 | 
148 | /**
149 |  * Register selected workflows based on environment variable
150 |  */
151 | export async function registerSelectedWorkflows(
152 |   server: McpServer,
153 |   workflowNames: string[],
154 | ): Promise<void> {
155 |   const { loadWorkflowGroups } = await import('../core/plugin-registry.js');
156 |   const workflowGroups = await loadWorkflowGroups();
157 |   const selectedTools = [];
158 | 
159 |   for (const workflowName of workflowNames) {
160 |     const workflow = workflowGroups.get(workflowName.trim());
161 |     if (workflow) {
162 |       for (const tool of workflow.tools) {
163 |         selectedTools.push({
164 |           name: tool.name,
165 |           config: {
166 |             description: tool.description ?? '',
167 |             inputSchema: tool.schema,
168 |           },
169 |           callback: (args: unknown): Promise<ToolResponse> =>
170 |             tool.handler(args as Record<string, unknown>),
171 |         });
172 |       }
173 |     }
174 |   }
175 | 
176 |   if (selectedTools.length > 0) {
177 |     server.registerTools(selectedTools);
178 |   }
179 | 
180 |   log(
181 |     'info',
182 |     `✅ Registered ${selectedTools.length} tools from workflows: ${workflowNames.join(', ')}`,
183 |   );
184 | }
185 | 
186 | /**
187 |  * Register all tools (static mode) - no tracking needed since these won't be removed
188 |  */
189 | export async function registerAllToolsStatic(server: McpServer): Promise<void> {
190 |   const plugins = await loadPlugins();
191 |   const allTools = [];
192 | 
193 |   for (const plugin of plugins.values()) {
194 |     // Exclude discovery tools in static mode - they should only be available in dynamic mode
195 |     if (plugin.name === 'discover_tools') {
196 |       continue;
197 |     }
198 | 
199 |     allTools.push({
200 |       name: plugin.name,
201 |       config: {
202 |         description: plugin.description ?? '',
203 |         inputSchema: plugin.schema,
204 |       },
205 |       // Adapt callback to match SDK's expected signature
206 |       callback: (args: unknown): Promise<ToolResponse> =>
207 |         plugin.handler(args as Record<string, unknown>),
208 |     });
209 |   }
210 | 
211 |   // Register all tools using bulk registration (no tracking since static tools aren't removed)
212 |   if (allTools.length > 0) {
213 |     server.registerTools(allTools);
214 |   }
215 | 
216 |   log('info', `✅ Registered ${allTools.length} tools in static mode.`);
217 | }
218 | 
```

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

```typescript
  1 | import { describe, it, expect } from 'vitest';
  2 | import { determineSimulatorUuid } from '../simulator-utils.ts';
  3 | import { createMockExecutor } from '../../test-utils/mock-executors.ts';
  4 | 
  5 | describe('determineSimulatorUuid', () => {
  6 |   const mockSimulatorListOutput = JSON.stringify({
  7 |     devices: {
  8 |       'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
  9 |         {
 10 |           udid: 'ABC-123-UUID',
 11 |           name: 'iPhone 16',
 12 |           isAvailable: true,
 13 |         },
 14 |         {
 15 |           udid: 'DEF-456-UUID',
 16 |           name: 'iPhone 15',
 17 |           isAvailable: false,
 18 |         },
 19 |       ],
 20 |       'com.apple.CoreSimulator.SimRuntime.iOS-16-0': [
 21 |         {
 22 |           udid: 'GHI-789-UUID',
 23 |           name: 'iPhone 14',
 24 |           isAvailable: true,
 25 |         },
 26 |       ],
 27 |     },
 28 |   });
 29 | 
 30 |   describe('UUID provided directly', () => {
 31 |     it('should return UUID when simulatorUuid is provided', async () => {
 32 |       const mockExecutor = createMockExecutor(
 33 |         new Error('Should not call executor when UUID provided'),
 34 |       );
 35 | 
 36 |       const result = await determineSimulatorUuid(
 37 |         { simulatorUuid: 'DIRECT-UUID-123' },
 38 |         mockExecutor,
 39 |       );
 40 | 
 41 |       expect(result.uuid).toBe('DIRECT-UUID-123');
 42 |       expect(result.warning).toBeUndefined();
 43 |       expect(result.error).toBeUndefined();
 44 |     });
 45 | 
 46 |     it('should prefer simulatorUuid when both UUID and name are provided', async () => {
 47 |       const mockExecutor = createMockExecutor(
 48 |         new Error('Should not call executor when UUID provided'),
 49 |       );
 50 | 
 51 |       const result = await determineSimulatorUuid(
 52 |         { simulatorUuid: 'DIRECT-UUID', simulatorName: 'iPhone 16' },
 53 |         mockExecutor,
 54 |       );
 55 | 
 56 |       expect(result.uuid).toBe('DIRECT-UUID');
 57 |     });
 58 |   });
 59 | 
 60 |   describe('Name that looks like UUID', () => {
 61 |     it('should detect and use UUID-like name directly', async () => {
 62 |       const mockExecutor = createMockExecutor(
 63 |         new Error('Should not call executor for UUID-like name'),
 64 |       );
 65 |       const uuidLikeName = '12345678-1234-1234-1234-123456789abc';
 66 | 
 67 |       const result = await determineSimulatorUuid({ simulatorName: uuidLikeName }, mockExecutor);
 68 | 
 69 |       expect(result.uuid).toBe(uuidLikeName);
 70 |       expect(result.warning).toContain('appears to be a UUID');
 71 |       expect(result.error).toBeUndefined();
 72 |     });
 73 | 
 74 |     it('should detect uppercase UUID-like name', async () => {
 75 |       const mockExecutor = createMockExecutor(
 76 |         new Error('Should not call executor for UUID-like name'),
 77 |       );
 78 |       const uuidLikeName = '12345678-1234-1234-1234-123456789ABC';
 79 | 
 80 |       const result = await determineSimulatorUuid({ simulatorName: uuidLikeName }, mockExecutor);
 81 | 
 82 |       expect(result.uuid).toBe(uuidLikeName);
 83 |       expect(result.warning).toContain('appears to be a UUID');
 84 |     });
 85 |   });
 86 | 
 87 |   describe('Name resolution via simctl', () => {
 88 |     it('should resolve name to UUID for available simulator', async () => {
 89 |       const mockExecutor = createMockExecutor({
 90 |         success: true,
 91 |         output: mockSimulatorListOutput,
 92 |       });
 93 | 
 94 |       const result = await determineSimulatorUuid({ simulatorName: 'iPhone 16' }, mockExecutor);
 95 | 
 96 |       expect(result.uuid).toBe('ABC-123-UUID');
 97 |       expect(result.warning).toBeUndefined();
 98 |       expect(result.error).toBeUndefined();
 99 |     });
100 | 
101 |     it('should find simulator across different runtimes', async () => {
102 |       const mockExecutor = createMockExecutor({
103 |         success: true,
104 |         output: mockSimulatorListOutput,
105 |       });
106 | 
107 |       const result = await determineSimulatorUuid({ simulatorName: 'iPhone 14' }, mockExecutor);
108 | 
109 |       expect(result.uuid).toBe('GHI-789-UUID');
110 |       expect(result.error).toBeUndefined();
111 |     });
112 | 
113 |     it('should error for unavailable simulator', async () => {
114 |       const mockExecutor = createMockExecutor({
115 |         success: true,
116 |         output: mockSimulatorListOutput,
117 |       });
118 | 
119 |       const result = await determineSimulatorUuid({ simulatorName: 'iPhone 15' }, mockExecutor);
120 | 
121 |       expect(result.uuid).toBeUndefined();
122 |       expect(result.error).toBeDefined();
123 |       expect(result.error?.content[0].text).toContain('exists but is not available');
124 |     });
125 | 
126 |     it('should error for non-existent simulator', async () => {
127 |       const mockExecutor = createMockExecutor({
128 |         success: true,
129 |         output: mockSimulatorListOutput,
130 |       });
131 | 
132 |       const result = await determineSimulatorUuid({ simulatorName: 'iPhone 99' }, mockExecutor);
133 | 
134 |       expect(result.uuid).toBeUndefined();
135 |       expect(result.error).toBeDefined();
136 |       expect(result.error?.content[0].text).toContain('not found');
137 |     });
138 | 
139 |     it('should handle simctl list failure', async () => {
140 |       const mockExecutor = createMockExecutor({
141 |         success: false,
142 |         error: 'simctl command failed',
143 |       });
144 | 
145 |       const result = await determineSimulatorUuid({ simulatorName: 'iPhone 16' }, mockExecutor);
146 | 
147 |       expect(result.uuid).toBeUndefined();
148 |       expect(result.error).toBeDefined();
149 |       expect(result.error?.content[0].text).toContain('Failed to list simulators');
150 |     });
151 | 
152 |     it('should handle invalid JSON from simctl', async () => {
153 |       const mockExecutor = createMockExecutor({
154 |         success: true,
155 |         output: 'invalid json {',
156 |       });
157 | 
158 |       const result = await determineSimulatorUuid({ simulatorName: 'iPhone 16' }, mockExecutor);
159 | 
160 |       expect(result.uuid).toBeUndefined();
161 |       expect(result.error).toBeDefined();
162 |       expect(result.error?.content[0].text).toContain('Failed to parse simulator list');
163 |     });
164 |   });
165 | 
166 |   describe('No identifier provided', () => {
167 |     it('should error when neither UUID nor name is provided', async () => {
168 |       const mockExecutor = createMockExecutor(
169 |         new Error('Should not call executor when no identifier'),
170 |       );
171 | 
172 |       const result = await determineSimulatorUuid({}, mockExecutor);
173 | 
174 |       expect(result.uuid).toBeUndefined();
175 |       expect(result.error).toBeDefined();
176 |       expect(result.error?.content[0].text).toContain('No simulator identifier provided');
177 |     });
178 |   });
179 | });
180 | 
```

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

```typescript
  1 | /**
  2 |  * Simulator Build Plugin: Build Simulator (Unified)
  3 |  *
  4 |  * Builds an app from a project or workspace for a specific simulator by UUID or name.
  5 |  * Accepts mutually exclusive `projectPath` or `workspacePath`.
  6 |  * Accepts mutually exclusive `simulatorId` or `simulatorName`.
  7 |  */
  8 | 
  9 | import { z } from 'zod';
 10 | import { log } from '../../../utils/logging/index.ts';
 11 | import { executeXcodeBuildCommand } from '../../../utils/build/index.ts';
 12 | import { ToolResponse, XcodePlatform } from '../../../types/common.ts';
 13 | import type { CommandExecutor } from '../../../utils/execution/index.ts';
 14 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
 15 | import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
 16 | import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
 17 | 
 18 | // Unified schema: XOR between projectPath and workspacePath, and XOR between simulatorId and simulatorName
 19 | const baseOptions = {
 20 |   scheme: z.string().describe('The scheme to use (Required)'),
 21 |   simulatorId: z
 22 |     .string()
 23 |     .optional()
 24 |     .describe(
 25 |       'UUID of the simulator (from list_sims). Provide EITHER this OR simulatorName, not both',
 26 |     ),
 27 |   simulatorName: z
 28 |     .string()
 29 |     .optional()
 30 |     .describe(
 31 |       "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both",
 32 |     ),
 33 |   configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
 34 |   derivedDataPath: z
 35 |     .string()
 36 |     .optional()
 37 |     .describe('Path where build products and other derived data will go'),
 38 |   extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'),
 39 |   useLatestOS: z
 40 |     .boolean()
 41 |     .optional()
 42 |     .describe('Whether to use the latest OS version for the named simulator'),
 43 |   preferXcodebuild: z
 44 |     .boolean()
 45 |     .optional()
 46 |     .describe(
 47 |       'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.',
 48 |     ),
 49 | };
 50 | 
 51 | const baseSchemaObject = z.object({
 52 |   projectPath: z
 53 |     .string()
 54 |     .optional()
 55 |     .describe('Path to .xcodeproj file. Provide EITHER this OR workspacePath, not both'),
 56 |   workspacePath: z
 57 |     .string()
 58 |     .optional()
 59 |     .describe('Path to .xcworkspace file. Provide EITHER this OR projectPath, not both'),
 60 |   ...baseOptions,
 61 | });
 62 | 
 63 | const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);
 64 | 
 65 | const buildSimulatorSchema = baseSchema
 66 |   .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
 67 |     message: 'Either projectPath or workspacePath is required.',
 68 |   })
 69 |   .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
 70 |     message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
 71 |   })
 72 |   .refine((val) => val.simulatorId !== undefined || val.simulatorName !== undefined, {
 73 |     message: 'Either simulatorId or simulatorName is required.',
 74 |   })
 75 |   .refine((val) => !(val.simulatorId !== undefined && val.simulatorName !== undefined), {
 76 |     message: 'simulatorId and simulatorName are mutually exclusive. Provide only one.',
 77 |   });
 78 | 
 79 | export type BuildSimulatorParams = z.infer<typeof buildSimulatorSchema>;
 80 | 
 81 | // Internal logic for building Simulator apps.
 82 | async function _handleSimulatorBuildLogic(
 83 |   params: BuildSimulatorParams,
 84 |   executor: CommandExecutor = getDefaultCommandExecutor(),
 85 | ): Promise<ToolResponse> {
 86 |   const projectType = params.projectPath ? 'project' : 'workspace';
 87 |   const filePath = params.projectPath ?? params.workspacePath;
 88 | 
 89 |   // Log warning if useLatestOS is provided with simulatorId
 90 |   if (params.simulatorId && params.useLatestOS !== undefined) {
 91 |     log(
 92 |       'warning',
 93 |       `useLatestOS parameter is ignored when using simulatorId (UUID implies exact device/OS)`,
 94 |     );
 95 |   }
 96 | 
 97 |   log(
 98 |     'info',
 99 |     `Starting iOS Simulator build for scheme ${params.scheme} from ${projectType}: ${filePath}`,
100 |   );
101 | 
102 |   // Ensure configuration has a default value for SharedBuildParams compatibility
103 |   const sharedBuildParams = {
104 |     ...params,
105 |     configuration: params.configuration ?? 'Debug',
106 |   };
107 | 
108 |   // executeXcodeBuildCommand handles both simulatorId and simulatorName
109 |   return executeXcodeBuildCommand(
110 |     sharedBuildParams,
111 |     {
112 |       platform: XcodePlatform.iOSSimulator,
113 |       simulatorName: params.simulatorName,
114 |       simulatorId: params.simulatorId,
115 |       useLatestOS: params.simulatorId ? false : params.useLatestOS, // Ignore useLatestOS with ID
116 |       logPrefix: 'iOS Simulator Build',
117 |     },
118 |     params.preferXcodebuild ?? false,
119 |     'build',
120 |     executor,
121 |   );
122 | }
123 | 
124 | export async function build_simLogic(
125 |   params: BuildSimulatorParams,
126 |   executor: CommandExecutor,
127 | ): Promise<ToolResponse> {
128 |   // Provide defaults
129 |   const processedParams: BuildSimulatorParams = {
130 |     ...params,
131 |     configuration: params.configuration ?? 'Debug',
132 |     useLatestOS: params.useLatestOS ?? true, // May be ignored if simulatorId is provided
133 |     preferXcodebuild: params.preferXcodebuild ?? false,
134 |   };
135 | 
136 |   return _handleSimulatorBuildLogic(processedParams, executor);
137 | }
138 | 
139 | // Public schema = internal minus session-managed fields
140 | const publicSchemaObject = baseSchemaObject.omit({
141 |   projectPath: true,
142 |   workspacePath: true,
143 |   scheme: true,
144 |   configuration: true,
145 |   simulatorId: true,
146 |   simulatorName: true,
147 |   useLatestOS: true,
148 | } as const);
149 | 
150 | export default {
151 |   name: 'build_sim',
152 |   description: 'Builds an app for an iOS simulator.',
153 |   schema: publicSchemaObject.shape, // MCP SDK compatibility (public inputs only)
154 |   handler: createSessionAwareTool<BuildSimulatorParams>({
155 |     internalSchema: buildSimulatorSchema as unknown as z.ZodType<BuildSimulatorParams>,
156 |     logicFunction: build_simLogic,
157 |     getExecutor: getDefaultCommandExecutor,
158 |     requirements: [
159 |       { allOf: ['scheme'], message: 'scheme is required' },
160 |       { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
161 |       { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
162 |     ],
163 |     exclusivePairs: [
164 |       ['projectPath', 'workspacePath'],
165 |       ['simulatorId', 'simulatorName'],
166 |     ],
167 |   }),
168 | };
169 | 
```

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

```typescript
  1 | /**
  2 |  * Utilities Plugin: Clean (Unified)
  3 |  *
  4 |  * Cleans build products for either a project or workspace using xcodebuild.
  5 |  * Accepts mutually exclusive `projectPath` or `workspacePath`.
  6 |  */
  7 | 
  8 | import { z } from 'zod';
  9 | import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
 10 | import type { CommandExecutor } from '../../../utils/execution/index.ts';
 11 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
 12 | import { executeXcodeBuildCommand } from '../../../utils/build/index.ts';
 13 | import { ToolResponse, SharedBuildParams, XcodePlatform } from '../../../types/common.ts';
 14 | import { createErrorResponse } from '../../../utils/responses/index.ts';
 15 | import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
 16 | 
 17 | // Unified schema: XOR between projectPath and workspacePath, sharing common options
 18 | const baseOptions = {
 19 |   scheme: z.string().optional().describe('Optional: The scheme to clean'),
 20 |   configuration: z
 21 |     .string()
 22 |     .optional()
 23 |     .describe('Optional: Build configuration to clean (Debug, Release, etc.)'),
 24 |   derivedDataPath: z
 25 |     .string()
 26 |     .optional()
 27 |     .describe('Optional: Path where derived data might be located'),
 28 |   extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'),
 29 |   preferXcodebuild: z
 30 |     .boolean()
 31 |     .optional()
 32 |     .describe(
 33 |       'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.',
 34 |     ),
 35 |   platform: z
 36 |     .enum([
 37 |       'macOS',
 38 |       'iOS',
 39 |       'iOS Simulator',
 40 |       'watchOS',
 41 |       'watchOS Simulator',
 42 |       'tvOS',
 43 |       'tvOS Simulator',
 44 |       'visionOS',
 45 |       'visionOS Simulator',
 46 |     ])
 47 |     .optional()
 48 |     .describe(
 49 |       'Optional: Platform to clean for (defaults to iOS). Choose from macOS, iOS, iOS Simulator, watchOS, watchOS Simulator, tvOS, tvOS Simulator, visionOS, visionOS Simulator',
 50 |     ),
 51 | };
 52 | 
 53 | const baseSchemaObject = z.object({
 54 |   projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
 55 |   workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
 56 |   ...baseOptions,
 57 | });
 58 | 
 59 | const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);
 60 | 
 61 | const cleanSchema = baseSchema
 62 |   .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
 63 |     message: 'Either projectPath or workspacePath is required.',
 64 |   })
 65 |   .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
 66 |     message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
 67 |   })
 68 |   .refine((val) => !(val.workspacePath && !val.scheme), {
 69 |     message: 'scheme is required when workspacePath is provided.',
 70 |     path: ['scheme'],
 71 |   });
 72 | 
 73 | export type CleanParams = z.infer<typeof cleanSchema>;
 74 | 
 75 | export async function cleanLogic(
 76 |   params: CleanParams,
 77 |   executor: CommandExecutor,
 78 | ): Promise<ToolResponse> {
 79 |   // Extra safety: ensure workspace path has a scheme (xcodebuild requires it)
 80 |   if (params.workspacePath && !params.scheme) {
 81 |     return createErrorResponse(
 82 |       'Parameter validation failed',
 83 |       'Invalid parameters:\nscheme: scheme is required when workspacePath is provided.',
 84 |     );
 85 |   }
 86 | 
 87 |   // Use provided platform or default to iOS
 88 |   const targetPlatform = params.platform ?? 'iOS';
 89 | 
 90 |   // Map human-friendly platform names to XcodePlatform enum values
 91 |   // This is safer than direct key lookup and handles the space-containing simulator names
 92 |   const platformMap = {
 93 |     macOS: XcodePlatform.macOS,
 94 |     iOS: XcodePlatform.iOS,
 95 |     'iOS Simulator': XcodePlatform.iOSSimulator,
 96 |     watchOS: XcodePlatform.watchOS,
 97 |     'watchOS Simulator': XcodePlatform.watchOSSimulator,
 98 |     tvOS: XcodePlatform.tvOS,
 99 |     'tvOS Simulator': XcodePlatform.tvOSSimulator,
100 |     visionOS: XcodePlatform.visionOS,
101 |     'visionOS Simulator': XcodePlatform.visionOSSimulator,
102 |   };
103 | 
104 |   const platformEnum = platformMap[targetPlatform];
105 |   if (!platformEnum) {
106 |     return createErrorResponse(
107 |       'Parameter validation failed',
108 |       `Invalid parameters:\nplatform: unsupported value "${targetPlatform}".`,
109 |     );
110 |   }
111 | 
112 |   const hasProjectPath = typeof params.projectPath === 'string';
113 |   const typedParams: SharedBuildParams = {
114 |     ...(hasProjectPath
115 |       ? { projectPath: params.projectPath as string }
116 |       : { workspacePath: params.workspacePath as string }),
117 |     // scheme may be omitted for project; when omitted we do not pass -scheme
118 |     // Provide empty string to satisfy type, executeXcodeBuildCommand only emits -scheme when non-empty
119 |     scheme: params.scheme ?? '',
120 |     configuration: params.configuration ?? 'Debug',
121 |     derivedDataPath: params.derivedDataPath,
122 |     extraArgs: params.extraArgs,
123 |   };
124 | 
125 |   // For clean operations, simulator platforms should be mapped to their device equivalents
126 |   // since clean works at the build product level, not runtime level, and build products
127 |   // are shared between device and simulator platforms
128 |   const cleanPlatformMap: Partial<Record<XcodePlatform, XcodePlatform>> = {
129 |     [XcodePlatform.iOSSimulator]: XcodePlatform.iOS,
130 |     [XcodePlatform.watchOSSimulator]: XcodePlatform.watchOS,
131 |     [XcodePlatform.tvOSSimulator]: XcodePlatform.tvOS,
132 |     [XcodePlatform.visionOSSimulator]: XcodePlatform.visionOS,
133 |   };
134 | 
135 |   const cleanPlatform = cleanPlatformMap[platformEnum] ?? platformEnum;
136 | 
137 |   return executeXcodeBuildCommand(
138 |     typedParams,
139 |     {
140 |       platform: cleanPlatform,
141 |       logPrefix: 'Clean',
142 |     },
143 |     false,
144 |     'clean',
145 |     executor,
146 |   );
147 | }
148 | 
149 | const publicSchemaObject = baseSchemaObject.omit({
150 |   projectPath: true,
151 |   workspacePath: true,
152 |   scheme: true,
153 |   configuration: true,
154 | } as const);
155 | 
156 | export default {
157 |   name: 'clean',
158 |   description: 'Cleans build products with xcodebuild.',
159 |   schema: publicSchemaObject.shape,
160 |   handler: createSessionAwareTool<CleanParams>({
161 |     internalSchema: cleanSchema as unknown as z.ZodType<CleanParams>,
162 |     logicFunction: cleanLogic,
163 |     getExecutor: getDefaultCommandExecutor,
164 |     requirements: [
165 |       { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
166 |     ],
167 |     exclusivePairs: [['projectPath', 'workspacePath']],
168 |   }),
169 | };
170 | 
```

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

```markdown
  1 | # Release Process
  2 | 
  3 | ## Step-by-Step Development Workflow
  4 | 
  5 | ### 1. Starting New Work
  6 | 
  7 | **Always start by syncing with main:**
  8 | ```bash
  9 | git checkout main
 10 | git pull origin main
 11 | ```
 12 | 
 13 | **Create feature branch using standardized naming convention:**
 14 | ```bash
 15 | git checkout -b feature/issue-123-add-new-feature
 16 | git checkout -b bugfix/issue-456-fix-simulator-crash
 17 | ```
 18 | 
 19 | ### 2. Development & Commits
 20 | 
 21 | **Before committing, ALWAYS run quality checks:**
 22 | ```bash
 23 | npm run build      # Ensure code compiles
 24 | npm run typecheck  # MANDATORY: Fix all TypeScript errors
 25 | npm run lint       # Fix linting issues
 26 | npm run test       # Ensure tests pass
 27 | ```
 28 | 
 29 | **🚨 CRITICAL: TypeScript errors are BLOCKING:**
 30 | - **ZERO tolerance** for TypeScript errors in commits
 31 | - The `npm run typecheck` command must pass with no errors
 32 | - Fix all `ts(XXXX)` errors before committing
 33 | - Do not ignore or suppress TypeScript errors without explicit approval
 34 | 
 35 | **Make logical, atomic commits:**
 36 | - Each commit should represent a single logical change  
 37 | - Write short, descriptive commit summaries
 38 | - Commit frequently to your feature branch
 39 | 
 40 | ```bash
 41 | # Always run quality checks first
 42 | npm run typecheck && npm run lint && npm run test
 43 | 
 44 | # Then commit your changes
 45 | git add .
 46 | git commit -m "feat: add simulator boot validation logic"
 47 | git commit -m "fix: handle null response in device list parser"
 48 | ```
 49 | 
 50 | ### 3. Pushing Changes
 51 | 
 52 | **🚨 CRITICAL: Always ask permission before pushing**
 53 | - **NEVER push without explicit user permission**
 54 | - **NEVER force push without explicit permission**
 55 | - Pushing without permission is a fatal error resulting in termination
 56 | 
 57 | ```bash
 58 | # Only after getting permission:
 59 | git push origin feature/your-branch-name
 60 | ```
 61 | 
 62 | ### 4. Pull Request Creation
 63 | 
 64 | **Use GitHub CLI tool exclusively:**
 65 | ```bash
 66 | gh pr create --title "feat: add simulator boot validation" --body "$(cat <<'EOF'
 67 | ## Summary
 68 | Brief description of what this PR does and why.
 69 | 
 70 | ## Background/Details
 71 | ### For New Features:
 72 | - Detailed explanation of the new feature
 73 | - Context and requirements that led to this implementation
 74 | - Design decisions and approach taken
 75 | 
 76 | ### For Bug Fixes:
 77 | - **Root Cause Analysis**: Detailed explanation of what caused the bug
 78 | - Specific conditions that trigger the issue
 79 | - Why the current code fails in these scenarios
 80 | 
 81 | ## Solution
 82 | - How the root cause was addressed
 83 | - Technical approach and implementation details
 84 | - Key changes made to resolve the issue
 85 | 
 86 | ## Testing
 87 | - **Reproduction Steps**: How to reproduce the original issue (for bugs)
 88 | - **Validation Method**: How you verified the fix works
 89 | - **Test Coverage**: What tests were added or modified
 90 | - **Manual Testing**: Steps taken to validate the solution
 91 | - **Edge Cases**: Additional scenarios tested
 92 | 
 93 | ## Notes
 94 | - Any important considerations for reviewers
 95 | - Potential impacts or side effects
 96 | - Future improvements or technical debt
 97 | - Deployment considerations
 98 | EOF
 99 | )"
100 | ```
101 | 
102 | **After PR creation, add automated review trigger:**
103 | ```bash
104 | gh pr comment --body "Cursor review"
105 | ```
106 | 
107 | ### 5. Branch Management & Rebasing
108 | 
109 | **Keep branch up to date with main:**
110 | ```bash
111 | git checkout main
112 | git pull origin main
113 | git checkout your-feature-branch
114 | git rebase main
115 | ```
116 | 
117 | **If rebase creates conflicts:**
118 | - Resolve conflicts manually
119 | - `git add .` resolved files
120 | - `git rebase --continue`
121 | - **Ask permission before force pushing rebased branch**
122 | 
123 | ### 6. Merge Process
124 | 
125 | **Only merge via Pull Requests:**
126 | - No direct merges to `main`
127 | - Maintain linear commit history through rebasing
128 | - Use "Squash and merge" or "Rebase and merge" as appropriate
129 | - Delete feature branch after successful merge
130 | 
131 | ## Pull Request Template Structure
132 | 
133 | Every PR must include these sections in order:
134 | 
135 | 1. **Summary**: Brief overview of changes and purpose
136 | 2. **Background/Details**: 
137 |    - New Feature: Requirements, context, design decisions
138 |    - Bug Fix: Detailed root cause analysis
139 | 3. **Solution**: Technical approach and implementation details  
140 | 4. **Testing**: Reproduction steps, validation methods, test coverage
141 | 5. **Notes**: Additional considerations, impacts, future work
142 | 
143 | ## Critical Rules
144 | 
145 | ### ❌ FATAL ERRORS (Result in Termination)
146 | - **NEVER push to `main` directly**
147 | - **NEVER push without explicit user permission**
148 | - **NEVER force push without explicit permission**
149 | - **NEVER commit code with TypeScript errors**
150 | 
151 | ### ✅ Required Practices
152 | - Always pull from `main` before creating branches
153 | - **MANDATORY: Run `npm run typecheck` before every commit**
154 | - **MANDATORY: Fix all TypeScript errors before committing**
155 | - Use `gh` CLI tool for all PR operations
156 | - Add "Cursor review" comment after PR creation
157 | - Maintain linear commit history via rebasing
158 | - Ask permission before any push operation
159 | - Use standardized branch naming conventions
160 | 
161 | ## Branch Naming Conventions
162 | 
163 | - `feature/issue-xxx-description` - New features
164 | - `bugfix/issue-xxx-description` - Bug fixes  
165 | - `hotfix/critical-issue-description` - Critical production fixes
166 | - `docs/update-readme` - Documentation updates
167 | - `refactor/improve-error-handling` - Code refactoring
168 | 
169 | ## Automated Quality Gates
170 | 
171 | ### CI/CD Pipeline
172 | Our GitHub Actions CI pipeline automatically enforces these quality checks:
173 | 1. `npm run build` - Compilation check
174 | 2. `npm run lint` - ESLint validation  
175 | 3. `npm run format:check` - Prettier formatting check
176 | 4. `npm run typecheck` - **TypeScript error validation**
177 | 5. `npm run test` - Test suite execution
178 | 
179 | **All checks must pass before PR merge is allowed.**
180 | 
181 | ### Optional: Pre-commit Hook Setup
182 | To catch TypeScript errors before committing locally:
183 | 
184 | ```bash
185 | # Create pre-commit hook
186 | cat > .git/hooks/pre-commit << 'EOF'
187 | #!/bin/sh
188 | echo "🔍 Running pre-commit checks..."
189 | 
190 | # Run TypeScript type checking
191 | echo "📝 Checking TypeScript..."
192 | npm run typecheck
193 | if [ $? -ne 0 ]; then
194 |   echo "❌ TypeScript errors found. Please fix before committing."
195 |   exit 1
196 | fi
197 | 
198 | # Run linting
199 | echo "🧹 Running linter..."
200 | npm run lint
201 | if [ $? -ne 0 ]; then
202 |   echo "❌ Linting errors found. Please fix before committing."
203 |   exit 1
204 | fi
205 | 
206 | echo "✅ Pre-commit checks passed!"
207 | EOF
208 | 
209 | # Make it executable  
210 | chmod +x .git/hooks/pre-commit
211 | ```
212 | 
213 | This hook will automatically run `typecheck` and `lint` before every commit, preventing TypeScript errors from being committed.
```

--------------------------------------------------------------------------------
/src/mcp/resources/__tests__/simulators.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach } from 'vitest';
  2 | import { z } from 'zod';
  3 | 
  4 | import simulatorsResource, { simulatorsResourceLogic } from '../simulators.ts';
  5 | import { createMockExecutor } from '../../../test-utils/mock-executors.ts';
  6 | 
  7 | describe('simulators resource', () => {
  8 |   describe('Export Field Validation', () => {
  9 |     it('should export correct uri', () => {
 10 |       expect(simulatorsResource.uri).toBe('xcodebuildmcp://simulators');
 11 |     });
 12 | 
 13 |     it('should export correct description', () => {
 14 |       expect(simulatorsResource.description).toBe(
 15 |         'Available iOS simulators with their UUIDs and states',
 16 |       );
 17 |     });
 18 | 
 19 |     it('should export correct mimeType', () => {
 20 |       expect(simulatorsResource.mimeType).toBe('text/plain');
 21 |     });
 22 | 
 23 |     it('should export handler function', () => {
 24 |       expect(typeof simulatorsResource.handler).toBe('function');
 25 |     });
 26 |   });
 27 | 
 28 |   describe('Handler Functionality', () => {
 29 |     it('should handle successful simulator data retrieval', async () => {
 30 |       const mockExecutor = createMockExecutor({
 31 |         success: true,
 32 |         output: JSON.stringify({
 33 |           devices: {
 34 |             'iOS 17.0': [
 35 |               {
 36 |                 name: 'iPhone 15 Pro',
 37 |                 udid: 'ABC123-DEF456-GHI789',
 38 |                 state: 'Shutdown',
 39 |                 isAvailable: true,
 40 |               },
 41 |             ],
 42 |           },
 43 |         }),
 44 |       });
 45 | 
 46 |       const result = await simulatorsResourceLogic(mockExecutor);
 47 | 
 48 |       expect(result.contents).toHaveLength(1);
 49 |       expect(result.contents[0].text).toContain('Available iOS Simulators:');
 50 |       expect(result.contents[0].text).toContain('iPhone 15 Pro');
 51 |       expect(result.contents[0].text).toContain('ABC123-DEF456-GHI789');
 52 |     });
 53 | 
 54 |     it('should handle command execution failure', async () => {
 55 |       const mockExecutor = createMockExecutor({
 56 |         success: false,
 57 |         output: '',
 58 |         error: 'Command failed',
 59 |       });
 60 | 
 61 |       const result = await simulatorsResourceLogic(mockExecutor);
 62 | 
 63 |       expect(result.contents).toHaveLength(1);
 64 |       expect(result.contents[0].text).toContain('Failed to list simulators');
 65 |       expect(result.contents[0].text).toContain('Command failed');
 66 |     });
 67 | 
 68 |     it('should handle JSON parsing errors and fall back to text parsing', async () => {
 69 |       const mockTextOutput = `== Devices ==
 70 | -- iOS 17.0 --
 71 |     iPhone 15 (test-uuid-123) (Shutdown)`;
 72 | 
 73 |       const mockExecutor = async (command: string[]) => {
 74 |         // JSON command returns invalid JSON
 75 |         if (command.includes('--json')) {
 76 |           return {
 77 |             success: true,
 78 |             output: 'invalid json',
 79 |             error: undefined,
 80 |             process: { pid: 12345 },
 81 |           };
 82 |         }
 83 | 
 84 |         // Text command returns valid text output
 85 |         return {
 86 |           success: true,
 87 |           output: mockTextOutput,
 88 |           error: undefined,
 89 |           process: { pid: 12345 },
 90 |         };
 91 |       };
 92 | 
 93 |       const result = await simulatorsResourceLogic(mockExecutor);
 94 | 
 95 |       expect(result.contents).toHaveLength(1);
 96 |       expect(result.contents[0].text).toContain('iPhone 15 (test-uuid-123)');
 97 |       expect(result.contents[0].text).toContain('iOS 17.0');
 98 |     });
 99 | 
100 |     it('should handle spawn errors', async () => {
101 |       const mockExecutor = createMockExecutor(new Error('spawn xcrun ENOENT'));
102 | 
103 |       const result = await simulatorsResourceLogic(mockExecutor);
104 | 
105 |       expect(result.contents).toHaveLength(1);
106 |       expect(result.contents[0].text).toContain('Failed to list simulators');
107 |       expect(result.contents[0].text).toContain('spawn xcrun ENOENT');
108 |     });
109 | 
110 |     it('should handle empty simulator data', async () => {
111 |       const mockExecutor = createMockExecutor({
112 |         success: true,
113 |         output: JSON.stringify({ devices: {} }),
114 |       });
115 | 
116 |       const result = await simulatorsResourceLogic(mockExecutor);
117 | 
118 |       expect(result.contents).toHaveLength(1);
119 |       expect(result.contents[0].text).toContain('Available iOS Simulators:');
120 |     });
121 | 
122 |     it('should handle booted simulators correctly', async () => {
123 |       const mockExecutor = createMockExecutor({
124 |         success: true,
125 |         output: JSON.stringify({
126 |           devices: {
127 |             'iOS 17.0': [
128 |               {
129 |                 name: 'iPhone 15 Pro',
130 |                 udid: 'ABC123-DEF456-GHI789',
131 |                 state: 'Booted',
132 |                 isAvailable: true,
133 |               },
134 |             ],
135 |           },
136 |         }),
137 |       });
138 | 
139 |       const result = await simulatorsResourceLogic(mockExecutor);
140 | 
141 |       expect(result.contents[0].text).toContain('[Booted]');
142 |     });
143 | 
144 |     it('should filter out unavailable simulators', async () => {
145 |       const mockExecutor = createMockExecutor({
146 |         success: true,
147 |         output: JSON.stringify({
148 |           devices: {
149 |             'iOS 17.0': [
150 |               {
151 |                 name: 'iPhone 15 Pro',
152 |                 udid: 'ABC123-DEF456-GHI789',
153 |                 state: 'Shutdown',
154 |                 isAvailable: true,
155 |               },
156 |               {
157 |                 name: 'iPhone 14',
158 |                 udid: 'XYZ789-UVW456-RST123',
159 |                 state: 'Shutdown',
160 |                 isAvailable: false,
161 |               },
162 |             ],
163 |           },
164 |         }),
165 |       });
166 | 
167 |       const result = await simulatorsResourceLogic(mockExecutor);
168 | 
169 |       expect(result.contents[0].text).toContain('iPhone 15 Pro');
170 |       expect(result.contents[0].text).not.toContain('iPhone 14');
171 |     });
172 | 
173 |     it('should include next steps guidance', async () => {
174 |       const mockExecutor = createMockExecutor({
175 |         success: true,
176 |         output: JSON.stringify({
177 |           devices: {
178 |             'iOS 17.0': [
179 |               {
180 |                 name: 'iPhone 15 Pro',
181 |                 udid: 'ABC123-DEF456-GHI789',
182 |                 state: 'Shutdown',
183 |                 isAvailable: true,
184 |               },
185 |             ],
186 |           },
187 |         }),
188 |       });
189 | 
190 |       const result = await simulatorsResourceLogic(mockExecutor);
191 | 
192 |       expect(result.contents[0].text).toContain('Next Steps:');
193 |       expect(result.contents[0].text).toContain('boot_sim');
194 |       expect(result.contents[0].text).toContain('open_sim');
195 |       expect(result.contents[0].text).toContain('build_sim');
196 |       expect(result.contents[0].text).toContain('get_sim_app_path');
197 |     });
198 |   });
199 | });
200 | 
```

--------------------------------------------------------------------------------
/example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/CalculatorService.swift:
--------------------------------------------------------------------------------

```swift
  1 | import Foundation
  2 | 
  3 | // MARK: - Calculator Business Logic Service
  4 | 
  5 | /// Handles all calculator operations and state management
  6 | /// Separated from UI concerns for better testability and modularity
  7 | @Observable
  8 | public final class CalculatorService {
  9 |     // MARK: - Public Properties
 10 | 
 11 |     public private(set) var display: String = "0"
 12 |     public private(set) var expressionDisplay: String = ""
 13 |     public private(set) var hasError: Bool = false
 14 | 
 15 |     // MARK: - Private State
 16 | 
 17 |     private var currentNumber: Double = 0
 18 |     private var previousNumber: Double = 0
 19 |     private var operation: Operation?
 20 |     private var shouldResetDisplay = false
 21 |     private var isNewCalculation = true
 22 |     private var lastOperation: Operation?
 23 |     private var lastOperand: Double = 0
 24 | 
 25 |     // MARK: - Operations
 26 | 
 27 |     public enum Operation: String, CaseIterable, Sendable {
 28 |         case add = "+"
 29 |         case subtract = "-"
 30 |         case multiply = "×"
 31 |         case divide = "÷"
 32 | 
 33 |         public func calculate(_ a: Double, _ b: Double) -> Double {
 34 |             switch self {
 35 |             case .add: return a + b
 36 |             case .subtract: return a - b
 37 |             case .multiply: return a * b
 38 |             case .divide: return b != 0 ? a / b : 0
 39 |             }
 40 |         }
 41 |     }
 42 | 
 43 |     public init() {}
 44 | 
 45 |     // MARK: - Public Interface
 46 | 
 47 |     public func inputNumber(_ digit: String) {
 48 |         guard !hasError else { clear(); return }
 49 | 
 50 |         if shouldResetDisplay || isNewCalculation {
 51 |             display = digit
 52 |             shouldResetDisplay = false
 53 |             isNewCalculation = false
 54 |         } else if display.count < 12 {
 55 |             display = display == "0" ? digit : display + digit
 56 |         }
 57 | 
 58 |         currentNumber = Double(display) ?? 0
 59 |         updateExpressionDisplay()
 60 |     }
 61 | 
 62 |     /// Inputs a decimal point into the display
 63 |     public func inputDecimal() {
 64 |         guard !hasError else {
 65 |             clear(); return
 66 |         }
 67 | 
 68 |         if shouldResetDisplay || isNewCalculation {
 69 |             display = "0."
 70 |             shouldResetDisplay = false
 71 |             isNewCalculation = false
 72 |         } else if !display.contains("."), display.count < 11 {
 73 |             display += "."
 74 |         }
 75 |         updateExpressionDisplay()
 76 |     }
 77 | 
 78 |     public func setOperation(_ op: Operation) {
 79 |         guard !hasError else { return }
 80 | 
 81 |         if operation != nil, !shouldResetDisplay {
 82 |             calculate()
 83 |             if hasError { return }
 84 |         }
 85 | 
 86 |         previousNumber = currentNumber
 87 |         operation = op
 88 |         shouldResetDisplay = true
 89 |         isNewCalculation = false
 90 |         updateExpressionDisplay()
 91 |     }
 92 | 
 93 |     public func calculate() {
 94 |         guard let op = operation ?? lastOperation else { return }
 95 |         let operand = (operation != nil) ? currentNumber : lastOperand
 96 | 
 97 |         let result = op.calculate(previousNumber, operand)
 98 | 
 99 |         // Error handling
100 |         if result.isNaN || result.isInfinite {
101 |             setError("Cannot divide by zero")
102 |             return
103 |         }
104 | 
105 |         if abs(result) > 1e12 {
106 |             setError("Number too large")
107 |             return
108 |         }
109 | 
110 |         // Success path
111 |         let prevFormatted = formatNumber(previousNumber)
112 |         let currFormatted = formatNumber(operand)
113 |         display = formatNumber(result)
114 |         expressionDisplay = "\(prevFormatted) \(op.rawValue) \(currFormatted) ="
115 | 
116 |         previousNumber = result
117 |         if operation != nil {
118 |             lastOperand = currentNumber
119 |         }
120 | 
121 |         lastOperation = op
122 |         operation = nil
123 |         currentNumber = result
124 |         shouldResetDisplay = true
125 |         isNewCalculation = false
126 |     }
127 | 
128 |     public func toggleSign() {
129 |         guard !hasError, currentNumber != 0 else { return }
130 |         currentNumber *= -1
131 |         display = formatNumber(currentNumber)
132 |         updateExpressionDisplay()
133 |     }
134 | 
135 |     public func percentage() {
136 |         guard !hasError else { return }
137 |         currentNumber /= 100
138 |         display = formatNumber(currentNumber)
139 |         updateExpressionDisplay()
140 |     }
141 | 
142 |     public func clear() {
143 |         display = "0"
144 |         expressionDisplay = ""
145 |         currentNumber = 0
146 |         previousNumber = 0
147 |         operation = nil
148 |         shouldResetDisplay = false
149 |         hasError = false
150 |         isNewCalculation = true
151 |     }
152 | 
153 |     public func deleteLastDigit() {
154 |         guard !hasError else { clear(); return }
155 | 
156 |         if shouldResetDisplay || isNewCalculation {
157 |             display = "0"
158 |             shouldResetDisplay = false
159 |             isNewCalculation = false
160 |         } else if display.count > 1 {
161 |             display.removeLast()
162 |             if display == "-" { display = "0" }
163 |         } else {
164 |             display = "0"
165 |         }
166 |         currentNumber = Double(display) ?? 0
167 |         updateExpressionDisplay()
168 |     }
169 | 
170 |     // MARK: - Private Helpers
171 | 
172 |     private func setError(_ message: String) {
173 |         hasError = true
174 |         display = "Error"
175 |         expressionDisplay = message
176 |     }
177 | 
178 |     private func updateExpressionDisplay() {
179 |         guard !hasError else { return }
180 | 
181 |         if let op = operation {
182 |             let prevFormatted = formatNumber(previousNumber)
183 |             expressionDisplay = "\(prevFormatted) \(op.rawValue)"
184 |         } else if isNewCalculation {
185 |             expressionDisplay = ""
186 |         }
187 |     }
188 | 
189 |     private func formatNumber(_ number: Double) -> String {
190 |         guard !number.isNaN && !number.isInfinite else { return "Error" }
191 | 
192 |         let formatter = NumberFormatter()
193 |         formatter.numberStyle = .decimal
194 |         formatter.maximumFractionDigits = 8
195 |         formatter.minimumFractionDigits = 0
196 | 
197 |         // For integers, don't show decimal places
198 |         if number == floor(number) && abs(number) < 1e10 {
199 |             formatter.maximumFractionDigits = 0
200 |         }
201 | 
202 |         // For very small decimals, use scientific notation
203 |         if abs(number) < 0.000001 && number != 0 {
204 |             formatter.numberStyle = .scientific
205 |             formatter.maximumFractionDigits = 2
206 |         }
207 | 
208 |         return formatter.string(from: NSNumber(value: number)) ?? "0"
209 |     }
210 | }
211 | 
212 | // MARK: - Testing Support
213 | 
214 | public extension CalculatorService {
215 |     var currentValue: Double { currentNumber }
216 |     var previousValue: Double { previousNumber }
217 |     var currentOperation: Operation? { operation }
218 |     var willResetDisplay: Bool { shouldResetDisplay }
219 | }
220 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/device/get_device_app_path.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Device Shared Plugin: Get Device App Path (Unified)
  3 |  *
  4 |  * Gets the app bundle path for a physical device application (iOS, watchOS, tvOS, visionOS) using either a project or workspace.
  5 |  * Accepts mutually exclusive `projectPath` or `workspacePath`.
  6 |  */
  7 | 
  8 | import { z } from 'zod';
  9 | import { ToolResponse, XcodePlatform } from '../../../types/common.ts';
 10 | import { log } from '../../../utils/logging/index.ts';
 11 | import { createTextResponse } from '../../../utils/responses/index.ts';
 12 | import type { CommandExecutor } from '../../../utils/execution/index.ts';
 13 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
 14 | import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
 15 | import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
 16 | 
 17 | // Unified schema: XOR between projectPath and workspacePath, sharing common options
 18 | const baseOptions = {
 19 |   scheme: z.string().describe('The scheme to use'),
 20 |   configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
 21 |   platform: z
 22 |     .enum(['iOS', 'watchOS', 'tvOS', 'visionOS'])
 23 |     .optional()
 24 |     .describe('Target platform (defaults to iOS)'),
 25 | };
 26 | 
 27 | const baseSchemaObject = z.object({
 28 |   projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
 29 |   workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
 30 |   ...baseOptions,
 31 | });
 32 | 
 33 | const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);
 34 | 
 35 | const getDeviceAppPathSchema = baseSchema
 36 |   .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
 37 |     message: 'Either projectPath or workspacePath is required.',
 38 |   })
 39 |   .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
 40 |     message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
 41 |   });
 42 | 
 43 | // Use z.infer for type safety
 44 | type GetDeviceAppPathParams = z.infer<typeof getDeviceAppPathSchema>;
 45 | 
 46 | export async function get_device_app_pathLogic(
 47 |   params: GetDeviceAppPathParams,
 48 |   executor: CommandExecutor,
 49 | ): Promise<ToolResponse> {
 50 |   const platformMap = {
 51 |     iOS: XcodePlatform.iOS,
 52 |     watchOS: XcodePlatform.watchOS,
 53 |     tvOS: XcodePlatform.tvOS,
 54 |     visionOS: XcodePlatform.visionOS,
 55 |   };
 56 | 
 57 |   const platform = platformMap[params.platform ?? 'iOS'];
 58 |   const configuration = params.configuration ?? 'Debug';
 59 | 
 60 |   log('info', `Getting app path for scheme ${params.scheme} on platform ${platform}`);
 61 | 
 62 |   try {
 63 |     // Create the command array for xcodebuild with -showBuildSettings option
 64 |     const command = ['xcodebuild', '-showBuildSettings'];
 65 | 
 66 |     // Add the project or workspace
 67 |     if (params.projectPath) {
 68 |       command.push('-project', params.projectPath);
 69 |     } else if (params.workspacePath) {
 70 |       command.push('-workspace', params.workspacePath);
 71 |     } else {
 72 |       // This should never happen due to schema validation
 73 |       throw new Error('Either projectPath or workspacePath is required.');
 74 |     }
 75 | 
 76 |     // Add the scheme and configuration
 77 |     command.push('-scheme', params.scheme);
 78 |     command.push('-configuration', configuration);
 79 | 
 80 |     // Handle destination based on platform
 81 |     let destinationString = '';
 82 | 
 83 |     if (platform === XcodePlatform.iOS) {
 84 |       destinationString = 'generic/platform=iOS';
 85 |     } else if (platform === XcodePlatform.watchOS) {
 86 |       destinationString = 'generic/platform=watchOS';
 87 |     } else if (platform === XcodePlatform.tvOS) {
 88 |       destinationString = 'generic/platform=tvOS';
 89 |     } else if (platform === XcodePlatform.visionOS) {
 90 |       destinationString = 'generic/platform=visionOS';
 91 |     } else {
 92 |       return createTextResponse(`Unsupported platform: ${platform}`, true);
 93 |     }
 94 | 
 95 |     command.push('-destination', destinationString);
 96 | 
 97 |     // Execute the command directly
 98 |     const result = await executor(command, 'Get App Path', true);
 99 | 
100 |     if (!result.success) {
101 |       return createTextResponse(`Failed to get app path: ${result.error}`, true);
102 |     }
103 | 
104 |     if (!result.output) {
105 |       return createTextResponse('Failed to extract build settings output from the result.', true);
106 |     }
107 | 
108 |     const buildSettingsOutput = result.output;
109 |     const builtProductsDirMatch = buildSettingsOutput.match(/^\s*BUILT_PRODUCTS_DIR\s*=\s*(.+)$/m);
110 |     const fullProductNameMatch = buildSettingsOutput.match(/^\s*FULL_PRODUCT_NAME\s*=\s*(.+)$/m);
111 | 
112 |     if (!builtProductsDirMatch || !fullProductNameMatch) {
113 |       return createTextResponse(
114 |         'Failed to extract app path from build settings. Make sure the app has been built first.',
115 |         true,
116 |       );
117 |     }
118 | 
119 |     const builtProductsDir = builtProductsDirMatch[1].trim();
120 |     const fullProductName = fullProductNameMatch[1].trim();
121 |     const appPath = `${builtProductsDir}/${fullProductName}`;
122 | 
123 |     const nextStepsText = `Next Steps:
124 | 1. Get bundle ID: get_app_bundle_id({ appPath: "${appPath}" })
125 | 2. Install app on device: install_app_device({ deviceId: "DEVICE_UDID", appPath: "${appPath}" })
126 | 3. Launch app on device: launch_app_device({ deviceId: "DEVICE_UDID", bundleId: "BUNDLE_ID" })`;
127 | 
128 |     return {
129 |       content: [
130 |         {
131 |           type: 'text',
132 |           text: `✅ App path retrieved successfully: ${appPath}`,
133 |         },
134 |         {
135 |           type: 'text',
136 |           text: nextStepsText,
137 |         },
138 |       ],
139 |     };
140 |   } catch (error) {
141 |     const errorMessage = error instanceof Error ? error.message : String(error);
142 |     log('error', `Error retrieving app path: ${errorMessage}`);
143 |     return createTextResponse(`Error retrieving app path: ${errorMessage}`, true);
144 |   }
145 | }
146 | 
147 | export default {
148 |   name: 'get_device_app_path',
149 |   description: 'Retrieves the built app path for a connected device.',
150 |   schema: baseSchemaObject.omit({
151 |     projectPath: true,
152 |     workspacePath: true,
153 |     scheme: true,
154 |     configuration: true,
155 |   } as const).shape,
156 |   handler: createSessionAwareTool<GetDeviceAppPathParams>({
157 |     internalSchema: getDeviceAppPathSchema as unknown as z.ZodType<GetDeviceAppPathParams>,
158 |     logicFunction: get_device_app_pathLogic,
159 |     getExecutor: getDefaultCommandExecutor,
160 |     requirements: [
161 |       { allOf: ['scheme'], message: 'scheme is required' },
162 |       { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
163 |     ],
164 |     exclusivePairs: [['projectPath', 'workspacePath']],
165 |   }),
166 | };
167 | 
```

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

```typescript
  1 | import { z } from 'zod';
  2 | import type { ToolResponse } from '../../../types/common.ts';
  3 | import { log } from '../../../utils/logging/index.ts';
  4 | import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.ts';
  5 | import type { CommandExecutor } from '../../../utils/execution/index.ts';
  6 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
  7 | import {
  8 |   createAxeNotAvailableResponse,
  9 |   getAxePath,
 10 |   getBundledAxeEnvironment,
 11 | } from '../../../utils/axe-helpers.ts';
 12 | import { DependencyError, AxeError, SystemError } from '../../../utils/errors.ts';
 13 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
 14 | 
 15 | export interface AxeHelpers {
 16 |   getAxePath: () => string | null;
 17 |   getBundledAxeEnvironment: () => Record<string, string>;
 18 |   createAxeNotAvailableResponse: () => ToolResponse;
 19 | }
 20 | 
 21 | // Define schema as ZodObject
 22 | const tapSchema = z.object({
 23 |   simulatorUuid: z.string().uuid('Invalid Simulator UUID format'),
 24 |   x: z.number().int('X coordinate must be an integer'),
 25 |   y: z.number().int('Y coordinate must be an integer'),
 26 |   preDelay: z.number().min(0, 'Pre-delay must be non-negative').optional(),
 27 |   postDelay: z.number().min(0, 'Post-delay must be non-negative').optional(),
 28 | });
 29 | 
 30 | // Use z.infer for type safety
 31 | type TapParams = z.infer<typeof tapSchema>;
 32 | 
 33 | const LOG_PREFIX = '[AXe]';
 34 | 
 35 | // Session tracking for describe_ui warnings (shared across UI tools)
 36 | const describeUITimestamps = new Map<string, { timestamp: number }>();
 37 | const DESCRIBE_UI_WARNING_TIMEOUT = 60000; // 60 seconds
 38 | 
 39 | function getCoordinateWarning(simulatorUuid: string): string | null {
 40 |   const session = describeUITimestamps.get(simulatorUuid);
 41 |   if (!session) {
 42 |     return 'Warning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.';
 43 |   }
 44 | 
 45 |   const timeSinceDescribe = Date.now() - session.timestamp;
 46 |   if (timeSinceDescribe > DESCRIBE_UI_WARNING_TIMEOUT) {
 47 |     const secondsAgo = Math.round(timeSinceDescribe / 1000);
 48 |     return `Warning: describe_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with describe_ui instead of using potentially stale coordinates.`;
 49 |   }
 50 | 
 51 |   return null;
 52 | }
 53 | 
 54 | export async function tapLogic(
 55 |   params: TapParams,
 56 |   executor: CommandExecutor,
 57 |   axeHelpers: AxeHelpers = {
 58 |     getAxePath,
 59 |     getBundledAxeEnvironment,
 60 |     createAxeNotAvailableResponse,
 61 |   },
 62 | ): Promise<ToolResponse> {
 63 |   const toolName = 'tap';
 64 |   const { simulatorUuid, x, y, preDelay, postDelay } = params;
 65 |   const commandArgs = ['tap', '-x', String(x), '-y', String(y)];
 66 |   if (preDelay !== undefined) {
 67 |     commandArgs.push('--pre-delay', String(preDelay));
 68 |   }
 69 |   if (postDelay !== undefined) {
 70 |     commandArgs.push('--post-delay', String(postDelay));
 71 |   }
 72 | 
 73 |   log('info', `${LOG_PREFIX}/${toolName}: Starting for (${x}, ${y}) on ${simulatorUuid}`);
 74 | 
 75 |   try {
 76 |     await executeAxeCommand(commandArgs, simulatorUuid, 'tap', executor, axeHelpers);
 77 |     log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`);
 78 | 
 79 |     const warning = getCoordinateWarning(simulatorUuid);
 80 |     const message = `Tap at (${x}, ${y}) simulated successfully.`;
 81 | 
 82 |     if (warning) {
 83 |       return createTextResponse(`${message}\n\n${warning}`);
 84 |     }
 85 | 
 86 |     return createTextResponse(message);
 87 |   } catch (error) {
 88 |     log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
 89 |     if (error instanceof DependencyError) {
 90 |       return axeHelpers.createAxeNotAvailableResponse();
 91 |     } else if (error instanceof AxeError) {
 92 |       return createErrorResponse(
 93 |         `Failed to simulate tap at (${x}, ${y}): ${error.message}`,
 94 |         error.axeOutput,
 95 |       );
 96 |     } else if (error instanceof SystemError) {
 97 |       return createErrorResponse(
 98 |         `System error executing axe: ${error.message}`,
 99 |         error.originalError?.stack,
100 |       );
101 |     }
102 |     return createErrorResponse(
103 |       `An unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`,
104 |     );
105 |   }
106 | }
107 | 
108 | export default {
109 |   name: 'tap',
110 |   description:
111 |     "Tap at specific coordinates. Use describe_ui to get precise element coordinates (don't guess from screenshots). Supports optional timing delays.",
112 |   schema: tapSchema.shape, // MCP SDK compatibility
113 |   handler: createTypedTool(
114 |     tapSchema,
115 |     (params: TapParams, executor: CommandExecutor) => {
116 |       return tapLogic(params, executor, {
117 |         getAxePath,
118 |         getBundledAxeEnvironment,
119 |         createAxeNotAvailableResponse,
120 |       });
121 |     },
122 |     getDefaultCommandExecutor,
123 |   ),
124 | };
125 | 
126 | // Helper function for executing axe commands (inlined from src/tools/axe/index.ts)
127 | async function executeAxeCommand(
128 |   commandArgs: string[],
129 |   simulatorUuid: string,
130 |   commandName: string,
131 |   executor: CommandExecutor = getDefaultCommandExecutor(),
132 |   axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse },
133 | ): Promise<void> {
134 |   // Get the appropriate axe binary path
135 |   const axeBinary = axeHelpers.getAxePath();
136 |   if (!axeBinary) {
137 |     throw new DependencyError('AXe binary not found');
138 |   }
139 | 
140 |   // Add --udid parameter to all commands
141 |   const fullArgs = [...commandArgs, '--udid', simulatorUuid];
142 | 
143 |   // Construct the full command array with the axe binary as the first element
144 |   const fullCommand = [axeBinary, ...fullArgs];
145 | 
146 |   try {
147 |     // Determine environment variables for bundled AXe
148 |     const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined;
149 | 
150 |     const result = await executor(fullCommand, `${LOG_PREFIX}: ${commandName}`, false, axeEnv);
151 | 
152 |     if (!result.success) {
153 |       throw new AxeError(
154 |         `axe command '${commandName}' failed.`,
155 |         commandName,
156 |         result.error ?? result.output,
157 |         simulatorUuid,
158 |       );
159 |     }
160 | 
161 |     // Check for stderr output in successful commands
162 |     if (result.error) {
163 |       log(
164 |         'warn',
165 |         `${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${result.error}`,
166 |       );
167 |     }
168 | 
169 |     // Function now returns void - the calling code creates its own response
170 |   } catch (error) {
171 |     if (error instanceof Error) {
172 |       if (error instanceof AxeError) {
173 |         throw error;
174 |       }
175 | 
176 |       // Otherwise wrap it in a SystemError
177 |       throw new SystemError(`Failed to execute axe command: ${error.message}`, error);
178 |     }
179 | 
180 |     // For any other type of error
181 |     throw new SystemError(`Failed to execute axe command: ${String(error)}`);
182 |   }
183 | }
184 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for get_sim_app_path plugin (session-aware version)
  3 |  * Mirrors patterns from other simulator session-aware migrations.
  4 |  */
  5 | 
  6 | import { describe, it, expect, beforeEach } from 'vitest';
  7 | import { ChildProcess } from 'child_process';
  8 | import { z } from 'zod';
  9 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
 10 | import { sessionStore } from '../../../../utils/session-store.ts';
 11 | import getSimAppPath, { get_sim_app_pathLogic } from '../get_sim_app_path.ts';
 12 | import type { CommandExecutor } from '../../../../utils/CommandExecutor.ts';
 13 | 
 14 | describe('get_sim_app_path tool', () => {
 15 |   beforeEach(() => {
 16 |     sessionStore.clear();
 17 |   });
 18 | 
 19 |   describe('Export Field Validation (Literal)', () => {
 20 |     it('should have correct name', () => {
 21 |       expect(getSimAppPath.name).toBe('get_sim_app_path');
 22 |     });
 23 | 
 24 |     it('should have concise description', () => {
 25 |       expect(getSimAppPath.description).toBe('Retrieves the built app path for an iOS simulator.');
 26 |     });
 27 | 
 28 |     it('should have handler function', () => {
 29 |       expect(typeof getSimAppPath.handler).toBe('function');
 30 |     });
 31 | 
 32 |     it('should expose only platform in public schema', () => {
 33 |       const schema = z.object(getSimAppPath.schema);
 34 | 
 35 |       expect(schema.safeParse({ platform: 'iOS Simulator' }).success).toBe(true);
 36 |       expect(schema.safeParse({}).success).toBe(false);
 37 |       expect(schema.safeParse({ platform: 'iOS' }).success).toBe(false);
 38 | 
 39 |       const schemaKeys = Object.keys(getSimAppPath.schema).sort();
 40 |       expect(schemaKeys).toEqual(['platform']);
 41 |     });
 42 |   });
 43 | 
 44 |   describe('Handler Requirements', () => {
 45 |     it('should require scheme when not provided', async () => {
 46 |       const result = await getSimAppPath.handler({
 47 |         platform: 'iOS Simulator',
 48 |       });
 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 when scheme default exists', async () => {
 55 |       sessionStore.setDefaults({ scheme: 'MyScheme' });
 56 | 
 57 |       const result = await getSimAppPath.handler({
 58 |         platform: 'iOS Simulator',
 59 |       });
 60 | 
 61 |       expect(result.isError).toBe(true);
 62 |       expect(result.content[0].text).toContain('Provide a project or workspace');
 63 |     });
 64 | 
 65 |     it('should require simulator identifier when scheme and project defaults exist', async () => {
 66 |       sessionStore.setDefaults({
 67 |         scheme: 'MyScheme',
 68 |         projectPath: '/path/to/project.xcodeproj',
 69 |       });
 70 | 
 71 |       const result = await getSimAppPath.handler({
 72 |         platform: 'iOS Simulator',
 73 |       });
 74 | 
 75 |       expect(result.isError).toBe(true);
 76 |       expect(result.content[0].text).toContain('Provide simulatorId or simulatorName');
 77 |     });
 78 | 
 79 |     it('should error when both projectPath and workspacePath provided explicitly', async () => {
 80 |       sessionStore.setDefaults({ scheme: 'MyScheme' });
 81 | 
 82 |       const result = await getSimAppPath.handler({
 83 |         platform: 'iOS Simulator',
 84 |         projectPath: '/path/project.xcodeproj',
 85 |         workspacePath: '/path/workspace.xcworkspace',
 86 |       });
 87 | 
 88 |       expect(result.isError).toBe(true);
 89 |       expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
 90 |       expect(result.content[0].text).toContain('projectPath');
 91 |       expect(result.content[0].text).toContain('workspacePath');
 92 |     });
 93 | 
 94 |     it('should error when both simulatorId and simulatorName provided explicitly', async () => {
 95 |       sessionStore.setDefaults({
 96 |         scheme: 'MyScheme',
 97 |         workspacePath: '/path/to/workspace.xcworkspace',
 98 |       });
 99 | 
100 |       const result = await getSimAppPath.handler({
101 |         platform: 'iOS Simulator',
102 |         simulatorId: 'SIM-UUID',
103 |         simulatorName: 'iPhone 16',
104 |       });
105 | 
106 |       expect(result.isError).toBe(true);
107 |       expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
108 |       expect(result.content[0].text).toContain('simulatorId');
109 |       expect(result.content[0].text).toContain('simulatorName');
110 |     });
111 |   });
112 | 
113 |   describe('Logic Behavior', () => {
114 |     it('should return app path with simulator name destination', async () => {
115 |       const callHistory: Array<{
116 |         command: string[];
117 |         logPrefix?: string;
118 |         useShell?: boolean;
119 |         opts?: unknown;
120 |       }> = [];
121 | 
122 |       const trackingExecutor: CommandExecutor = async (
123 |         command,
124 |         logPrefix,
125 |         useShell,
126 |         opts,
127 |       ): Promise<{
128 |         success: boolean;
129 |         output: string;
130 |         process: ChildProcess;
131 |       }> => {
132 |         callHistory.push({ command, logPrefix, useShell, opts });
133 |         return {
134 |           success: true,
135 |           output:
136 |             '    BUILT_PRODUCTS_DIR = /tmp/DerivedData/Build\n    FULL_PRODUCT_NAME = MyApp.app\n',
137 |           process: { pid: 12345 } as unknown as ChildProcess,
138 |         };
139 |       };
140 | 
141 |       const result = await get_sim_app_pathLogic(
142 |         {
143 |           workspacePath: '/path/to/workspace.xcworkspace',
144 |           scheme: 'MyScheme',
145 |           platform: 'iOS Simulator',
146 |           simulatorName: 'iPhone 16',
147 |           useLatestOS: true,
148 |         },
149 |         trackingExecutor,
150 |       );
151 | 
152 |       expect(callHistory).toHaveLength(1);
153 |       expect(callHistory[0].logPrefix).toBe('Get App Path');
154 |       expect(callHistory[0].useShell).toBe(true);
155 |       expect(callHistory[0].command).toEqual([
156 |         'xcodebuild',
157 |         '-showBuildSettings',
158 |         '-workspace',
159 |         '/path/to/workspace.xcworkspace',
160 |         '-scheme',
161 |         'MyScheme',
162 |         '-configuration',
163 |         'Debug',
164 |         '-destination',
165 |         'platform=iOS Simulator,name=iPhone 16,OS=latest',
166 |       ]);
167 | 
168 |       expect(result.isError).toBe(false);
169 |       expect(result.content[0].text).toContain(
170 |         '✅ App path retrieved successfully: /tmp/DerivedData/Build/MyApp.app',
171 |       );
172 |     });
173 | 
174 |     it('should surface executor failures when build settings cannot be retrieved', async () => {
175 |       const mockExecutor = createMockExecutor({
176 |         success: false,
177 |         error: 'Failed to run xcodebuild',
178 |       });
179 | 
180 |       const result = await get_sim_app_pathLogic(
181 |         {
182 |           projectPath: '/path/to/project.xcodeproj',
183 |           scheme: 'MyScheme',
184 |           platform: 'iOS Simulator',
185 |           simulatorId: 'SIM-UUID',
186 |         },
187 |         mockExecutor,
188 |       );
189 | 
190 |       expect(result.isError).toBe(true);
191 |       expect(result.content[0].text).toContain('Failed to get app path');
192 |       expect(result.content[0].text).toContain('Failed to run xcodebuild');
193 |     });
194 |   });
195 | });
196 | 
```

--------------------------------------------------------------------------------
/src/utils/video_capture.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Video capture utility for simulator recordings using AXe.
  3 |  *
  4 |  * Manages long-running AXe "record-video" processes keyed by simulator UUID.
  5 |  * It aggregates stdout/stderr to parse the generated MP4 path on stop.
  6 |  */
  7 | 
  8 | import type { ChildProcess } from 'child_process';
  9 | import { log } from './logging/index.ts';
 10 | import { getAxePath, getBundledAxeEnvironment } from './axe-helpers.ts';
 11 | import type { CommandExecutor } from './execution/index.ts';
 12 | 
 13 | type Session = {
 14 |   process: unknown;
 15 |   sessionId: string;
 16 |   startedAt: number;
 17 |   buffer: string;
 18 |   ended: boolean;
 19 | };
 20 | 
 21 | const sessions = new Map<string, Session>();
 22 | let signalHandlersAttached = false;
 23 | 
 24 | export interface AxeHelpers {
 25 |   getAxePath: () => string | null;
 26 |   getBundledAxeEnvironment: () => Record<string, string>;
 27 | }
 28 | 
 29 | function ensureSignalHandlersAttached(): void {
 30 |   if (signalHandlersAttached) return;
 31 |   signalHandlersAttached = true;
 32 | 
 33 |   const stopAll = (): void => {
 34 |     for (const [simulatorUuid, sess] of sessions) {
 35 |       try {
 36 |         const child = sess.process as ChildProcess | undefined;
 37 |         child?.kill?.('SIGINT');
 38 |       } catch {
 39 |         // ignore
 40 |       } finally {
 41 |         sessions.delete(simulatorUuid);
 42 |       }
 43 |     }
 44 |   };
 45 | 
 46 |   try {
 47 |     process.on('SIGINT', stopAll);
 48 |     process.on('SIGTERM', stopAll);
 49 |     process.on('exit', stopAll);
 50 |   } catch {
 51 |     // Non-Node environments may not support process signals; ignore
 52 |   }
 53 | }
 54 | 
 55 | function parseLastAbsoluteMp4Path(buffer: string | undefined): string | null {
 56 |   if (!buffer) return null;
 57 |   const matches = [...buffer.matchAll(/(\s|^)(\/[^\s'"]+\.mp4)\b/gi)];
 58 |   if (matches.length === 0) return null;
 59 |   const last = matches[matches.length - 1];
 60 |   return last?.[2] ?? null;
 61 | }
 62 | 
 63 | function createSessionId(simulatorUuid: string): string {
 64 |   return `${simulatorUuid}:${Date.now()}`;
 65 | }
 66 | 
 67 | /**
 68 |  * Start recording video for a simulator using AXe.
 69 |  */
 70 | export async function startSimulatorVideoCapture(
 71 |   params: { simulatorUuid: string; fps?: number },
 72 |   executor: CommandExecutor,
 73 |   axeHelpers?: AxeHelpers,
 74 | ): Promise<{ started: boolean; sessionId?: string; warning?: string; error?: string }> {
 75 |   const simulatorUuid = params.simulatorUuid;
 76 |   if (!simulatorUuid) {
 77 |     return { started: false, error: 'simulatorUuid is required' };
 78 |   }
 79 | 
 80 |   if (sessions.has(simulatorUuid)) {
 81 |     return {
 82 |       started: false,
 83 |       error: 'A video recording session is already active for this simulator. Stop it first.',
 84 |     };
 85 |   }
 86 | 
 87 |   const helpers = axeHelpers ?? {
 88 |     getAxePath,
 89 |     getBundledAxeEnvironment,
 90 |   };
 91 | 
 92 |   const axeBinary = helpers.getAxePath();
 93 |   if (!axeBinary) {
 94 |     return { started: false, error: 'Bundled AXe binary not found' };
 95 |   }
 96 | 
 97 |   const fps = Number.isFinite(params.fps as number) ? Number(params.fps) : 30;
 98 |   const command = [axeBinary, 'record-video', '--udid', simulatorUuid, '--fps', String(fps)];
 99 |   const env = helpers.getBundledAxeEnvironment?.() ?? {};
100 | 
101 |   log('info', `Starting AXe video recording for simulator ${simulatorUuid} at ${fps} fps`);
102 | 
103 |   const result = await executor(command, 'Start Simulator Video Capture', true, { env }, true);
104 | 
105 |   if (!result.success || !result.process) {
106 |     return {
107 |       started: false,
108 |       error: result.error ?? 'Failed to start video capture process',
109 |     };
110 |   }
111 | 
112 |   const child = result.process as ChildProcess;
113 |   const session: Session = {
114 |     process: child,
115 |     sessionId: createSessionId(simulatorUuid),
116 |     startedAt: Date.now(),
117 |     buffer: '',
118 |     ended: false,
119 |   };
120 | 
121 |   try {
122 |     child.stdout?.on('data', (d: unknown) => {
123 |       try {
124 |         session.buffer += String(d ?? '');
125 |       } catch {
126 |         // ignore
127 |       }
128 |     });
129 |     child.stderr?.on('data', (d: unknown) => {
130 |       try {
131 |         session.buffer += String(d ?? '');
132 |       } catch {
133 |         // ignore
134 |       }
135 |     });
136 |   } catch {
137 |     // ignore stream listener setup failures
138 |   }
139 | 
140 |   // Track when the child process naturally ends, so stop can short-circuit
141 |   try {
142 |     child.once?.('exit', () => {
143 |       session.ended = true;
144 |     });
145 |     child.once?.('close', () => {
146 |       session.ended = true;
147 |     });
148 |   } catch {
149 |     // ignore
150 |   }
151 | 
152 |   sessions.set(simulatorUuid, session);
153 |   ensureSignalHandlersAttached();
154 | 
155 |   return {
156 |     started: true,
157 |     sessionId: session.sessionId,
158 |     warning: fps !== (params.fps ?? 30) ? `FPS coerced to ${fps}` : undefined,
159 |   };
160 | }
161 | 
162 | /**
163 |  * Stop recording video for a simulator. Returns aggregated output and parsed MP4 path if found.
164 |  */
165 | export async function stopSimulatorVideoCapture(
166 |   params: { simulatorUuid: string },
167 |   executor: CommandExecutor,
168 | ): Promise<{
169 |   stopped: boolean;
170 |   sessionId?: string;
171 |   stdout?: string;
172 |   parsedPath?: string;
173 |   error?: string;
174 | }> {
175 |   // Mark executor as used to satisfy lint rule
176 |   void executor;
177 | 
178 |   const simulatorUuid = params.simulatorUuid;
179 |   if (!simulatorUuid) {
180 |     return { stopped: false, error: 'simulatorUuid is required' };
181 |   }
182 | 
183 |   const session = sessions.get(simulatorUuid);
184 |   if (!session) {
185 |     return { stopped: false, error: 'No active video recording session for this simulator' };
186 |   }
187 | 
188 |   const child = session.process as ChildProcess | undefined;
189 | 
190 |   // Attempt graceful shutdown
191 |   try {
192 |     child?.kill?.('SIGINT');
193 |   } catch {
194 |     try {
195 |       child?.kill?.();
196 |     } catch {
197 |       // ignore
198 |     }
199 |   }
200 | 
201 |   // Wait for process to close (avoid hanging if it already exited)
202 |   await new Promise<void>((resolve): void => {
203 |     if (!child) return resolve();
204 | 
205 |     // If process has already ended, resolve immediately
206 |     const alreadyEnded = (session as Session).ended === true;
207 |     const hasExitCode = (child as ChildProcess).exitCode !== null;
208 |     const hasSignal = (child as unknown as { signalCode?: string | null }).signalCode != null;
209 |     if (alreadyEnded || hasExitCode || hasSignal) {
210 |       return resolve();
211 |     }
212 | 
213 |     let resolved = false;
214 |     const finish = (): void => {
215 |       if (!resolved) {
216 |         resolved = true;
217 |         resolve();
218 |       }
219 |     };
220 |     try {
221 |       child.once('close', finish);
222 |       child.once('exit', finish);
223 |     } catch {
224 |       return finish();
225 |     }
226 |     // Safety timeout to prevent indefinite hangs
227 |     setTimeout(finish, 5000);
228 |   });
229 | 
230 |   const combinedOutput = session.buffer;
231 |   const parsedPath = parseLastAbsoluteMp4Path(combinedOutput) ?? undefined;
232 | 
233 |   sessions.delete(simulatorUuid);
234 | 
235 |   log(
236 |     'info',
237 |     `Stopped AXe video recording for simulator ${simulatorUuid}. ${parsedPath ? `Detected file: ${parsedPath}` : 'No file detected in output.'}`,
238 |   );
239 | 
240 |   return {
241 |     stopped: true,
242 |     sessionId: session.sessionId,
243 |     stdout: combinedOutput,
244 |     parsedPath,
245 |   };
246 | }
247 | 
```

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

```typescript
  1 | import { z } from 'zod';
  2 | import { ToolResponse } from '../../../types/common.ts';
  3 | import { log } from '../../../utils/logging/index.ts';
  4 | import type { CommandExecutor } from '../../../utils/execution/index.ts';
  5 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
  6 | import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
  7 | import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
  8 | 
  9 | const baseSchemaObject = z.object({
 10 |   simulatorId: z
 11 |     .string()
 12 |     .optional()
 13 |     .describe(
 14 |       'UUID of the simulator to use (obtained from list_sims). Provide EITHER this OR simulatorName, not both',
 15 |     ),
 16 |   simulatorName: z
 17 |     .string()
 18 |     .optional()
 19 |     .describe(
 20 |       "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both",
 21 |     ),
 22 |   bundleId: z
 23 |     .string()
 24 |     .describe("Bundle identifier of the app to launch (e.g., 'com.example.MyApp')"),
 25 |   args: z.array(z.string()).optional().describe('Additional arguments to pass to the app'),
 26 | });
 27 | 
 28 | const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);
 29 | 
 30 | const launchAppSimSchema = baseSchema
 31 |   .refine((val) => val.simulatorId !== undefined || val.simulatorName !== undefined, {
 32 |     message: 'Either simulatorId or simulatorName is required.',
 33 |   })
 34 |   .refine((val) => !(val.simulatorId !== undefined && val.simulatorName !== undefined), {
 35 |     message: 'simulatorId and simulatorName are mutually exclusive. Provide only one.',
 36 |   });
 37 | 
 38 | export type LaunchAppSimParams = z.infer<typeof launchAppSimSchema>;
 39 | 
 40 | export async function launch_app_simLogic(
 41 |   params: LaunchAppSimParams,
 42 |   executor: CommandExecutor,
 43 | ): Promise<ToolResponse> {
 44 |   let simulatorId = params.simulatorId;
 45 |   let simulatorDisplayName = simulatorId ?? '';
 46 | 
 47 |   if (params.simulatorName && !simulatorId) {
 48 |     log('info', `Looking up simulator by name: ${params.simulatorName}`);
 49 | 
 50 |     const simulatorListResult = await executor(
 51 |       ['xcrun', 'simctl', 'list', 'devices', 'available', '--json'],
 52 |       'List Simulators',
 53 |       true,
 54 |     );
 55 |     if (!simulatorListResult.success) {
 56 |       return {
 57 |         content: [
 58 |           {
 59 |             type: 'text',
 60 |             text: `Failed to list simulators: ${simulatorListResult.error}`,
 61 |           },
 62 |         ],
 63 |         isError: true,
 64 |       };
 65 |     }
 66 | 
 67 |     const simulatorsData = JSON.parse(simulatorListResult.output) as {
 68 |       devices: Record<string, Array<{ udid: string; name: string }>>;
 69 |     };
 70 | 
 71 |     let foundSimulator: { udid: string; name: string } | null = null;
 72 |     for (const runtime in simulatorsData.devices) {
 73 |       const devices = simulatorsData.devices[runtime];
 74 |       const simulator = devices.find((device) => device.name === params.simulatorName);
 75 |       if (simulator) {
 76 |         foundSimulator = simulator;
 77 |         break;
 78 |       }
 79 |     }
 80 | 
 81 |     if (!foundSimulator) {
 82 |       return {
 83 |         content: [
 84 |           {
 85 |             type: 'text',
 86 |             text: `Simulator named "${params.simulatorName}" not found. Use list_sims to see available simulators.`,
 87 |           },
 88 |         ],
 89 |         isError: true,
 90 |       };
 91 |     }
 92 | 
 93 |     simulatorId = foundSimulator.udid;
 94 |     simulatorDisplayName = `"${params.simulatorName}" (${foundSimulator.udid})`;
 95 |   }
 96 | 
 97 |   if (!simulatorId) {
 98 |     return {
 99 |       content: [
100 |         {
101 |           type: 'text',
102 |           text: 'No simulator identifier provided',
103 |         },
104 |       ],
105 |       isError: true,
106 |     };
107 |   }
108 | 
109 |   log('info', `Starting xcrun simctl launch request for simulator ${simulatorId}`);
110 | 
111 |   try {
112 |     const getAppContainerCmd = [
113 |       'xcrun',
114 |       'simctl',
115 |       'get_app_container',
116 |       simulatorId,
117 |       params.bundleId,
118 |       'app',
119 |     ];
120 |     const getAppContainerResult = await executor(
121 |       getAppContainerCmd,
122 |       'Check App Installed',
123 |       true,
124 |       undefined,
125 |     );
126 |     if (!getAppContainerResult.success) {
127 |       return {
128 |         content: [
129 |           {
130 |             type: 'text',
131 |             text: `App is not installed on the simulator. Please use install_app_sim before launching.\n\nWorkflow: build → install → launch.`,
132 |           },
133 |         ],
134 |         isError: true,
135 |       };
136 |     }
137 |   } catch {
138 |     return {
139 |       content: [
140 |         {
141 |           type: 'text',
142 |           text: `App is not installed on the simulator (check failed). Please use install_app_sim before launching.\n\nWorkflow: build → install → launch.`,
143 |         },
144 |       ],
145 |       isError: true,
146 |     };
147 |   }
148 | 
149 |   try {
150 |     const command = ['xcrun', 'simctl', 'launch', simulatorId, params.bundleId];
151 |     if (params.args && params.args.length > 0) {
152 |       command.push(...params.args);
153 |     }
154 | 
155 |     const result = await executor(command, 'Launch App in Simulator', true, undefined);
156 | 
157 |     if (!result.success) {
158 |       return {
159 |         content: [
160 |           {
161 |             type: 'text',
162 |             text: `Launch app in simulator operation failed: ${result.error}`,
163 |           },
164 |         ],
165 |       };
166 |     }
167 | 
168 |     const userParamName = params.simulatorName ? 'simulatorName' : 'simulatorUuid';
169 |     const userParamValue = params.simulatorName ?? simulatorId;
170 | 
171 |     return {
172 |       content: [
173 |         {
174 |           type: 'text',
175 |           text: `✅ App launched successfully in simulator ${simulatorDisplayName || simulatorId}.
176 | 
177 | Next Steps:
178 | 1. To see simulator: open_sim()
179 | 2. Log capture: start_sim_log_cap({ ${userParamName}: "${userParamValue}", bundleId: "${params.bundleId}" })
180 |    With console: start_sim_log_cap({ ${userParamName}: "${userParamValue}", bundleId: "${params.bundleId}", captureConsole: true })
181 | 3. Stop logs: stop_sim_log_cap({ logSessionId: 'SESSION_ID' })`,
182 |         },
183 |       ],
184 |     };
185 |   } catch (error) {
186 |     const errorMessage = error instanceof Error ? error.message : String(error);
187 |     log('error', `Error during launch app in simulator operation: ${errorMessage}`);
188 |     return {
189 |       content: [
190 |         {
191 |           type: 'text',
192 |           text: `Launch app in simulator operation failed: ${errorMessage}`,
193 |         },
194 |       ],
195 |     };
196 |   }
197 | }
198 | 
199 | const publicSchemaObject = baseSchemaObject.omit({
200 |   simulatorId: true,
201 |   simulatorName: true,
202 | } as const);
203 | 
204 | export default {
205 |   name: 'launch_app_sim',
206 |   description: 'Launches an app in an iOS simulator.',
207 |   schema: publicSchemaObject.shape,
208 |   handler: createSessionAwareTool<LaunchAppSimParams>({
209 |     internalSchema: launchAppSimSchema as unknown as z.ZodType<LaunchAppSimParams>,
210 |     logicFunction: launch_app_simLogic,
211 |     getExecutor: getDefaultCommandExecutor,
212 |     requirements: [
213 |       { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
214 |     ],
215 |     exclusivePairs: [['simulatorId', 'simulatorName']],
216 |   }),
217 | };
218 | 
```
Page 4/14FirstPrevNextLast