#
tokens: 48398/50000 15/393 files (page 7/16)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 7 of 16. 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
│       ├── README.md
│       ├── release.yml
│       ├── sentry.yml
│       └── stale.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
├── docs
│   ├── CONFIGURATION.md
│   ├── DAP_BACKEND_IMPLEMENTATION_PLAN.md
│   ├── DEBUGGING_ARCHITECTURE.md
│   ├── DEMOS.md
│   ├── dev
│   │   ├── ARCHITECTURE.md
│   │   ├── CODE_QUALITY.md
│   │   ├── CONTRIBUTING.md
│   │   ├── ESLINT_TYPE_SAFETY.md
│   │   ├── MANUAL_TESTING.md
│   │   ├── NODEJS_2025.md
│   │   ├── PLUGIN_DEVELOPMENT.md
│   │   ├── README.md
│   │   ├── RELEASE_PROCESS.md
│   │   ├── RELOADEROO_FOR_XCODEBUILDMCP.md
│   │   ├── RELOADEROO_XCODEBUILDMCP_PRIMER.md
│   │   ├── RELOADEROO.md
│   │   ├── session_management_plan.md
│   │   ├── session-aware-migration-todo.md
│   │   ├── SMITHERY.md
│   │   ├── TEST_RUNNER_ENV_IMPLEMENTATION_PLAN.md
│   │   ├── TESTING.md
│   │   └── ZOD_MIGRATION_GUIDE.md
│   ├── DEVICE_CODE_SIGNING.md
│   ├── GETTING_STARTED.md
│   ├── investigations
│   │   ├── issue-154-screenshot-downscaling.md
│   │   ├── issue-163.md
│   │   ├── issue-debugger-attach-stopped.md
│   │   └── issue-describe-ui-empty-after-debugger-resume.md
│   ├── OVERVIEW.md
│   ├── PRIVACY.md
│   ├── README.md
│   ├── SESSION_DEFAULTS.md
│   ├── TOOLS.md
│   └── TROUBLESHOOTING.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
│   │   ├── .gitignore
│   │   ├── 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
│   │   └── MCPTestTests
│   │       └── MCPTestTests.swift
│   └── 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
│   ├── generate-loaders.ts
│   ├── generate-version.ts
│   ├── release.sh
│   ├── tools-cli.ts
│   ├── update-tools-docs.ts
│   └── verify-smithery-bundle.sh
├── server.json
├── smithery.config.js
├── smithery.yaml
├── src
│   ├── core
│   │   ├── __tests__
│   │   │   └── resources.test.ts
│   │   ├── generated-plugins.ts
│   │   ├── generated-resources.ts
│   │   ├── plugin-registry.ts
│   │   ├── plugin-types.ts
│   │   └── resources.ts
│   ├── doctor-cli.ts
│   ├── index.ts
│   ├── mcp
│   │   ├── resources
│   │   │   ├── __tests__
│   │   │   │   ├── devices.test.ts
│   │   │   │   ├── doctor.test.ts
│   │   │   │   ├── session-status.test.ts
│   │   │   │   └── simulators.test.ts
│   │   │   ├── devices.ts
│   │   │   ├── doctor.ts
│   │   │   ├── session-status.ts
│   │   │   └── simulators.ts
│   │   └── tools
│   │       ├── debugging
│   │       │   ├── debug_attach_sim.ts
│   │       │   ├── debug_breakpoint_add.ts
│   │       │   ├── debug_breakpoint_remove.ts
│   │       │   ├── debug_continue.ts
│   │       │   ├── debug_detach.ts
│   │       │   ├── debug_lldb_command.ts
│   │       │   ├── debug_stack.ts
│   │       │   ├── debug_variables.ts
│   │       │   └── index.ts
│   │       ├── 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
│   │       ├── 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
│   │   ├── bootstrap.ts
│   │   └── server.ts
│   ├── smithery.ts
│   ├── test-utils
│   │   └── mock-executors.ts
│   ├── types
│   │   └── common.ts
│   ├── utils
│   │   ├── __tests__
│   │   │   ├── build-utils-suppress-warnings.test.ts
│   │   │   ├── build-utils.test.ts
│   │   │   ├── debugger-simctl.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
│   │   │   └── workflow-selection.test.ts
│   │   ├── axe
│   │   │   └── index.ts
│   │   ├── axe-helpers.ts
│   │   ├── build
│   │   │   └── index.ts
│   │   ├── build-utils.ts
│   │   ├── capabilities.ts
│   │   ├── command.ts
│   │   ├── CommandExecutor.ts
│   │   ├── debugger
│   │   │   ├── __tests__
│   │   │   │   └── debugger-manager-dap.test.ts
│   │   │   ├── backends
│   │   │   │   ├── __tests__
│   │   │   │   │   └── dap-backend.test.ts
│   │   │   │   ├── dap-backend.ts
│   │   │   │   ├── DebuggerBackend.ts
│   │   │   │   └── lldb-cli-backend.ts
│   │   │   ├── dap
│   │   │   │   ├── __tests__
│   │   │   │   │   └── transport-framing.test.ts
│   │   │   │   ├── adapter-discovery.ts
│   │   │   │   ├── transport.ts
│   │   │   │   └── types.ts
│   │   │   ├── debugger-manager.ts
│   │   │   ├── index.ts
│   │   │   ├── simctl.ts
│   │   │   ├── tool-context.ts
│   │   │   ├── types.ts
│   │   │   └── ui-automation-guard.ts
│   │   ├── environment.ts
│   │   ├── errors.ts
│   │   ├── execution
│   │   │   ├── index.ts
│   │   │   └── interactive-process.ts
│   │   ├── FileSystemExecutor.ts
│   │   ├── log_capture.ts
│   │   ├── log-capture
│   │   │   ├── device-log-sessions.ts
│   │   │   └── index.ts
│   │   ├── logger.ts
│   │   ├── logging
│   │   │   └── index.ts
│   │   ├── plugin-registry
│   │   │   └── index.ts
│   │   ├── responses
│   │   │   └── index.ts
│   │   ├── runtime-registry.ts
│   │   ├── schema-helpers.ts
│   │   ├── sentry.ts
│   │   ├── session-status.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
│   │   ├── workflow-selection.ts
│   │   ├── xcode.ts
│   │   ├── xcodemake
│   │   │   └── index.ts
│   │   └── xcodemake.ts
│   └── version.ts
├── tsconfig.json
├── tsconfig.test.json
├── tsconfig.tests.json
├── tsup.config.ts
├── vitest.config.ts
└── XcodeBuildMCP.code-workspace
```

# Files

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

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

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

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

--------------------------------------------------------------------------------
/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 | }
 10 | 
 11 | export function createPluginDiscoveryPlugin(): Plugin {
 12 |   return {
 13 |     name: 'plugin-discovery',
 14 |     setup(build) {
 15 |       // Generate the workflow loaders file before build starts
 16 |       build.onStart(async () => {
 17 |         try {
 18 |           await generateWorkflowLoaders();
 19 |           await generateResourceLoaders();
 20 |         } catch (error) {
 21 |           console.error('Failed to generate loaders:', error);
 22 |           throw error;
 23 |         }
 24 |       });
 25 |     },
 26 |   };
 27 | }
 28 | 
 29 | export async function generateWorkflowLoaders(): Promise<void> {
 30 |   const pluginsDir = path.resolve(process.cwd(), 'src/mcp/tools');
 31 | 
 32 |   if (!existsSync(pluginsDir)) {
 33 |     throw new Error(`Plugins directory not found: ${pluginsDir}`);
 34 |   }
 35 | 
 36 |   // Scan for workflow directories
 37 |   const workflowDirs = readdirSync(pluginsDir, { withFileTypes: true })
 38 |     .filter((dirent) => dirent.isDirectory())
 39 |     .map((dirent) => dirent.name);
 40 | 
 41 |   const workflowLoaders: Record<string, string> = {};
 42 |   const workflowMetadata: Record<string, WorkflowMetadata> = {};
 43 | 
 44 |   for (const dirName of workflowDirs) {
 45 |     const dirPath = join(pluginsDir, dirName);
 46 |     const indexPath = join(dirPath, 'index.ts');
 47 | 
 48 |     // Check if workflow has index.ts file
 49 |     if (!existsSync(indexPath)) {
 50 |       console.warn(`Skipping ${dirName}: no index.ts file found`);
 51 |       continue;
 52 |     }
 53 | 
 54 |     // Try to extract workflow metadata from index.ts
 55 |     try {
 56 |       const indexContent = readFileSync(indexPath, 'utf8');
 57 |       const metadata = extractWorkflowMetadata(indexContent);
 58 | 
 59 |       if (metadata) {
 60 |         // Find all tool files in this workflow directory
 61 |         const toolFiles = readdirSync(dirPath, { withFileTypes: true })
 62 |           .filter((dirent) => dirent.isFile())
 63 |           .map((dirent) => dirent.name)
 64 |           .filter(
 65 |             (name) =>
 66 |               (name.endsWith('.ts') || name.endsWith('.js')) &&
 67 |               name !== 'index.ts' &&
 68 |               name !== 'index.js' &&
 69 |               !name.endsWith('.test.ts') &&
 70 |               !name.endsWith('.test.js') &&
 71 |               name !== 'active-processes.ts',
 72 |           );
 73 | 
 74 |         workflowLoaders[dirName] = generateWorkflowLoader(dirName, toolFiles);
 75 |         workflowMetadata[dirName] = metadata;
 76 | 
 77 |         console.log(
 78 |           `✅ Discovered workflow: ${dirName} - ${metadata.name} (${toolFiles.length} tools)`,
 79 |         );
 80 |       } else {
 81 |         console.warn(`⚠️  Skipping ${dirName}: invalid workflow metadata`);
 82 |       }
 83 |     } catch (error) {
 84 |       console.warn(`⚠️  Error processing ${dirName}:`, error);
 85 |     }
 86 |   }
 87 | 
 88 |   // Generate the content for generated-plugins.ts
 89 |   const generatedContent = await generatePluginsFileContent(workflowLoaders, workflowMetadata);
 90 | 
 91 |   // Write to the generated file
 92 |   const outputPath = path.resolve(process.cwd(), 'src/core/generated-plugins.ts');
 93 | 
 94 |   const fs = await import('fs');
 95 |   await fs.promises.writeFile(outputPath, generatedContent, 'utf8');
 96 | 
 97 |   console.log(`🔧 Generated workflow loaders for ${Object.keys(workflowLoaders).length} workflows`);
 98 | }
 99 | 
100 | function generateWorkflowLoader(workflowName: string, toolFiles: string[]): string {
101 |   const toolImports = toolFiles
102 |     .map((file, index) => {
103 |       const toolName = file.replace(/\.(ts|js)$/, '');
104 |       return `const tool_${index} = await import('../mcp/tools/${workflowName}/${toolName}.ts').then(m => m.default)`;
105 |     })
106 |     .join(';\n    ');
107 | 
108 |   const toolExports = toolFiles
109 |     .map((file, index) => {
110 |       const toolName = file.replace(/\.(ts|js)$/, '');
111 |       return `'${toolName}': tool_${index}`;
112 |     })
113 |     .join(',\n      ');
114 | 
115 |   return `async () => {
116 |     const { workflow } = await import('../mcp/tools/${workflowName}/index.ts');
117 |     ${toolImports ? toolImports + ';\n    ' : ''}
118 |     return {
119 |       workflow,
120 |       ${toolExports ? toolExports : ''}
121 |     };
122 |   }`;
123 | }
124 | 
125 | function extractWorkflowMetadata(content: string): WorkflowMetadata | null {
126 |   try {
127 |     // Simple regex to extract workflow export object
128 |     const workflowMatch = content.match(/export\s+const\s+workflow\s*=\s*({[\s\S]*?});/);
129 | 
130 |     if (!workflowMatch) {
131 |       return null;
132 |     }
133 | 
134 |     const workflowObj = workflowMatch[1];
135 | 
136 |     // Extract name
137 |     const nameMatch = workflowObj.match(/name\s*:\s*['"`]([^'"`]+)['"`]/);
138 |     if (!nameMatch) return null;
139 | 
140 |     // Extract description
141 |     const descMatch = workflowObj.match(/description\s*:\s*['"`]([\s\S]*?)['"`]/);
142 |     if (!descMatch) return null;
143 | 
144 |     return {
145 |       name: nameMatch[1],
146 |       description: descMatch[1],
147 |     };
148 |   } catch (error) {
149 |     console.warn('Failed to extract workflow metadata:', error);
150 |     return null;
151 |   }
152 | }
153 | 
154 | async function generatePluginsFileContent(
155 |   workflowLoaders: Record<string, string>,
156 |   workflowMetadata: Record<string, WorkflowMetadata>,
157 | ): Promise<string> {
158 |   const loaderEntries = Object.entries(workflowLoaders)
159 |     .map(([key, loader]) => {
160 |       const indentedLoader = loader
161 |         .split('\n')
162 |         .map((line, index) => (index === 0 ? `  '${key}': ${line}` : `  ${line}`))
163 |         .join('\n');
164 |       return indentedLoader;
165 |     })
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 |   const content = `// 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 |   return formatGenerated(content);
194 | }
195 | 
196 | export async function generateResourceLoaders(): Promise<void> {
197 |   const resourcesDir = path.resolve(process.cwd(), 'src/mcp/resources');
198 | 
199 |   if (!existsSync(resourcesDir)) {
200 |     console.log('Resources directory not found, skipping resource generation');
201 |     return;
202 |   }
203 | 
204 |   const resourceFiles = readdirSync(resourcesDir, { withFileTypes: true })
205 |     .filter((dirent) => dirent.isFile())
206 |     .map((dirent) => dirent.name)
207 |     .filter(
208 |       (name) =>
209 |         (name.endsWith('.ts') || name.endsWith('.js')) &&
210 |         !name.endsWith('.test.ts') &&
211 |         !name.endsWith('.test.js') &&
212 |         !name.startsWith('__'),
213 |     );
214 | 
215 |   const resourceLoaders: Record<string, string> = {};
216 | 
217 |   for (const fileName of resourceFiles) {
218 |     const resourceName = fileName.replace(/\.(ts|js)$/, '');
219 |     resourceLoaders[resourceName] = `async () => {
220 |     const module = await import('../mcp/resources/${resourceName}.ts');
221 |     return module.default;
222 |   }`;
223 | 
224 |     console.log(`✅ Discovered resource: ${resourceName}`);
225 |   }
226 | 
227 |   const generatedContent = await generateResourcesFileContent(resourceLoaders);
228 |   const outputPath = path.resolve(process.cwd(), 'src/core/generated-resources.ts');
229 | 
230 |   const fs = await import('fs');
231 |   await fs.promises.writeFile(outputPath, generatedContent, 'utf8');
232 | 
233 |   console.log(`🔧 Generated resource loaders for ${Object.keys(resourceLoaders).length} resources`);
234 | }
235 | 
236 | async function generateResourcesFileContent(
237 |   resourceLoaders: Record<string, string>,
238 | ): Promise<string> {
239 |   const loaderEntries = Object.entries(resourceLoaders)
240 |     .map(([key, loader]) => `  '${key}': ${loader}`)
241 |     .join(',\n');
242 | 
243 |   const content = `// AUTO-GENERATED - DO NOT EDIT
244 | // This file is generated by the plugin discovery esbuild plugin
245 | 
246 | export const RESOURCE_LOADERS = {
247 | ${loaderEntries}
248 | };
249 | 
250 | export type ResourceName = keyof typeof RESOURCE_LOADERS;
251 | `;
252 |   return formatGenerated(content);
253 | }
254 | 
255 | async function formatGenerated(content: string): Promise<string> {
256 |   try {
257 |     const { resolve } = await import('node:path');
258 |     const { pathToFileURL } = await import('node:url');
259 |     const prettier = await import('prettier');
260 |     let config = (await prettier.resolveConfig(process.cwd())) ?? null;
261 |     if (!config) {
262 |       try {
263 |         const configUrl = pathToFileURL(resolve(process.cwd(), '.prettierrc.js')).href;
264 |         const configModule = await import(configUrl);
265 |         config = (configModule as { default?: unknown }).default ?? configModule;
266 |       } catch {
267 |         config = null;
268 |       }
269 |     }
270 |     const options = {
271 |       semi: true,
272 |       trailingComma: 'all' as const,
273 |       singleQuote: true,
274 |       printWidth: 100,
275 |       tabWidth: 2,
276 |       endOfLine: 'auto' as const,
277 |       ...(config as Record<string, unknown> | null),
278 |       parser: 'typescript',
279 |     };
280 |     return prettier.format(content, options);
281 |   } catch {
282 |     return content;
283 |   }
284 | }
285 | 
```

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

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

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

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

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

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

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

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

--------------------------------------------------------------------------------
/src/mcp/tools/logging/__tests__/start_sim_log_cap.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for start_sim_log_cap plugin
  3 |  */
  4 | import { describe, it, expect } from 'vitest';
  5 | import * as z from 'zod';
  6 | import plugin, { start_sim_log_capLogic } from '../start_sim_log_cap.ts';
  7 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
  8 | 
  9 | describe('start_sim_log_cap plugin', () => {
 10 |   // Reset any test state if needed
 11 | 
 12 |   describe('Export Field Validation (Literal)', () => {
 13 |     it('should export an object with required properties', () => {
 14 |       expect(plugin).toHaveProperty('name');
 15 |       expect(plugin).toHaveProperty('description');
 16 |       expect(plugin).toHaveProperty('schema');
 17 |       expect(plugin).toHaveProperty('handler');
 18 |     });
 19 | 
 20 |     it('should have correct tool name', () => {
 21 |       expect(plugin.name).toBe('start_sim_log_cap');
 22 |     });
 23 | 
 24 |     it('should have correct description', () => {
 25 |       expect(plugin.description).toBe(
 26 |         'Starts capturing logs from a specified simulator. Returns a session ID. By default, captures only structured logs.',
 27 |       );
 28 |     });
 29 | 
 30 |     it('should have handler as a function', () => {
 31 |       expect(typeof plugin.handler).toBe('function');
 32 |     });
 33 | 
 34 |     it('should validate schema with valid parameters', () => {
 35 |       const schema = z.object(plugin.schema);
 36 |       expect(schema.safeParse({ bundleId: 'com.example.app' }).success).toBe(true);
 37 |       expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: true }).success).toBe(
 38 |         true,
 39 |       );
 40 |       expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: false }).success).toBe(
 41 |         true,
 42 |       );
 43 |     });
 44 | 
 45 |     it('should reject invalid schema parameters', () => {
 46 |       const schema = z.object(plugin.schema);
 47 |       expect(schema.safeParse({ bundleId: null }).success).toBe(false);
 48 |       expect(schema.safeParse({ captureConsole: true }).success).toBe(false);
 49 |       expect(schema.safeParse({}).success).toBe(false);
 50 |       expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: 'yes' }).success).toBe(
 51 |         false,
 52 |       );
 53 |       expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: 123 }).success).toBe(
 54 |         false,
 55 |       );
 56 | 
 57 |       const withSimId = schema.safeParse({ simulatorId: 'test-uuid', bundleId: 'com.example.app' });
 58 |       expect(withSimId.success).toBe(true);
 59 |       expect('simulatorId' in (withSimId.data as any)).toBe(false);
 60 |     });
 61 |   });
 62 | 
 63 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 64 |     // Note: Parameter validation is now handled by createTypedTool wrapper
 65 |     // Invalid parameters will not reach the logic function, so we test valid scenarios
 66 | 
 67 |     it('should return error when log capture fails', async () => {
 68 |       const mockExecutor = createMockExecutor({ success: true, output: '' });
 69 |       const logCaptureStub = (params: any, executor: any) => {
 70 |         return Promise.resolve({
 71 |           sessionId: '',
 72 |           logFilePath: '',
 73 |           processes: [],
 74 |           error: 'Permission denied',
 75 |         });
 76 |       };
 77 | 
 78 |       const result = await start_sim_log_capLogic(
 79 |         {
 80 |           simulatorId: 'test-uuid',
 81 |           bundleId: 'com.example.app',
 82 |         },
 83 |         mockExecutor,
 84 |         logCaptureStub,
 85 |       );
 86 | 
 87 |       expect(result.isError).toBe(true);
 88 |       expect(result.content[0].text).toBe('Error starting log capture: Permission denied');
 89 |     });
 90 | 
 91 |     it('should return success with session ID when log capture starts successfully', async () => {
 92 |       const mockExecutor = createMockExecutor({ success: true, output: '' });
 93 |       const logCaptureStub = (params: any, executor: any) => {
 94 |         return Promise.resolve({
 95 |           sessionId: 'test-uuid-123',
 96 |           logFilePath: '/tmp/test.log',
 97 |           processes: [],
 98 |           error: undefined,
 99 |         });
100 |       };
101 | 
102 |       const result = await start_sim_log_capLogic(
103 |         {
104 |           simulatorId: 'test-uuid',
105 |           bundleId: 'com.example.app',
106 |         },
107 |         mockExecutor,
108 |         logCaptureStub,
109 |       );
110 | 
111 |       expect(result.isError).toBeUndefined();
112 |       expect(result.content[0].text).toBe(
113 |         "Log capture started successfully. Session ID: test-uuid-123.\n\nNote: Only structured logs are being captured.\n\nNext Steps:\n1.  Interact with your simulator and app.\n2.  Use 'stop_sim_log_cap' with session ID 'test-uuid-123' to stop capture and retrieve logs.",
114 |       );
115 |     });
116 | 
117 |     it('should indicate console capture when captureConsole is true', async () => {
118 |       const mockExecutor = createMockExecutor({ success: true, output: '' });
119 |       const logCaptureStub = (params: any, executor: any) => {
120 |         return Promise.resolve({
121 |           sessionId: 'test-uuid-123',
122 |           logFilePath: '/tmp/test.log',
123 |           processes: [],
124 |           error: undefined,
125 |         });
126 |       };
127 | 
128 |       const result = await start_sim_log_capLogic(
129 |         {
130 |           simulatorId: 'test-uuid',
131 |           bundleId: 'com.example.app',
132 |           captureConsole: true,
133 |         },
134 |         mockExecutor,
135 |         logCaptureStub,
136 |       );
137 | 
138 |       expect(result.content[0].text).toBe(
139 |         "Log capture started successfully. Session ID: test-uuid-123.\n\nNote: Your app was relaunched to capture console output.\n\nNext Steps:\n1.  Interact with your simulator and app.\n2.  Use 'stop_sim_log_cap' with session ID 'test-uuid-123' to stop capture and retrieve logs.",
140 |       );
141 |     });
142 | 
143 |     it('should create correct spawn commands for console capture', async () => {
144 |       const mockExecutor = createMockExecutor({ success: true, output: '' });
145 |       const spawnCalls: Array<{
146 |         command: string;
147 |         args: string[];
148 |       }> = [];
149 | 
150 |       const logCaptureStub = (params: any, executor: any) => {
151 |         if (params.captureConsole) {
152 |           // Record the console capture spawn call
153 |           spawnCalls.push({
154 |             command: 'xcrun',
155 |             args: [
156 |               'simctl',
157 |               'launch',
158 |               '--console-pty',
159 |               '--terminate-running-process',
160 |               params.simulatorUuid,
161 |               params.bundleId,
162 |             ],
163 |           });
164 |         }
165 |         // Record the structured log capture spawn call
166 |         spawnCalls.push({
167 |           command: 'xcrun',
168 |           args: [
169 |             'simctl',
170 |             'spawn',
171 |             params.simulatorUuid,
172 |             'log',
173 |             'stream',
174 |             '--level=debug',
175 |             '--predicate',
176 |             `subsystem == "${params.bundleId}"`,
177 |           ],
178 |         });
179 | 
180 |         return Promise.resolve({
181 |           sessionId: 'test-uuid-123',
182 |           logFilePath: '/tmp/test.log',
183 |           processes: [],
184 |           error: undefined,
185 |         });
186 |       };
187 | 
188 |       await start_sim_log_capLogic(
189 |         {
190 |           simulatorId: 'test-uuid',
191 |           bundleId: 'com.example.app',
192 |           captureConsole: true,
193 |         },
194 |         mockExecutor,
195 |         logCaptureStub,
196 |       );
197 | 
198 |       // Should spawn both console capture and structured log capture
199 |       expect(spawnCalls).toHaveLength(2);
200 |       expect(spawnCalls[0]).toEqual({
201 |         command: 'xcrun',
202 |         args: [
203 |           'simctl',
204 |           'launch',
205 |           '--console-pty',
206 |           '--terminate-running-process',
207 |           'test-uuid',
208 |           'com.example.app',
209 |         ],
210 |       });
211 |       expect(spawnCalls[1]).toEqual({
212 |         command: 'xcrun',
213 |         args: [
214 |           'simctl',
215 |           'spawn',
216 |           'test-uuid',
217 |           'log',
218 |           'stream',
219 |           '--level=debug',
220 |           '--predicate',
221 |           'subsystem == "com.example.app"',
222 |         ],
223 |       });
224 |     });
225 | 
226 |     it('should create correct spawn commands for structured logs only', async () => {
227 |       const mockExecutor = createMockExecutor({ success: true, output: '' });
228 |       const spawnCalls: Array<{
229 |         command: string;
230 |         args: string[];
231 |       }> = [];
232 | 
233 |       const logCaptureStub = (params: any, executor: any) => {
234 |         // Record the structured log capture spawn call only
235 |         spawnCalls.push({
236 |           command: 'xcrun',
237 |           args: [
238 |             'simctl',
239 |             'spawn',
240 |             params.simulatorUuid,
241 |             'log',
242 |             'stream',
243 |             '--level=debug',
244 |             '--predicate',
245 |             `subsystem == "${params.bundleId}"`,
246 |           ],
247 |         });
248 | 
249 |         return Promise.resolve({
250 |           sessionId: 'test-uuid-123',
251 |           logFilePath: '/tmp/test.log',
252 |           processes: [],
253 |           error: undefined,
254 |         });
255 |       };
256 | 
257 |       await start_sim_log_capLogic(
258 |         {
259 |           simulatorId: 'test-uuid',
260 |           bundleId: 'com.example.app',
261 |           captureConsole: false,
262 |         },
263 |         mockExecutor,
264 |         logCaptureStub,
265 |       );
266 | 
267 |       // Should only spawn structured log capture
268 |       expect(spawnCalls).toHaveLength(1);
269 |       expect(spawnCalls[0]).toEqual({
270 |         command: 'xcrun',
271 |         args: [
272 |           'simctl',
273 |           'spawn',
274 |           'test-uuid',
275 |           'log',
276 |           'stream',
277 |           '--level=debug',
278 |           '--predicate',
279 |           'subsystem == "com.example.app"',
280 |         ],
281 |       });
282 |     });
283 |   });
284 | });
285 | 
```

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

```typescript
  1 | /**
  2 |  * Type-safe tool factory for XcodeBuildMCP
  3 |  *
  4 |  * This module provides a factory function to create MCP tool handlers that safely
  5 |  * convert from the generic Record<string, unknown> signature required by the MCP SDK
  6 |  * to strongly-typed parameters using runtime validation with Zod.
  7 |  *
  8 |  * This eliminates the need for unsafe type assertions while maintaining full
  9 |  * compatibility with the MCP SDK's tool handler signature requirements.
 10 |  */
 11 | 
 12 | import * as z from 'zod';
 13 | import { ToolResponse } from '../types/common.ts';
 14 | import type { CommandExecutor } from './execution/index.ts';
 15 | import { createErrorResponse } from './responses/index.ts';
 16 | import { sessionStore, type SessionDefaults } from './session-store.ts';
 17 | import { isSessionDefaultsSchemaOptOutEnabled } from './environment.ts';
 18 | 
 19 | function createValidatedHandler<TParams, TContext>(
 20 |   schema: z.ZodType<TParams, unknown>,
 21 |   logicFunction: (params: TParams, context: TContext) => Promise<ToolResponse>,
 22 |   getContext: () => TContext,
 23 | ): (args: Record<string, unknown>) => Promise<ToolResponse> {
 24 |   return async (args: Record<string, unknown>): Promise<ToolResponse> => {
 25 |     try {
 26 |       const validatedParams = schema.parse(args);
 27 | 
 28 |       return await logicFunction(validatedParams, getContext());
 29 |     } catch (error) {
 30 |       if (error instanceof z.ZodError) {
 31 |         const details = `Invalid parameters:\n${formatZodIssues(error)}`;
 32 |         return createErrorResponse('Parameter validation failed', details);
 33 |       }
 34 | 
 35 |       // Re-throw unexpected errors (they'll be caught by the MCP framework)
 36 |       throw error;
 37 |     }
 38 |   };
 39 | }
 40 | 
 41 | /**
 42 |  * Creates a type-safe tool handler that validates parameters at runtime
 43 |  * before passing them to the typed logic function.
 44 |  *
 45 |  * @param schema - Zod schema for parameter validation
 46 |  * @param logicFunction - The typed logic function to execute
 47 |  * @param getExecutor - Function to get the command executor (must be provided)
 48 |  * @returns A handler function compatible with MCP SDK requirements
 49 |  */
 50 | export function createTypedTool<TParams>(
 51 |   schema: z.ZodType<TParams, unknown>,
 52 |   logicFunction: (params: TParams, executor: CommandExecutor) => Promise<ToolResponse>,
 53 |   getExecutor: () => CommandExecutor,
 54 | ): (args: Record<string, unknown>) => Promise<ToolResponse> {
 55 |   return createValidatedHandler(schema, logicFunction, getExecutor);
 56 | }
 57 | 
 58 | export function createTypedToolWithContext<TParams, TContext>(
 59 |   schema: z.ZodType<TParams, unknown>,
 60 |   logicFunction: (params: TParams, context: TContext) => Promise<ToolResponse>,
 61 |   getContext: () => TContext,
 62 | ): (args: Record<string, unknown>) => Promise<ToolResponse> {
 63 |   return createValidatedHandler(schema, logicFunction, getContext);
 64 | }
 65 | 
 66 | export type SessionRequirement =
 67 |   | { allOf: (keyof SessionDefaults)[]; message?: string }
 68 |   | { oneOf: (keyof SessionDefaults)[]; message?: string };
 69 | 
 70 | function missingFromMerged(
 71 |   keys: (keyof SessionDefaults)[],
 72 |   merged: Record<string, unknown>,
 73 | ): string[] {
 74 |   return keys.filter((k) => merged[k] == null);
 75 | }
 76 | 
 77 | function formatRequirementError(opts: {
 78 |   message: string;
 79 |   setHint?: string;
 80 |   optOutEnabled: boolean;
 81 | }): { title: string; body: string } {
 82 |   const title = opts.optOutEnabled
 83 |     ? 'Missing required parameters'
 84 |     : 'Missing required session defaults';
 85 |   const body = opts.optOutEnabled
 86 |     ? opts.message
 87 |     : [opts.message, opts.setHint].filter(Boolean).join('\n');
 88 |   return { title, body };
 89 | }
 90 | 
 91 | type ToolSchemaShape = Record<string, z.ZodType>;
 92 | 
 93 | export function getSessionAwareToolSchemaShape(opts: {
 94 |   sessionAware: z.ZodObject<ToolSchemaShape>;
 95 |   legacy: z.ZodObject<ToolSchemaShape>;
 96 | }): ToolSchemaShape {
 97 |   return isSessionDefaultsSchemaOptOutEnabled() ? opts.legacy.shape : opts.sessionAware.shape;
 98 | }
 99 | 
100 | export function createSessionAwareTool<TParams>(opts: {
101 |   internalSchema: z.ZodType<TParams, unknown>;
102 |   logicFunction: (params: TParams, executor: CommandExecutor) => Promise<ToolResponse>;
103 |   getExecutor: () => CommandExecutor;
104 |   requirements?: SessionRequirement[];
105 |   exclusivePairs?: (keyof SessionDefaults)[][]; // when args provide one side, drop conflicting session-default side(s)
106 | }): (rawArgs: Record<string, unknown>) => Promise<ToolResponse> {
107 |   return createSessionAwareHandler({
108 |     internalSchema: opts.internalSchema,
109 |     logicFunction: opts.logicFunction,
110 |     getContext: opts.getExecutor,
111 |     requirements: opts.requirements,
112 |     exclusivePairs: opts.exclusivePairs,
113 |   });
114 | }
115 | 
116 | export function createSessionAwareToolWithContext<TParams, TContext>(opts: {
117 |   internalSchema: z.ZodType<TParams, unknown>;
118 |   logicFunction: (params: TParams, context: TContext) => Promise<ToolResponse>;
119 |   getContext: () => TContext;
120 |   requirements?: SessionRequirement[];
121 |   exclusivePairs?: (keyof SessionDefaults)[][];
122 | }): (rawArgs: Record<string, unknown>) => Promise<ToolResponse> {
123 |   return createSessionAwareHandler(opts);
124 | }
125 | 
126 | function createSessionAwareHandler<TParams, TContext>(opts: {
127 |   internalSchema: z.ZodType<TParams, unknown>;
128 |   logicFunction: (params: TParams, context: TContext) => Promise<ToolResponse>;
129 |   getContext: () => TContext;
130 |   requirements?: SessionRequirement[];
131 |   exclusivePairs?: (keyof SessionDefaults)[][];
132 | }): (rawArgs: Record<string, unknown>) => Promise<ToolResponse> {
133 |   const {
134 |     internalSchema,
135 |     logicFunction,
136 |     getContext,
137 |     requirements = [],
138 |     exclusivePairs = [],
139 |   } = opts;
140 | 
141 |   return async (rawArgs: Record<string, unknown>): Promise<ToolResponse> => {
142 |     try {
143 |       // Sanitize args: treat null/undefined as "not provided" so they don't override session defaults
144 |       const sanitizedArgs: Record<string, unknown> = {};
145 |       for (const [k, v] of Object.entries(rawArgs)) {
146 |         if (v === null || v === undefined) continue;
147 |         if (typeof v === 'string' && v.trim() === '') continue;
148 |         sanitizedArgs[k] = v;
149 |       }
150 | 
151 |       // Factory-level mutual exclusivity check: if user provides multiple explicit values
152 |       // within an exclusive group, reject early even if tool schema doesn't enforce XOR.
153 |       for (const pair of exclusivePairs) {
154 |         const provided = pair.filter((k) => Object.prototype.hasOwnProperty.call(sanitizedArgs, k));
155 |         if (provided.length >= 2) {
156 |           return createErrorResponse(
157 |             'Parameter validation failed',
158 |             `Invalid parameters:\nMutually exclusive parameters provided: ${provided.join(
159 |               ', ',
160 |             )}. Provide only one.`,
161 |           );
162 |         }
163 |       }
164 | 
165 |       // Start with session defaults merged with explicit args (args override session)
166 |       const merged: Record<string, unknown> = { ...sessionStore.getAll(), ...sanitizedArgs };
167 | 
168 |       // Apply exclusive pair pruning: only when caller provided a concrete (non-null/undefined) value
169 |       // for any key in the pair. When activated, drop other keys in the pair coming from session defaults.
170 |       for (const pair of exclusivePairs) {
171 |         const userProvidedConcrete = pair.some((k) =>
172 |           Object.prototype.hasOwnProperty.call(sanitizedArgs, k),
173 |         );
174 |         if (!userProvidedConcrete) continue;
175 | 
176 |         for (const k of pair) {
177 |           if (!Object.prototype.hasOwnProperty.call(sanitizedArgs, k) && k in merged) {
178 |             delete merged[k];
179 |           }
180 |         }
181 |       }
182 | 
183 |       for (const req of requirements) {
184 |         if ('allOf' in req) {
185 |           const missing = missingFromMerged(req.allOf, merged);
186 |           if (missing.length > 0) {
187 |             const setHint = `Set with: session-set-defaults { ${missing
188 |               .map((k) => `"${k}": "..."`)
189 |               .join(', ')} }`;
190 |             const { title, body } = formatRequirementError({
191 |               message: req.message ?? `Required: ${req.allOf.join(', ')}`,
192 |               setHint,
193 |               optOutEnabled: isSessionDefaultsSchemaOptOutEnabled(),
194 |             });
195 |             return createErrorResponse(title, body);
196 |           }
197 |         } else if ('oneOf' in req) {
198 |           const satisfied = req.oneOf.some((k) => merged[k] != null);
199 |           if (!satisfied) {
200 |             const options = req.oneOf.join(', ');
201 |             const setHints = req.oneOf
202 |               .map((k) => `session-set-defaults { "${k}": "..." }`)
203 |               .join(' OR ');
204 |             const { title, body } = formatRequirementError({
205 |               message: req.message ?? `Provide one of: ${options}`,
206 |               setHint: `Set with: ${setHints}`,
207 |               optOutEnabled: isSessionDefaultsSchemaOptOutEnabled(),
208 |             });
209 |             return createErrorResponse(title, body);
210 |           }
211 |         }
212 |       }
213 | 
214 |       const validated = internalSchema.parse(merged);
215 |       return await logicFunction(validated, getContext());
216 |     } catch (error) {
217 |       if (error instanceof z.ZodError) {
218 |         const details = `Invalid parameters:\n${formatZodIssues(error)}`;
219 |         return createErrorResponse('Parameter validation failed', details);
220 |       }
221 |       throw error;
222 |     }
223 |   };
224 | }
225 | 
226 | function formatZodIssues(error: z.ZodError): string {
227 |   return error.issues
228 |     .map((issue) => {
229 |       const path = issue.path.length > 0 ? issue.path.map(String).join('.') : 'root';
230 |       return `${path}: ${issue.message}`;
231 |     })
232 |     .join('\n');
233 | }
234 | 
```

--------------------------------------------------------------------------------
/docs/dev/SMITHERY.md:
--------------------------------------------------------------------------------

```markdown
  1 | # TypeScript Servers
  2 | 
  3 | > Deploy and publish TypeScript MCP servers on Smithery using Smithery CLI
  4 | 
  5 | ## Overview
  6 | 
  7 | Deploy TypeScript MCP servers using the official MCP SDK with two deployment options:
  8 | 
  9 | * **Remote deployment**: Automatic containerization and infrastructure managed by Smithery
 10 | * **Local servers** (Beta): Distribute your server as [MCP bundle](https://github.com/anthropics/mcpb) allowing users to run it locally and one-click install it
 11 | 
 12 | ## Prerequisites
 13 | 
 14 | * TypeScript MCP server using the official MCP SDK that exports the MCP server object at entry point
 15 | * Node.js 18+ and npm installed locally
 16 | * Smithery CLI installed as a dev dependency (`npm i -D @smithery/cli`)
 17 | 
 18 | <Note>
 19 |   **New to MCP servers?** See the [Getting Started guide](/getting_started) to learn how to build TypeScript MCP servers from scratch using the official SDK.
 20 | </Note>
 21 | 
 22 | ## Project Structure
 23 | 
 24 | Your TypeScript project should look like this:
 25 | 
 26 | ```
 27 | my-mcp-server/
 28 |   smithery.yaml          # Smithery configuration
 29 |   package.json           # Node.js dependencies and scripts
 30 |   tsconfig.json          # TypeScript configuration
 31 |   src/
 32 |     index.ts             # Your MCP server code with exported createServer function
 33 | ```
 34 | 
 35 | ## Setup
 36 | 
 37 | ### 1. Configure smithery.yaml
 38 | 
 39 | Create a `smithery.yaml` file in your repository root (usually where the `package.json` is):
 40 | 
 41 | #### Remote Deployment (Default)
 42 | 
 43 | ```yaml  theme={null}
 44 | runtime: "typescript"
 45 | ```
 46 | 
 47 | #### Local Server (Beta)
 48 | 
 49 | ```yaml  theme={null}
 50 | runtime: "typescript"
 51 | target: "local"
 52 | ```
 53 | 
 54 | <Note>
 55 |   **Local servers are in beta** - When you set `target: "local"`, your server runs locally on user's machine but is accessible through Smithery's registry for easy discovery and connection by MCP clients.
 56 | </Note>
 57 | 
 58 | ### 2. Configure package.json
 59 | 
 60 | Your `package.json` must include the `module` field pointing to your server entry point:
 61 | 
 62 | ```json  theme={null}
 63 | {
 64 |   "name": "my-mcp-server",
 65 |   "version": "1.0.0",
 66 |   "type": "module",
 67 |   "module": "src/index.ts",  // Points to your server entry point
 68 |   "scripts": {
 69 |     "build": "npx smithery build",
 70 |     "dev": "npx smithery dev"
 71 |   },
 72 |   "dependencies": {
 73 |     "@modelcontextprotocol/sdk": "^1.17.3",
 74 |     "zod": "^3.25.46"
 75 |   },
 76 |   "devDependencies": {
 77 |     "@smithery/cli": "^1.4.6"
 78 |   }
 79 | }
 80 | ```
 81 | 
 82 | <Note>
 83 |   Install the CLI locally with:
 84 | 
 85 |   ```bash  theme={null}
 86 |   npm i -D @smithery/cli
 87 |   ```
 88 | 
 89 |   The Smithery CLI externalizes your SDKs during bundling so your runtime uses the versions you install. If you see a warning about missing SDKs, add them to your dependencies (most servers need `@modelcontextprotocol/sdk` and `@smithery/sdk`).
 90 | </Note>
 91 | 
 92 | ### 3. Ensure Proper Server Structure
 93 | 
 94 | Your TypeScript MCP server must export a default `createServer` function that returns the MCP server object. If you built your server following the [Getting Started guide](/getting_started), it should already have this structure.
 95 | 
 96 | ```typescript  theme={null}
 97 | // src/index.ts
 98 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
 99 | 
100 | // Required: Export default createServer function
101 | export default function createServer({ config }) {
102 |   // config contains user-provided settings (see configSchema below)
103 |   const server = new McpServer({
104 |     name: "Your Server Name",
105 |     version: "1.0.0",
106 |   });
107 | 
108 |   // Register your tools here...
109 | 
110 |   return server.server; // Must return the MCP server object
111 | }
112 | ```
113 | 
114 | **Optional Configuration Schema**: If your server needs user configuration (API keys, settings, etc.), export a `configSchema`:
115 | 
116 | ```typescript  theme={null}
117 | // Optional: If your server doesn't need configuration, omit this
118 | export const configSchema = z.object({
119 |   apiKey: z.string().describe("Your API key"),
120 |   timeout: z.number().default(5000).describe("Request timeout in milliseconds"),
121 | });
122 | ```
123 | 
124 | **Where it goes**: Export `configSchema` from the same file as your `createServer` function (typically `src/index.ts`).
125 | 
126 | **What it does**: Automatically generates [session configuration](/build/session-config) forms for users connecting to your server.
127 | 
128 | ## OAuth
129 | 
130 | <Note>
131 |   OAuth is designed only for **remote servers**. OAuth is not available for local servers (`target: "local"`).
132 | </Note>
133 | 
134 | If your entry module exports `oauth`, Smithery CLI auto-mounts the required OAuth endpoints for you during remote deployment.
135 | 
136 | ### Export an OAuth provider
137 | 
138 | ```typescript  theme={null}
139 | // src/index.ts
140 | import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js"
141 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
142 | import type { OAuthProvider } from "@smithery/sdk"
143 | import { MyProvider } from "./provider.js"
144 | 
145 | export default function createServer({ auth }: { auth: AuthInfo }) {
146 |   const server = new McpServer({ name: "My MCP", version: "1.0.0" })
147 |   // register tools...
148 |   return server.server
149 | }
150 | 
151 | export const oauth: OAuthProvider = new MyProvider() // [!code highlight]
152 | ```
153 | 
154 | The CLI detects `oauth` and injects the auth routes automatically. For implementing `OAuthServerProvider`, see the [official MCP SDK authorization guide](https://modelcontextprotocol.io/docs/tutorials/security/authorization).
155 | 
156 | <Tip>
157 |   **You don't need to implement client registration.** Modern MCP clients use [Client ID Metadata Documents](https://modelcontextprotocol.io/specification/draft/basic/authorization#client-id-metadata-documents) (CIMD). Your server should advertise `client_id_metadata_document_supported: true` in its OAuth metadata — see the [spec requirements](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#implementation-requirements).
158 | </Tip>
159 | 
160 | ## Local Development
161 | 
162 | Test your server locally using the Smithery CLI:
163 | 
164 | ```bash  theme={null}
165 | # Start development server with interactive playground
166 | npm run dev
167 | ```
168 | 
169 | This opens the **Smithery interactive playground** where you can:
170 | 
171 | * Test your MCP server tools in real-time
172 | * See tool responses and debug issues
173 | * Validate your configuration schema
174 | * Experiment with different inputs
175 | 
176 | ## Advanced Build Configuration
177 | 
178 | For advanced use cases, you can customize the build process using a `smithery.config.js` file. This is useful for:
179 | 
180 | * Marking packages as external (to avoid bundling issues)
181 | * Configuring minification, targets, and other build options
182 | * Adding custom esbuild plugins
183 | 
184 | ### Configuration File
185 | 
186 | Create `smithery.config.js` in your project root:
187 | 
188 | ```javascript  theme={null}
189 | export default {
190 |   esbuild: {
191 |     // Mark problematic packages as external
192 |     external: ["playwright-core", "puppeteer-core"],
193 | 
194 |     // Enable minification for production
195 |     minify: true,
196 | 
197 |     // Set Node.js target version
198 |     target: "node18",
199 |   },
200 | };
201 | ```
202 | 
203 | ### Common Use Cases
204 | 
205 | **External Dependencies**: If you encounter bundling issues with packages like Playwright or native modules:
206 | 
207 | ```javascript  theme={null}
208 | export default {
209 |   esbuild: {
210 |     external: ["playwright-core", "sharp", "@grpc/grpc-js"],
211 |   },
212 | };
213 | ```
214 | 
215 | Configuration applies to both `build` and `dev` commands.
216 | 
217 | ## Deploy
218 | 
219 | 1. Push your code (including `smithery.yaml`) to GitHub
220 | 2. [Connect your GitHub](https://smithery.ai/new) to Smithery (or claim your server if already listed)
221 | 3. Navigate to the Deployments tab on your server page
222 | 4. Click Deploy to build and host your server
223 | 
224 | ## Good to Know
225 | 
226 | <Accordion title="What happens under the hood">
227 |   **Remote Deployment**: When you deploy to Smithery's infrastructure:
228 | 
229 |   1. Clone your repository
230 |   2. Parse your `smithery.yaml` to detect TypeScript runtime
231 |   3. Install dependencies with `npm ci`
232 |   4. Build your TypeScript code using the `module` entry point from your `package.json`
233 |   5. Package your server into a containerized HTTP service
234 |   6. Deploy the container to our hosting infrastructure
235 |   7. Send MCP `initialize` and `list_tools` messages with a dummy configuration to discover your server's capabilities
236 |   8. Make it available at `https://server.smithery.ai/your-server`
237 |   9. Handle load balancing, scaling, and monitoring
238 | 
239 |   **Local Server (Beta)**: When you use `target: "local"`:
240 | 
241 |   1. Your server runs locally on user's machine using `npm run dev`
242 |   2. Smithery registers your server in the registry for discovery
243 |   3. MCP clients can find and connect to your local server through Smithery
244 |   4. Your server remains under your control while being accessible to others
245 | </Accordion>
246 | 
247 | ## Troubleshooting
248 | 
249 | <Accordion title="Why does my deployment fail?">
250 |   Common issues and solutions:
251 | 
252 |   **Remote Deployment Issues**:
253 | 
254 |   * **Missing module field**: Ensure your `package.json` has the `module` field pointing to your entry point
255 |   * **Dependencies not found**: All dependencies must be listed in `dependencies` or `devDependencies`
256 |   * **Server doesn't build locally**: Before deploying, verify your server builds and runs locally:
257 |     ```bash  theme={null}
258 |     npm install
259 |     npm run build
260 |     ```
261 |     If this fails, fix any TypeScript compilation errors or missing dependencies first
262 | 
263 |   **Local Server Issues** (Beta):
264 | 
265 |   * **Server not discoverable**: Ensure you have `target: "local"` in your `smithery.yaml`
266 |   * **Local server won't start**: Verify your server runs with `npm run dev` before expecting registry integration
267 |   * **Connection issues**: Make sure your local development environment allows the necessary network connections
268 | </Accordion>
269 | 
270 | 
271 | ---
272 | 
273 | > To find navigation and other pages in this documentation, fetch the llms.txt file at: https://smithery.ai/docs/llms.txt
```

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

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

--------------------------------------------------------------------------------
/src/utils/debugger/backends/lldb-cli-backend.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { InteractiveProcess, InteractiveSpawner } from '../../execution/index.ts';
  2 | import { getDefaultInteractiveSpawner } from '../../execution/index.ts';
  3 | import type { DebuggerBackend } from './DebuggerBackend.ts';
  4 | import type { BreakpointInfo, BreakpointSpec, DebugExecutionState } from '../types.ts';
  5 | 
  6 | const DEFAULT_COMMAND_TIMEOUT_MS = 30_000;
  7 | const DEFAULT_STARTUP_TIMEOUT_MS = 10_000;
  8 | const LLDB_PROMPT = 'XCODEBUILDMCP_LLDB> ';
  9 | const COMMAND_SENTINEL = '__XCODEBUILDMCP_DONE__';
 10 | const COMMAND_SENTINEL_REGEX = new RegExp(`(^|\\r?\\n)${COMMAND_SENTINEL}(\\r?\\n)`);
 11 | 
 12 | class LldbCliBackend implements DebuggerBackend {
 13 |   readonly kind = 'lldb-cli' as const;
 14 | 
 15 |   private readonly spawner: InteractiveSpawner;
 16 |   private readonly prompt = LLDB_PROMPT;
 17 |   private readonly process: InteractiveProcess;
 18 |   private buffer = '';
 19 |   private pending: {
 20 |     resolve: (output: string) => void;
 21 |     reject: (error: Error) => void;
 22 |     timeout: NodeJS.Timeout;
 23 |   } | null = null;
 24 |   private queue: Promise<unknown> = Promise.resolve();
 25 |   private ready: Promise<void>;
 26 |   private disposed = false;
 27 | 
 28 |   constructor(spawner: InteractiveSpawner) {
 29 |     this.spawner = spawner;
 30 |     const lldbCommand = [
 31 |       'xcrun',
 32 |       'lldb',
 33 |       '--no-lldbinit',
 34 |       '-o',
 35 |       `settings set prompt "${this.prompt}"`,
 36 |     ];
 37 | 
 38 |     this.process = this.spawner(lldbCommand);
 39 | 
 40 |     this.process.process.stdout?.on('data', (data: Buffer) => this.handleData(data));
 41 |     this.process.process.stderr?.on('data', (data: Buffer) => this.handleData(data));
 42 |     this.process.process.on('exit', (code, signal) => {
 43 |       const detail = signal ? `signal ${signal}` : `code ${code ?? 'unknown'}`;
 44 |       this.failPending(new Error(`LLDB process exited (${detail})`));
 45 |     });
 46 | 
 47 |     this.ready = this.initialize();
 48 |   }
 49 | 
 50 |   private async initialize(): Promise<void> {
 51 |     // Prime the prompt by running a sentinel command we can parse reliably.
 52 |     this.process.write(`script print("${COMMAND_SENTINEL}")\n`);
 53 |     await this.waitForSentinel(DEFAULT_STARTUP_TIMEOUT_MS);
 54 |   }
 55 | 
 56 |   async waitUntilReady(): Promise<void> {
 57 |     await this.ready;
 58 |   }
 59 | 
 60 |   async attach(opts: { pid: number; simulatorId: string; waitFor?: boolean }): Promise<void> {
 61 |     const command = opts.waitFor
 62 |       ? `process attach --pid ${opts.pid} --waitfor`
 63 |       : `process attach --pid ${opts.pid}`;
 64 |     const output = await this.runCommand(command);
 65 |     assertNoLldbError('attach', output);
 66 |   }
 67 | 
 68 |   async detach(): Promise<void> {
 69 |     const output = await this.runCommand('process detach');
 70 |     assertNoLldbError('detach', output);
 71 |   }
 72 | 
 73 |   async runCommand(command: string, opts?: { timeoutMs?: number }): Promise<string> {
 74 |     return this.enqueue(async () => {
 75 |       if (this.disposed) {
 76 |         throw new Error('LLDB backend disposed');
 77 |       }
 78 |       await this.ready;
 79 |       this.process.write(`${command}\n`);
 80 |       this.process.write(`script print("${COMMAND_SENTINEL}")\n`);
 81 |       const output = await this.waitForSentinel(opts?.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS);
 82 |       return sanitizeOutput(output, this.prompt).trimEnd();
 83 |     });
 84 |   }
 85 | 
 86 |   async resume(): Promise<void> {
 87 |     return this.enqueue(async () => {
 88 |       if (this.disposed) {
 89 |         throw new Error('LLDB backend disposed');
 90 |       }
 91 |       await this.ready;
 92 |       this.process.write('process continue\n');
 93 |     });
 94 |   }
 95 | 
 96 |   async addBreakpoint(
 97 |     spec: BreakpointSpec,
 98 |     opts?: { condition?: string },
 99 |   ): Promise<BreakpointInfo> {
100 |     const command =
101 |       spec.kind === 'file-line'
102 |         ? `breakpoint set --file "${spec.file}" --line ${spec.line}`
103 |         : `breakpoint set --name "${spec.name}"`;
104 |     const output = await this.runCommand(command);
105 |     assertNoLldbError('breakpoint', output);
106 | 
107 |     const match = output.match(/Breakpoint\s+(\d+):/);
108 |     if (!match) {
109 |       throw new Error(`Unable to parse breakpoint id from output: ${output}`);
110 |     }
111 | 
112 |     const id = Number(match[1]);
113 | 
114 |     if (opts?.condition) {
115 |       const condition = formatConditionForLldb(opts.condition);
116 |       const modifyOutput = await this.runCommand(`breakpoint modify -c ${condition} ${id}`);
117 |       assertNoLldbError('breakpoint modify', modifyOutput);
118 |     }
119 | 
120 |     return {
121 |       id,
122 |       spec,
123 |       rawOutput: output,
124 |     };
125 |   }
126 | 
127 |   async removeBreakpoint(id: number): Promise<string> {
128 |     const output = await this.runCommand(`breakpoint delete ${id}`);
129 |     assertNoLldbError('breakpoint delete', output);
130 |     return output;
131 |   }
132 | 
133 |   async getStack(opts?: { threadIndex?: number; maxFrames?: number }): Promise<string> {
134 |     let command = 'thread backtrace';
135 |     if (typeof opts?.maxFrames === 'number') {
136 |       command += ` -c ${opts.maxFrames}`;
137 |     }
138 |     if (typeof opts?.threadIndex === 'number') {
139 |       command += ` ${opts.threadIndex}`;
140 |     }
141 |     return this.runCommand(command);
142 |   }
143 | 
144 |   async getVariables(opts?: { frameIndex?: number }): Promise<string> {
145 |     if (typeof opts?.frameIndex === 'number') {
146 |       await this.runCommand(`frame select ${opts.frameIndex}`);
147 |     }
148 |     return this.runCommand('frame variable');
149 |   }
150 | 
151 |   async getExecutionState(opts?: { timeoutMs?: number }): Promise<DebugExecutionState> {
152 |     try {
153 |       const output = await this.runCommand('process status', {
154 |         timeoutMs: opts?.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS,
155 |       });
156 |       const normalized = output.toLowerCase();
157 | 
158 |       if (/no process|exited|terminated/.test(normalized)) {
159 |         return { status: 'terminated', description: output.trim() };
160 |       }
161 |       if (/\bstopped\b/.test(normalized)) {
162 |         return {
163 |           status: 'stopped',
164 |           reason: parseStopReason(output),
165 |           description: output.trim(),
166 |         };
167 |       }
168 |       if (/\brunning\b/.test(normalized)) {
169 |         return { status: 'running', description: output.trim() };
170 |       }
171 |       if (/error:/.test(normalized)) {
172 |         return { status: 'unknown', description: output.trim() };
173 |       }
174 | 
175 |       return { status: 'unknown', description: output.trim() };
176 |     } catch (error) {
177 |       return {
178 |         status: 'unknown',
179 |         description: error instanceof Error ? error.message : String(error),
180 |       };
181 |     }
182 |   }
183 | 
184 |   async dispose(): Promise<void> {
185 |     if (this.disposed) return;
186 |     this.disposed = true;
187 |     this.failPending(new Error('LLDB backend disposed'));
188 |     this.process.dispose();
189 |   }
190 | 
191 |   private enqueue<T>(work: () => Promise<T>): Promise<T> {
192 |     const next = this.queue.then(work, work) as Promise<T>;
193 |     this.queue = next.then(
194 |       () => undefined,
195 |       () => undefined,
196 |     );
197 |     return next;
198 |   }
199 | 
200 |   private handleData(data: Buffer): void {
201 |     this.buffer += data.toString('utf8');
202 |     this.checkPending();
203 |   }
204 | 
205 |   private waitForSentinel(timeoutMs: number): Promise<string> {
206 |     if (this.pending) {
207 |       return Promise.reject(new Error('LLDB command already pending'));
208 |     }
209 | 
210 |     return new Promise((resolve, reject) => {
211 |       const timeout = setTimeout(() => {
212 |         this.pending = null;
213 |         reject(new Error(`LLDB command timed out after ${timeoutMs}ms`));
214 |       }, timeoutMs);
215 | 
216 |       this.pending = { resolve, reject, timeout };
217 |       this.checkPending();
218 |     });
219 |   }
220 | 
221 |   private checkPending(): void {
222 |     if (!this.pending) return;
223 |     const sentinelMatch = this.buffer.match(COMMAND_SENTINEL_REGEX);
224 |     const sentinelIndex = sentinelMatch?.index;
225 |     const sentinelLength = sentinelMatch?.[0].length;
226 |     if (sentinelIndex == null || sentinelLength == null) return;
227 | 
228 |     const output = this.buffer.slice(0, sentinelIndex);
229 |     const remainderStart = sentinelIndex + sentinelLength;
230 | 
231 |     const promptIndex = this.buffer.indexOf(this.prompt, remainderStart);
232 |     if (promptIndex !== -1) {
233 |       this.buffer = this.buffer.slice(promptIndex + this.prompt.length);
234 |     } else {
235 |       this.buffer = this.buffer.slice(remainderStart);
236 |     }
237 | 
238 |     const { resolve, timeout } = this.pending;
239 |     this.pending = null;
240 |     clearTimeout(timeout);
241 |     resolve(output);
242 |   }
243 | 
244 |   private failPending(error: Error): void {
245 |     if (!this.pending) return;
246 |     const { reject, timeout } = this.pending;
247 |     this.pending = null;
248 |     clearTimeout(timeout);
249 |     reject(error);
250 |   }
251 | }
252 | 
253 | function assertNoLldbError(context: string, output: string): void {
254 |   if (/error:/i.test(output)) {
255 |     throw new Error(`LLDB ${context} failed: ${output.trim()}`);
256 |   }
257 | }
258 | 
259 | function sanitizeOutput(output: string, prompt: string): string {
260 |   const lines = output.split(/\r?\n/);
261 |   const filtered = lines.filter((line) => {
262 |     if (!line) return false;
263 |     if (line.startsWith(prompt)) return false;
264 |     if (line.includes(`script print("${COMMAND_SENTINEL}")`)) return false;
265 |     if (line.includes(COMMAND_SENTINEL)) return false;
266 |     return true;
267 |   });
268 |   return filtered.join('\n');
269 | }
270 | 
271 | function formatConditionForLldb(condition: string): string {
272 |   const escaped = condition.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
273 |   return `"${escaped}"`;
274 | }
275 | 
276 | function parseStopReason(output: string): string | undefined {
277 |   const match = output.match(/stop reason\s*=\s*(.+)/i);
278 |   if (!match) return undefined;
279 |   return match[1]?.trim() || undefined;
280 | }
281 | 
282 | export async function createLldbCliBackend(
283 |   spawner: InteractiveSpawner = getDefaultInteractiveSpawner(),
284 | ): Promise<DebuggerBackend> {
285 |   const backend = new LldbCliBackend(spawner);
286 |   try {
287 |     await backend.waitUntilReady();
288 |   } catch (error) {
289 |     try {
290 |       await backend.dispose();
291 |     } catch {
292 |       // Best-effort cleanup; keep original error.
293 |     }
294 |     throw error;
295 |   }
296 |   return backend;
297 | }
298 | 
```

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

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

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

```typescript
  1 | import * as 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 { getDefaultDebuggerManager } from '../../../utils/debugger/index.ts';
  8 | import type { DebuggerManager } from '../../../utils/debugger/debugger-manager.ts';
  9 | import { guardUiAutomationAgainstStoppedDebugger } from '../../../utils/debugger/ui-automation-guard.ts';
 10 | import {
 11 |   createAxeNotAvailableResponse,
 12 |   getAxePath,
 13 |   getBundledAxeEnvironment,
 14 | } from '../../../utils/axe-helpers.ts';
 15 | import { DependencyError, AxeError, SystemError } from '../../../utils/errors.ts';
 16 | import {
 17 |   createSessionAwareTool,
 18 |   getSessionAwareToolSchemaShape,
 19 | } from '../../../utils/typed-tool-factory.ts';
 20 | 
 21 | export interface AxeHelpers {
 22 |   getAxePath: () => string | null;
 23 |   getBundledAxeEnvironment: () => Record<string, string>;
 24 |   createAxeNotAvailableResponse: () => ToolResponse;
 25 | }
 26 | 
 27 | // Define schema as ZodObject
 28 | const baseTapSchema = z.object({
 29 |   simulatorId: z.uuid({ message: 'Invalid Simulator UUID format' }),
 30 |   x: z.number().int({ message: 'X coordinate must be an integer' }).optional(),
 31 |   y: z.number().int({ message: 'Y coordinate must be an integer' }).optional(),
 32 |   id: z.string().min(1, { message: 'Id must be non-empty' }).optional(),
 33 |   label: z.string().min(1, { message: 'Label must be non-empty' }).optional(),
 34 |   preDelay: z.number().min(0, { message: 'Pre-delay must be non-negative' }).optional(),
 35 |   postDelay: z.number().min(0, { message: 'Post-delay must be non-negative' }).optional(),
 36 | });
 37 | 
 38 | const tapSchema = baseTapSchema.superRefine((values, ctx) => {
 39 |   const hasX = values.x !== undefined;
 40 |   const hasY = values.y !== undefined;
 41 |   const hasId = values.id !== undefined;
 42 |   const hasLabel = values.label !== undefined;
 43 | 
 44 |   if (!hasX && !hasY && hasId && hasLabel) {
 45 |     ctx.addIssue({
 46 |       code: z.ZodIssueCode.custom,
 47 |       path: ['id'],
 48 |       message: 'Provide either id or label, not both.',
 49 |     });
 50 |   }
 51 | 
 52 |   if (hasX !== hasY) {
 53 |     if (!hasX) {
 54 |       ctx.addIssue({
 55 |         code: z.ZodIssueCode.custom,
 56 |         path: ['x'],
 57 |         message: 'X coordinate is required when y is provided.',
 58 |       });
 59 |     }
 60 |     if (!hasY) {
 61 |       ctx.addIssue({
 62 |         code: z.ZodIssueCode.custom,
 63 |         path: ['y'],
 64 |         message: 'Y coordinate is required when x is provided.',
 65 |       });
 66 |     }
 67 |   }
 68 | 
 69 |   if (!hasX && !hasY && !hasId && !hasLabel) {
 70 |     ctx.addIssue({
 71 |       code: z.ZodIssueCode.custom,
 72 |       path: ['x'],
 73 |       message: 'Provide x/y coordinates or an element id/label.',
 74 |     });
 75 |   }
 76 | });
 77 | 
 78 | // Use z.infer for type safety
 79 | type TapParams = z.infer<typeof tapSchema>;
 80 | 
 81 | const publicSchemaObject = z.strictObject(baseTapSchema.omit({ simulatorId: true } as const).shape);
 82 | 
 83 | const LOG_PREFIX = '[AXe]';
 84 | 
 85 | // Session tracking for describe_ui warnings (shared across UI tools)
 86 | const describeUITimestamps = new Map<string, { timestamp: number }>();
 87 | const DESCRIBE_UI_WARNING_TIMEOUT = 60000; // 60 seconds
 88 | 
 89 | function getCoordinateWarning(simulatorId: string): string | null {
 90 |   const session = describeUITimestamps.get(simulatorId);
 91 |   if (!session) {
 92 |     return 'Warning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.';
 93 |   }
 94 | 
 95 |   const timeSinceDescribe = Date.now() - session.timestamp;
 96 |   if (timeSinceDescribe > DESCRIBE_UI_WARNING_TIMEOUT) {
 97 |     const secondsAgo = Math.round(timeSinceDescribe / 1000);
 98 |     return `Warning: describe_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with describe_ui instead of using potentially stale coordinates.`;
 99 |   }
100 | 
101 |   return null;
102 | }
103 | 
104 | export async function tapLogic(
105 |   params: TapParams,
106 |   executor: CommandExecutor,
107 |   axeHelpers: AxeHelpers = {
108 |     getAxePath,
109 |     getBundledAxeEnvironment,
110 |     createAxeNotAvailableResponse,
111 |   },
112 |   debuggerManager: DebuggerManager = getDefaultDebuggerManager(),
113 | ): Promise<ToolResponse> {
114 |   const toolName = 'tap';
115 |   const { simulatorId, x, y, id, label, preDelay, postDelay } = params;
116 | 
117 |   const guard = await guardUiAutomationAgainstStoppedDebugger({
118 |     debugger: debuggerManager,
119 |     simulatorId,
120 |     toolName,
121 |   });
122 |   if (guard.blockedResponse) return guard.blockedResponse;
123 | 
124 |   let targetDescription = '';
125 |   let actionDescription = '';
126 |   let usesCoordinates = false;
127 |   const commandArgs = ['tap'];
128 | 
129 |   if (x !== undefined && y !== undefined) {
130 |     usesCoordinates = true;
131 |     targetDescription = `(${x}, ${y})`;
132 |     actionDescription = `Tap at ${targetDescription}`;
133 |     commandArgs.push('-x', String(x), '-y', String(y));
134 |   } else if (id !== undefined) {
135 |     targetDescription = `element id "${id}"`;
136 |     actionDescription = `Tap on ${targetDescription}`;
137 |     commandArgs.push('--id', id);
138 |   } else if (label !== undefined) {
139 |     targetDescription = `element label "${label}"`;
140 |     actionDescription = `Tap on ${targetDescription}`;
141 |     commandArgs.push('--label', label);
142 |   } else {
143 |     return createErrorResponse(
144 |       'Parameter validation failed',
145 |       'Invalid parameters:\nroot: Missing tap target',
146 |     );
147 |   }
148 | 
149 |   if (preDelay !== undefined) {
150 |     commandArgs.push('--pre-delay', String(preDelay));
151 |   }
152 |   if (postDelay !== undefined) {
153 |     commandArgs.push('--post-delay', String(postDelay));
154 |   }
155 | 
156 |   log('info', `${LOG_PREFIX}/${toolName}: Starting for ${targetDescription} on ${simulatorId}`);
157 | 
158 |   try {
159 |     await executeAxeCommand(commandArgs, simulatorId, 'tap', executor, axeHelpers);
160 |     log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`);
161 | 
162 |     const coordinateWarning = usesCoordinates ? getCoordinateWarning(simulatorId) : null;
163 |     const message = `${actionDescription} simulated successfully.`;
164 |     const warnings = [guard.warningText, coordinateWarning].filter(Boolean).join('\n\n');
165 | 
166 |     if (warnings) {
167 |       return createTextResponse(`${message}\n\n${warnings}`);
168 |     }
169 | 
170 |     return createTextResponse(message);
171 |   } catch (error: unknown) {
172 |     const errorMessage = error instanceof Error ? error.message : String(error);
173 |     log('error', `${LOG_PREFIX}/${toolName}: Failed - ${errorMessage}`);
174 |     if (error instanceof DependencyError) {
175 |       return axeHelpers.createAxeNotAvailableResponse();
176 |     } else if (error instanceof AxeError) {
177 |       return createErrorResponse(
178 |         `Failed to simulate ${actionDescription.toLowerCase()}: ${error.message}`,
179 |         error.axeOutput,
180 |       );
181 |     } else if (error instanceof SystemError) {
182 |       return createErrorResponse(
183 |         `System error executing axe: ${error.message}`,
184 |         error.originalError?.stack,
185 |       );
186 |     }
187 |     return createErrorResponse(
188 |       `An unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`,
189 |     );
190 |   }
191 | }
192 | 
193 | export default {
194 |   name: 'tap',
195 |   description:
196 |     "Tap at specific coordinates or target elements by accessibility id or label. Use describe_ui to get precise element coordinates prior to using x/y parameters (don't guess from screenshots). Supports optional timing delays.",
197 |   schema: getSessionAwareToolSchemaShape({
198 |     sessionAware: publicSchemaObject,
199 |     legacy: baseTapSchema,
200 |   }),
201 |   annotations: {
202 |     title: 'Tap',
203 |     destructiveHint: true,
204 |   },
205 |   handler: createSessionAwareTool<TapParams>({
206 |     internalSchema: tapSchema as unknown as z.ZodType<TapParams, unknown>,
207 |     logicFunction: (params: TapParams, executor: CommandExecutor) =>
208 |       tapLogic(params, executor, {
209 |         getAxePath,
210 |         getBundledAxeEnvironment,
211 |         createAxeNotAvailableResponse,
212 |       }),
213 |     getExecutor: getDefaultCommandExecutor,
214 |     requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],
215 |   }),
216 | };
217 | 
218 | // Helper function for executing axe commands (inlined from src/tools/axe/index.ts)
219 | async function executeAxeCommand(
220 |   commandArgs: string[],
221 |   simulatorId: string,
222 |   commandName: string,
223 |   executor: CommandExecutor = getDefaultCommandExecutor(),
224 |   axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse },
225 | ): Promise<void> {
226 |   // Get the appropriate axe binary path
227 |   const axeBinary = axeHelpers.getAxePath();
228 |   if (!axeBinary) {
229 |     throw new DependencyError('AXe binary not found');
230 |   }
231 | 
232 |   // Add --udid parameter to all commands
233 |   const fullArgs = [...commandArgs, '--udid', simulatorId];
234 | 
235 |   // Construct the full command array with the axe binary as the first element
236 |   const fullCommand = [axeBinary, ...fullArgs];
237 | 
238 |   try {
239 |     // Determine environment variables for bundled AXe
240 |     const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined;
241 | 
242 |     const result = await executor(
243 |       fullCommand,
244 |       `${LOG_PREFIX}: ${commandName}`,
245 |       false,
246 |       axeEnv ? { env: axeEnv } : undefined,
247 |     );
248 | 
249 |     if (!result.success) {
250 |       throw new AxeError(
251 |         `axe command '${commandName}' failed.`,
252 |         commandName,
253 |         result.error ?? result.output,
254 |         simulatorId,
255 |       );
256 |     }
257 | 
258 |     // Check for stderr output in successful commands
259 |     if (result.error) {
260 |       log(
261 |         'warn',
262 |         `${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${result.error}`,
263 |       );
264 |     }
265 | 
266 |     // Function now returns void - the calling code creates its own response
267 |   } catch (error: unknown) {
268 |     if (error instanceof Error) {
269 |       if (error instanceof AxeError) {
270 |         throw error;
271 |       }
272 | 
273 |       // Otherwise wrap it in a SystemError
274 |       throw new SystemError(`Failed to execute axe command: ${error.message}`, error);
275 |     }
276 | 
277 |     // For any other type of error
278 |     throw new SystemError(`Failed to execute axe command: ${String(error)}`);
279 |   }
280 | }
281 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator-management/__tests__/set_sim_location.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for set_sim_location plugin
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using pure dependency injection for deterministic testing
  5 |  */
  6 | 
  7 | import { describe, it, expect, beforeEach } from 'vitest';
  8 | import * as z from 'zod';
  9 | import {
 10 |   createMockCommandResponse,
 11 |   createMockExecutor,
 12 |   createNoopExecutor,
 13 | } from '../../../../test-utils/mock-executors.ts';
 14 | import setSimLocation, { set_sim_locationLogic } from '../set_sim_location.ts';
 15 | 
 16 | describe('set_sim_location tool', () => {
 17 |   // No mocks to clear since we use pure dependency injection
 18 | 
 19 |   describe('Export Field Validation (Literal)', () => {
 20 |     it('should have correct name', () => {
 21 |       expect(setSimLocation.name).toBe('set_sim_location');
 22 |     });
 23 | 
 24 |     it('should have correct description', () => {
 25 |       expect(setSimLocation.description).toBe('Sets a custom GPS location for the simulator.');
 26 |     });
 27 | 
 28 |     it('should have handler function', () => {
 29 |       expect(typeof setSimLocation.handler).toBe('function');
 30 |     });
 31 | 
 32 |     it('should expose public schema without simulatorId field', () => {
 33 |       const schema = z.object(setSimLocation.schema);
 34 | 
 35 |       expect(schema.safeParse({ latitude: 37.7749, longitude: -122.4194 }).success).toBe(true);
 36 |       expect(schema.safeParse({ latitude: 0, longitude: 0 }).success).toBe(true);
 37 |       expect(schema.safeParse({ latitude: 37.7749 }).success).toBe(false);
 38 |       expect(schema.safeParse({ longitude: -122.4194 }).success).toBe(false);
 39 |       const withSimId = schema.safeParse({
 40 |         simulatorId: 'test-uuid-123',
 41 |         latitude: 37.7749,
 42 |         longitude: -122.4194,
 43 |       });
 44 |       expect(withSimId.success).toBe(true);
 45 |       expect('simulatorId' in (withSimId.data as any)).toBe(false);
 46 |     });
 47 |   });
 48 | 
 49 |   describe('Command Generation', () => {
 50 |     it('should generate correct simctl command', async () => {
 51 |       let capturedCommand: string[] = [];
 52 | 
 53 |       const mockExecutor = async (command: string[]) => {
 54 |         capturedCommand = command;
 55 |         return createMockCommandResponse({
 56 |           success: true,
 57 |           output: 'Location set successfully',
 58 |           error: undefined,
 59 |         });
 60 |       };
 61 | 
 62 |       await set_sim_locationLogic(
 63 |         {
 64 |           simulatorId: 'test-uuid-123',
 65 |           latitude: 37.7749,
 66 |           longitude: -122.4194,
 67 |         },
 68 |         mockExecutor,
 69 |       );
 70 | 
 71 |       expect(capturedCommand).toEqual([
 72 |         'xcrun',
 73 |         'simctl',
 74 |         'location',
 75 |         'test-uuid-123',
 76 |         'set',
 77 |         '37.7749,-122.4194',
 78 |       ]);
 79 |     });
 80 | 
 81 |     it('should generate command with different coordinates', async () => {
 82 |       let capturedCommand: string[] = [];
 83 | 
 84 |       const mockExecutor = async (command: string[]) => {
 85 |         capturedCommand = command;
 86 |         return createMockCommandResponse({
 87 |           success: true,
 88 |           output: 'Location set successfully',
 89 |           error: undefined,
 90 |         });
 91 |       };
 92 | 
 93 |       await set_sim_locationLogic(
 94 |         {
 95 |           simulatorId: 'different-uuid',
 96 |           latitude: 45.5,
 97 |           longitude: -73.6,
 98 |         },
 99 |         mockExecutor,
100 |       );
101 | 
102 |       expect(capturedCommand).toEqual([
103 |         'xcrun',
104 |         'simctl',
105 |         'location',
106 |         'different-uuid',
107 |         'set',
108 |         '45.5,-73.6',
109 |       ]);
110 |     });
111 | 
112 |     it('should generate command with negative coordinates', async () => {
113 |       let capturedCommand: string[] = [];
114 | 
115 |       const mockExecutor = async (command: string[]) => {
116 |         capturedCommand = command;
117 |         return createMockCommandResponse({
118 |           success: true,
119 |           output: 'Location set successfully',
120 |           error: undefined,
121 |         });
122 |       };
123 | 
124 |       await set_sim_locationLogic(
125 |         {
126 |           simulatorId: 'test-uuid',
127 |           latitude: -90,
128 |           longitude: -180,
129 |         },
130 |         mockExecutor,
131 |       );
132 | 
133 |       expect(capturedCommand).toEqual([
134 |         'xcrun',
135 |         'simctl',
136 |         'location',
137 |         'test-uuid',
138 |         'set',
139 |         '-90,-180',
140 |       ]);
141 |     });
142 |   });
143 | 
144 |   describe('Response Processing', () => {
145 |     it('should handle successful location setting', async () => {
146 |       const mockExecutor = createMockExecutor({
147 |         success: true,
148 |         output: 'Location set successfully',
149 |         error: undefined,
150 |       });
151 | 
152 |       const result = await set_sim_locationLogic(
153 |         {
154 |           simulatorId: 'test-uuid-123',
155 |           latitude: 37.7749,
156 |           longitude: -122.4194,
157 |         },
158 |         mockExecutor,
159 |       );
160 | 
161 |       expect(result).toEqual({
162 |         content: [
163 |           {
164 |             type: 'text',
165 |             text: 'Successfully set simulator test-uuid-123 location to 37.7749,-122.4194',
166 |           },
167 |         ],
168 |       });
169 |     });
170 | 
171 |     it('should handle latitude validation failure', async () => {
172 |       const result = await set_sim_locationLogic(
173 |         {
174 |           simulatorId: 'test-uuid-123',
175 |           latitude: 95,
176 |           longitude: -122.4194,
177 |         },
178 |         createNoopExecutor(),
179 |       );
180 | 
181 |       expect(result).toEqual({
182 |         content: [
183 |           {
184 |             type: 'text',
185 |             text: 'Latitude must be between -90 and 90 degrees',
186 |           },
187 |         ],
188 |       });
189 |     });
190 | 
191 |     it('should handle longitude validation failure', async () => {
192 |       const result = await set_sim_locationLogic(
193 |         {
194 |           simulatorId: 'test-uuid-123',
195 |           latitude: 37.7749,
196 |           longitude: -185,
197 |         },
198 |         createNoopExecutor(),
199 |       );
200 | 
201 |       expect(result).toEqual({
202 |         content: [
203 |           {
204 |             type: 'text',
205 |             text: 'Longitude must be between -180 and 180 degrees',
206 |           },
207 |         ],
208 |       });
209 |     });
210 | 
211 |     it('should handle command failure', async () => {
212 |       const mockExecutor = createMockExecutor({
213 |         success: false,
214 |         output: '',
215 |         error: 'Simulator not found',
216 |       });
217 | 
218 |       const result = await set_sim_locationLogic(
219 |         {
220 |           simulatorId: 'invalid-uuid',
221 |           latitude: 37.7749,
222 |           longitude: -122.4194,
223 |         },
224 |         mockExecutor,
225 |       );
226 | 
227 |       expect(result).toEqual({
228 |         content: [
229 |           {
230 |             type: 'text',
231 |             text: 'Failed to set simulator location: Simulator not found',
232 |           },
233 |         ],
234 |       });
235 |     });
236 | 
237 |     it('should handle exception with Error object', async () => {
238 |       const mockExecutor = createMockExecutor(new Error('Connection failed'));
239 | 
240 |       const result = await set_sim_locationLogic(
241 |         {
242 |           simulatorId: 'test-uuid-123',
243 |           latitude: 37.7749,
244 |           longitude: -122.4194,
245 |         },
246 |         mockExecutor,
247 |       );
248 | 
249 |       expect(result).toEqual({
250 |         content: [
251 |           {
252 |             type: 'text',
253 |             text: 'Failed to set simulator location: Connection failed',
254 |           },
255 |         ],
256 |       });
257 |     });
258 | 
259 |     it('should handle exception with string error', async () => {
260 |       const mockExecutor = createMockExecutor('String error');
261 | 
262 |       const result = await set_sim_locationLogic(
263 |         {
264 |           simulatorId: 'test-uuid-123',
265 |           latitude: 37.7749,
266 |           longitude: -122.4194,
267 |         },
268 |         mockExecutor,
269 |       );
270 | 
271 |       expect(result).toEqual({
272 |         content: [
273 |           {
274 |             type: 'text',
275 |             text: 'Failed to set simulator location: String error',
276 |           },
277 |         ],
278 |       });
279 |     });
280 | 
281 |     it('should handle boundary values for coordinates', async () => {
282 |       const mockExecutor = createMockExecutor({
283 |         success: true,
284 |         output: 'Location set successfully',
285 |         error: undefined,
286 |       });
287 | 
288 |       const result = await set_sim_locationLogic(
289 |         {
290 |           simulatorId: 'test-uuid-123',
291 |           latitude: 90,
292 |           longitude: 180,
293 |         },
294 |         mockExecutor,
295 |       );
296 | 
297 |       expect(result).toEqual({
298 |         content: [
299 |           {
300 |             type: 'text',
301 |             text: 'Successfully set simulator test-uuid-123 location to 90,180',
302 |           },
303 |         ],
304 |       });
305 |     });
306 | 
307 |     it('should handle boundary values for negative coordinates', async () => {
308 |       const mockExecutor = createMockExecutor({
309 |         success: true,
310 |         output: 'Location set successfully',
311 |         error: undefined,
312 |       });
313 | 
314 |       const result = await set_sim_locationLogic(
315 |         {
316 |           simulatorId: 'test-uuid-123',
317 |           latitude: -90,
318 |           longitude: -180,
319 |         },
320 |         mockExecutor,
321 |       );
322 | 
323 |       expect(result).toEqual({
324 |         content: [
325 |           {
326 |             type: 'text',
327 |             text: 'Successfully set simulator test-uuid-123 location to -90,-180',
328 |           },
329 |         ],
330 |       });
331 |     });
332 | 
333 |     it('should handle zero coordinates', async () => {
334 |       const mockExecutor = createMockExecutor({
335 |         success: true,
336 |         output: 'Location set successfully',
337 |         error: undefined,
338 |       });
339 | 
340 |       const result = await set_sim_locationLogic(
341 |         {
342 |           simulatorId: 'test-uuid-123',
343 |           latitude: 0,
344 |           longitude: 0,
345 |         },
346 |         mockExecutor,
347 |       );
348 | 
349 |       expect(result).toEqual({
350 |         content: [
351 |           {
352 |             type: 'text',
353 |             text: 'Successfully set simulator test-uuid-123 location to 0,0',
354 |           },
355 |         ],
356 |       });
357 |     });
358 | 
359 |     it('should verify correct executor arguments', async () => {
360 |       let capturedArgs: any[] = [];
361 | 
362 |       const mockExecutor = async (...args: any[]) => {
363 |         capturedArgs = args;
364 |         return createMockCommandResponse({
365 |           success: true,
366 |           output: 'Location set successfully',
367 |           error: undefined,
368 |         });
369 |       };
370 | 
371 |       await set_sim_locationLogic(
372 |         {
373 |           simulatorId: 'test-uuid-123',
374 |           latitude: 37.7749,
375 |           longitude: -122.4194,
376 |         },
377 |         mockExecutor,
378 |       );
379 | 
380 |       expect(capturedArgs).toEqual([
381 |         ['xcrun', 'simctl', 'location', 'test-uuid-123', 'set', '37.7749,-122.4194'],
382 |         'Set Simulator Location',
383 |         true,
384 |         {},
385 |       ]);
386 |     });
387 |   });
388 | });
389 | 
```
Page 7/16FirstPrevNextLast