This is page 15 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/ui-testing/__tests__/touch.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Tests for touch tool plugin
3 | * Following CLAUDE.md testing standards with dependency injection
4 | */
5 |
6 | import { describe, it, expect, beforeEach } from 'vitest';
7 | import * as z from 'zod';
8 | import { createMockExecutor, mockProcess } from '../../../../test-utils/mock-executors.ts';
9 | import { sessionStore } from '../../../../utils/session-store.ts';
10 | import touchPlugin, { touchLogic } from '../touch.ts';
11 |
12 | describe('Touch Plugin', () => {
13 | beforeEach(() => {
14 | sessionStore.clear();
15 | });
16 |
17 | describe('Export Field Validation (Literal)', () => {
18 | it('should have correct name', () => {
19 | expect(touchPlugin.name).toBe('touch');
20 | });
21 |
22 | it('should have correct description', () => {
23 | expect(touchPlugin.description).toBe(
24 | "Perform touch down/up events at specific coordinates. Use describe_ui for precise coordinates (don't guess from screenshots).",
25 | );
26 | });
27 |
28 | it('should have handler function', () => {
29 | expect(typeof touchPlugin.handler).toBe('function');
30 | });
31 |
32 | it('should validate schema fields with safeParse', () => {
33 | const schema = z.object(touchPlugin.schema);
34 |
35 | expect(
36 | schema.safeParse({
37 | x: 100,
38 | y: 200,
39 | down: true,
40 | }).success,
41 | ).toBe(true);
42 |
43 | expect(
44 | schema.safeParse({
45 | x: 100,
46 | y: 200,
47 | up: true,
48 | }).success,
49 | ).toBe(true);
50 |
51 | expect(
52 | schema.safeParse({
53 | x: 100.5,
54 | y: 200,
55 | down: true,
56 | }).success,
57 | ).toBe(false);
58 |
59 | expect(
60 | schema.safeParse({
61 | x: 100,
62 | y: 200.5,
63 | down: true,
64 | }).success,
65 | ).toBe(false);
66 |
67 | expect(
68 | schema.safeParse({
69 | x: 100,
70 | y: 200,
71 | down: true,
72 | delay: -1,
73 | }).success,
74 | ).toBe(false);
75 |
76 | const withSimId = schema.safeParse({
77 | simulatorId: '12345678-1234-4234-8234-123456789012',
78 | x: 100,
79 | y: 200,
80 | down: true,
81 | });
82 | expect(withSimId.success).toBe(true);
83 | expect('simulatorId' in (withSimId.data as Record<string, unknown>)).toBe(false);
84 | });
85 | });
86 |
87 | describe('Handler Requirements', () => {
88 | it('should require simulatorId session default', async () => {
89 | const result = await touchPlugin.handler({
90 | x: 100,
91 | y: 200,
92 | down: true,
93 | });
94 |
95 | expect(result.isError).toBe(true);
96 | const message = result.content[0].text;
97 | expect(message).toContain('Missing required session defaults');
98 | expect(message).toContain('simulatorId is required');
99 | expect(message).toContain('session-set-defaults');
100 | });
101 |
102 | it('should surface parameter validation errors when defaults exist', async () => {
103 | sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });
104 |
105 | const result = await touchPlugin.handler({
106 | y: 200,
107 | down: true,
108 | });
109 |
110 | expect(result.isError).toBe(true);
111 | const message = result.content[0].text;
112 | expect(message).toContain('Parameter validation failed');
113 | expect(message).toContain('x: Invalid input: expected number, received undefined');
114 | });
115 | });
116 |
117 | describe('Command Generation', () => {
118 | it('should generate correct axe command for touch down', async () => {
119 | let capturedCommand: string[] = [];
120 | const trackingExecutor = async (command: string[]) => {
121 | capturedCommand = command;
122 | return {
123 | success: true,
124 | output: 'touch completed',
125 | error: undefined,
126 | process: mockProcess,
127 | };
128 | };
129 |
130 | const mockAxeHelpers = {
131 | getAxePath: () => '/usr/local/bin/axe',
132 | getBundledAxeEnvironment: () => ({}),
133 | createAxeNotAvailableResponse: () => ({
134 | content: [
135 | {
136 | type: 'text',
137 | 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.',
138 | },
139 | ],
140 | isError: true,
141 | }),
142 | };
143 |
144 | await touchLogic(
145 | {
146 | simulatorId: '12345678-1234-4234-8234-123456789012',
147 | x: 100,
148 | y: 200,
149 | down: true,
150 | },
151 | trackingExecutor,
152 | mockAxeHelpers,
153 | );
154 |
155 | expect(capturedCommand).toEqual([
156 | '/usr/local/bin/axe',
157 | 'touch',
158 | '-x',
159 | '100',
160 | '-y',
161 | '200',
162 | '--down',
163 | '--udid',
164 | '12345678-1234-4234-8234-123456789012',
165 | ]);
166 | });
167 |
168 | it('should generate correct axe command for touch up', async () => {
169 | let capturedCommand: string[] = [];
170 | const trackingExecutor = async (command: string[]) => {
171 | capturedCommand = command;
172 | return {
173 | success: true,
174 | output: 'touch completed',
175 | error: undefined,
176 | process: mockProcess,
177 | };
178 | };
179 |
180 | const mockAxeHelpers = {
181 | getAxePath: () => '/usr/local/bin/axe',
182 | getBundledAxeEnvironment: () => ({}),
183 | createAxeNotAvailableResponse: () => ({
184 | content: [
185 | {
186 | type: 'text',
187 | 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.',
188 | },
189 | ],
190 | isError: true,
191 | }),
192 | };
193 |
194 | await touchLogic(
195 | {
196 | simulatorId: '12345678-1234-4234-8234-123456789012',
197 | x: 150,
198 | y: 250,
199 | up: true,
200 | },
201 | trackingExecutor,
202 | mockAxeHelpers,
203 | );
204 |
205 | expect(capturedCommand).toEqual([
206 | '/usr/local/bin/axe',
207 | 'touch',
208 | '-x',
209 | '150',
210 | '-y',
211 | '250',
212 | '--up',
213 | '--udid',
214 | '12345678-1234-4234-8234-123456789012',
215 | ]);
216 | });
217 |
218 | it('should generate correct axe command for touch down+up', async () => {
219 | let capturedCommand: string[] = [];
220 | const trackingExecutor = async (command: string[]) => {
221 | capturedCommand = command;
222 | return {
223 | success: true,
224 | output: 'touch completed',
225 | error: undefined,
226 | process: mockProcess,
227 | };
228 | };
229 |
230 | const mockAxeHelpers = {
231 | getAxePath: () => '/usr/local/bin/axe',
232 | getBundledAxeEnvironment: () => ({}),
233 | createAxeNotAvailableResponse: () => ({
234 | content: [
235 | {
236 | type: 'text',
237 | 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.',
238 | },
239 | ],
240 | isError: true,
241 | }),
242 | };
243 |
244 | await touchLogic(
245 | {
246 | simulatorId: '12345678-1234-4234-8234-123456789012',
247 | x: 300,
248 | y: 400,
249 | down: true,
250 | up: true,
251 | },
252 | trackingExecutor,
253 | mockAxeHelpers,
254 | );
255 |
256 | expect(capturedCommand).toEqual([
257 | '/usr/local/bin/axe',
258 | 'touch',
259 | '-x',
260 | '300',
261 | '-y',
262 | '400',
263 | '--down',
264 | '--up',
265 | '--udid',
266 | '12345678-1234-4234-8234-123456789012',
267 | ]);
268 | });
269 |
270 | it('should generate correct axe command for touch with delay', async () => {
271 | let capturedCommand: string[] = [];
272 | const trackingExecutor = async (command: string[]) => {
273 | capturedCommand = command;
274 | return {
275 | success: true,
276 | output: 'touch completed',
277 | error: undefined,
278 | process: mockProcess,
279 | };
280 | };
281 |
282 | const mockAxeHelpers = {
283 | getAxePath: () => '/usr/local/bin/axe',
284 | getBundledAxeEnvironment: () => ({}),
285 | createAxeNotAvailableResponse: () => ({
286 | content: [
287 | {
288 | type: 'text',
289 | 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.',
290 | },
291 | ],
292 | isError: true,
293 | }),
294 | };
295 |
296 | await touchLogic(
297 | {
298 | simulatorId: '12345678-1234-4234-8234-123456789012',
299 | x: 50,
300 | y: 75,
301 | down: true,
302 | up: true,
303 | delay: 1.5,
304 | },
305 | trackingExecutor,
306 | mockAxeHelpers,
307 | );
308 |
309 | expect(capturedCommand).toEqual([
310 | '/usr/local/bin/axe',
311 | 'touch',
312 | '-x',
313 | '50',
314 | '-y',
315 | '75',
316 | '--down',
317 | '--up',
318 | '--delay',
319 | '1.5',
320 | '--udid',
321 | '12345678-1234-4234-8234-123456789012',
322 | ]);
323 | });
324 |
325 | it('should generate correct axe command with bundled axe path', async () => {
326 | let capturedCommand: string[] = [];
327 | const trackingExecutor = async (command: string[]) => {
328 | capturedCommand = command;
329 | return {
330 | success: true,
331 | output: 'touch completed',
332 | error: undefined,
333 | process: mockProcess,
334 | };
335 | };
336 |
337 | const mockAxeHelpers = {
338 | getAxePath: () => '/path/to/bundled/axe',
339 | getBundledAxeEnvironment: () => ({ AXE_PATH: '/some/path' }),
340 | };
341 |
342 | await touchLogic(
343 | {
344 | simulatorId: 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF',
345 | x: 0,
346 | y: 0,
347 | up: true,
348 | delay: 0.5,
349 | },
350 | trackingExecutor,
351 | mockAxeHelpers,
352 | );
353 |
354 | expect(capturedCommand).toEqual([
355 | '/path/to/bundled/axe',
356 | 'touch',
357 | '-x',
358 | '0',
359 | '-y',
360 | '0',
361 | '--up',
362 | '--delay',
363 | '0.5',
364 | '--udid',
365 | 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF',
366 | ]);
367 | });
368 | });
369 |
370 | describe('Handler Behavior (Complete Literal Returns)', () => {
371 | it('should handle axe dependency error', async () => {
372 | const mockExecutor = createMockExecutor({ success: true });
373 | const mockAxeHelpers = {
374 | getAxePath: () => null,
375 | getBundledAxeEnvironment: () => ({}),
376 | createAxeNotAvailableResponse: () => ({
377 | content: [
378 | {
379 | type: 'text',
380 | 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.',
381 | },
382 | ],
383 | isError: true,
384 | }),
385 | };
386 |
387 | const result = await touchLogic(
388 | {
389 | simulatorId: '12345678-1234-4234-8234-123456789012',
390 | x: 100,
391 | y: 200,
392 | down: true,
393 | },
394 | mockExecutor,
395 | mockAxeHelpers,
396 | );
397 |
398 | expect(result).toEqual({
399 | content: [
400 | {
401 | type: 'text',
402 | 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.',
403 | },
404 | ],
405 | isError: true,
406 | });
407 | });
408 |
409 | it('should successfully perform touch down', async () => {
410 | const mockExecutor = createMockExecutor({ success: true, output: 'Touch down completed' });
411 | const mockAxeHelpers = {
412 | getAxePath: () => '/usr/local/bin/axe',
413 | getBundledAxeEnvironment: () => ({}),
414 | createAxeNotAvailableResponse: () => ({
415 | content: [
416 | {
417 | type: 'text',
418 | 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.',
419 | },
420 | ],
421 | isError: true,
422 | }),
423 | };
424 |
425 | const result = await touchLogic(
426 | {
427 | simulatorId: '12345678-1234-4234-8234-123456789012',
428 | x: 100,
429 | y: 200,
430 | down: true,
431 | },
432 | mockExecutor,
433 | mockAxeHelpers,
434 | );
435 |
436 | expect(result).toEqual({
437 | content: [
438 | {
439 | type: 'text',
440 | text: 'Touch event (touch down) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
441 | },
442 | ],
443 | isError: false,
444 | });
445 | });
446 |
447 | it('should successfully perform touch up', async () => {
448 | const mockExecutor = createMockExecutor({ success: true, output: 'Touch up completed' });
449 | const mockAxeHelpers = {
450 | getAxePath: () => '/usr/local/bin/axe',
451 | getBundledAxeEnvironment: () => ({}),
452 | createAxeNotAvailableResponse: () => ({
453 | content: [
454 | {
455 | type: 'text',
456 | 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.',
457 | },
458 | ],
459 | isError: true,
460 | }),
461 | };
462 |
463 | const result = await touchLogic(
464 | {
465 | simulatorId: '12345678-1234-4234-8234-123456789012',
466 | x: 100,
467 | y: 200,
468 | up: true,
469 | },
470 | mockExecutor,
471 | mockAxeHelpers,
472 | );
473 |
474 | expect(result).toEqual({
475 | content: [
476 | {
477 | type: 'text',
478 | text: 'Touch event (touch up) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
479 | },
480 | ],
481 | isError: false,
482 | });
483 | });
484 |
485 | it('should return error when neither down nor up is specified', async () => {
486 | const mockExecutor = createMockExecutor({ success: true });
487 |
488 | const result = await touchLogic(
489 | {
490 | simulatorId: '12345678-1234-4234-8234-123456789012',
491 | x: 100,
492 | y: 200,
493 | },
494 | mockExecutor,
495 | );
496 |
497 | expect(result).toEqual({
498 | content: [{ type: 'text', text: 'Error: At least one of "down" or "up" must be true' }],
499 | isError: true,
500 | });
501 | });
502 |
503 | it('should return success for touch down event', async () => {
504 | const mockExecutor = createMockExecutor({
505 | success: true,
506 | output: 'touch completed',
507 | error: undefined,
508 | });
509 |
510 | const mockAxeHelpers = {
511 | getAxePath: () => '/usr/local/bin/axe',
512 | getBundledAxeEnvironment: () => ({}),
513 | createAxeNotAvailableResponse: () => ({
514 | content: [
515 | {
516 | type: 'text',
517 | 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.',
518 | },
519 | ],
520 | isError: true,
521 | }),
522 | };
523 |
524 | const result = await touchLogic(
525 | {
526 | simulatorId: '12345678-1234-4234-8234-123456789012',
527 | x: 100,
528 | y: 200,
529 | down: true,
530 | },
531 | mockExecutor,
532 | mockAxeHelpers,
533 | );
534 |
535 | expect(result).toEqual({
536 | content: [
537 | {
538 | type: 'text',
539 | text: 'Touch event (touch down) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
540 | },
541 | ],
542 | isError: false,
543 | });
544 | });
545 |
546 | it('should return success for touch up event', async () => {
547 | const mockExecutor = createMockExecutor({
548 | success: true,
549 | output: 'touch completed',
550 | error: undefined,
551 | });
552 |
553 | const mockAxeHelpers = {
554 | getAxePath: () => '/usr/local/bin/axe',
555 | getBundledAxeEnvironment: () => ({}),
556 | createAxeNotAvailableResponse: () => ({
557 | content: [
558 | {
559 | type: 'text',
560 | 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.',
561 | },
562 | ],
563 | isError: true,
564 | }),
565 | };
566 |
567 | const result = await touchLogic(
568 | {
569 | simulatorId: '12345678-1234-4234-8234-123456789012',
570 | x: 100,
571 | y: 200,
572 | up: true,
573 | },
574 | mockExecutor,
575 | mockAxeHelpers,
576 | );
577 |
578 | expect(result).toEqual({
579 | content: [
580 | {
581 | type: 'text',
582 | text: 'Touch event (touch up) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
583 | },
584 | ],
585 | isError: false,
586 | });
587 | });
588 |
589 | it('should return success for touch down+up event', async () => {
590 | const mockExecutor = createMockExecutor({
591 | success: true,
592 | output: 'touch completed',
593 | error: undefined,
594 | });
595 |
596 | const mockAxeHelpers = {
597 | getAxePath: () => '/usr/local/bin/axe',
598 | getBundledAxeEnvironment: () => ({}),
599 | createAxeNotAvailableResponse: () => ({
600 | content: [
601 | {
602 | type: 'text',
603 | 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.',
604 | },
605 | ],
606 | isError: true,
607 | }),
608 | };
609 |
610 | const result = await touchLogic(
611 | {
612 | simulatorId: '12345678-1234-4234-8234-123456789012',
613 | x: 100,
614 | y: 200,
615 | down: true,
616 | up: true,
617 | },
618 | mockExecutor,
619 | mockAxeHelpers,
620 | );
621 |
622 | expect(result).toEqual({
623 | content: [
624 | {
625 | type: 'text',
626 | text: 'Touch event (touch down+up) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
627 | },
628 | ],
629 | isError: false,
630 | });
631 | });
632 |
633 | it('should handle DependencyError when axe is not available', async () => {
634 | const mockExecutor = createMockExecutor({ success: true });
635 |
636 | const mockAxeHelpers = {
637 | getAxePath: () => null,
638 | getBundledAxeEnvironment: () => ({}),
639 | createAxeNotAvailableResponse: () => ({
640 | content: [
641 | {
642 | type: 'text',
643 | 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.',
644 | },
645 | ],
646 | isError: true,
647 | }),
648 | };
649 |
650 | const result = await touchLogic(
651 | {
652 | simulatorId: '12345678-1234-4234-8234-123456789012',
653 | x: 100,
654 | y: 200,
655 | down: true,
656 | },
657 | mockExecutor,
658 | mockAxeHelpers,
659 | );
660 |
661 | expect(result).toEqual({
662 | content: [
663 | {
664 | type: 'text',
665 | 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.',
666 | },
667 | ],
668 | isError: true,
669 | });
670 | });
671 |
672 | it('should handle AxeError from failed command execution', async () => {
673 | const mockExecutor = createMockExecutor({
674 | success: false,
675 | output: '',
676 | error: 'axe command failed',
677 | });
678 |
679 | const mockAxeHelpers = {
680 | getAxePath: () => '/usr/local/bin/axe',
681 | getBundledAxeEnvironment: () => ({}),
682 | createAxeNotAvailableResponse: () => ({
683 | content: [
684 | {
685 | type: 'text',
686 | 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.',
687 | },
688 | ],
689 | isError: true,
690 | }),
691 | };
692 |
693 | const result = await touchLogic(
694 | {
695 | simulatorId: '12345678-1234-4234-8234-123456789012',
696 | x: 100,
697 | y: 200,
698 | down: true,
699 | },
700 | mockExecutor,
701 | mockAxeHelpers,
702 | );
703 |
704 | expect(result).toEqual({
705 | content: [
706 | {
707 | type: 'text',
708 | text: "Error: Failed to execute touch event: axe command 'touch' failed.\nDetails: axe command failed",
709 | },
710 | ],
711 | isError: true,
712 | });
713 | });
714 |
715 | it('should handle SystemError from command execution', async () => {
716 | const mockExecutor = async () => {
717 | throw new Error('System error occurred');
718 | };
719 |
720 | const mockAxeHelpers = {
721 | getAxePath: () => '/usr/local/bin/axe',
722 | getBundledAxeEnvironment: () => ({}),
723 | createAxeNotAvailableResponse: () => ({
724 | content: [
725 | {
726 | type: 'text',
727 | 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.',
728 | },
729 | ],
730 | isError: true,
731 | }),
732 | };
733 |
734 | const result = await touchLogic(
735 | {
736 | simulatorId: '12345678-1234-4234-8234-123456789012',
737 | x: 100,
738 | y: 200,
739 | down: true,
740 | },
741 | mockExecutor,
742 | mockAxeHelpers,
743 | );
744 |
745 | expect(result).toMatchObject({
746 | content: [
747 | {
748 | type: 'text',
749 | text: expect.stringContaining(
750 | 'Error: System error executing axe: Failed to execute axe command: System error occurred',
751 | ),
752 | },
753 | ],
754 | isError: true,
755 | });
756 | });
757 |
758 | it('should handle unexpected Error objects', async () => {
759 | const mockExecutor = async () => {
760 | throw new Error('Unexpected error');
761 | };
762 |
763 | const mockAxeHelpers = {
764 | getAxePath: () => '/usr/local/bin/axe',
765 | getBundledAxeEnvironment: () => ({}),
766 | createAxeNotAvailableResponse: () => ({
767 | content: [
768 | {
769 | type: 'text',
770 | 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.',
771 | },
772 | ],
773 | isError: true,
774 | }),
775 | };
776 |
777 | const result = await touchLogic(
778 | {
779 | simulatorId: '12345678-1234-4234-8234-123456789012',
780 | x: 100,
781 | y: 200,
782 | down: true,
783 | },
784 | mockExecutor,
785 | mockAxeHelpers,
786 | );
787 |
788 | expect(result).toMatchObject({
789 | content: [
790 | {
791 | type: 'text',
792 | text: expect.stringContaining(
793 | 'Error: System error executing axe: Failed to execute axe command: Unexpected error',
794 | ),
795 | },
796 | ],
797 | isError: true,
798 | });
799 | });
800 |
801 | it('should handle unexpected string errors', async () => {
802 | const mockExecutor = async () => {
803 | throw 'String error';
804 | };
805 |
806 | const mockAxeHelpers = {
807 | getAxePath: () => '/usr/local/bin/axe',
808 | getBundledAxeEnvironment: () => ({}),
809 | createAxeNotAvailableResponse: () => ({
810 | content: [
811 | {
812 | type: 'text',
813 | 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.',
814 | },
815 | ],
816 | isError: true,
817 | }),
818 | };
819 |
820 | const result = await touchLogic(
821 | {
822 | simulatorId: '12345678-1234-4234-8234-123456789012',
823 | x: 100,
824 | y: 200,
825 | down: true,
826 | },
827 | mockExecutor,
828 | mockAxeHelpers,
829 | );
830 |
831 | expect(result).toEqual({
832 | content: [
833 | {
834 | type: 'text',
835 | text: 'Error: System error executing axe: Failed to execute axe command: String error',
836 | },
837 | ],
838 | isError: true,
839 | });
840 | });
841 | });
842 | });
843 |
```
--------------------------------------------------------------------------------
/docs/dev/PLUGIN_DEVELOPMENT.md:
--------------------------------------------------------------------------------
```markdown
1 | # XcodeBuildMCP Plugin Development Guide
2 |
3 | This guide provides comprehensive instructions for creating new tools and workflow groups in XcodeBuildMCP using the filesystem-based auto-discovery system.
4 |
5 | ## Table of Contents
6 |
7 | 1. [Overview](#overview)
8 | 2. [Plugin Architecture](#plugin-architecture)
9 | 3. [Creating New Tools](#creating-new-tools)
10 | 4. [Creating New Workflow Groups](#creating-new-workflow-groups)
11 | 5. [Creating MCP Resources](#creating-mcp-resources)
12 | 6. [Auto-Discovery System](#auto-discovery-system)
13 | 7. [Testing Guidelines](#testing-guidelines)
14 | 8. [Development Workflow](#development-workflow)
15 | 9. [Best Practices](#best-practices)
16 |
17 | ## Overview
18 |
19 | XcodeBuildMCP uses a **plugin-based architecture** with **filesystem-based auto-discovery**. Tools are automatically discovered and loaded without manual registration, and can be selectively enabled using `XCODEBUILDMCP_ENABLED_WORKFLOWS`.
20 |
21 | ### Key Features
22 |
23 | - **Auto-Discovery**: Tools are automatically found by scanning `src/mcp/tools/` directory
24 | - **Selective Workflow Loading**: Limit startup tool registration with `XCODEBUILDMCP_ENABLED_WORKFLOWS`
25 | - **Dependency Injection**: All tools use testable patterns with mock-friendly executors
26 | - **Workflow Organization**: Tools are grouped into end-to-end development workflows
27 |
28 | ## Plugin Architecture
29 |
30 | ### Directory Structure
31 |
32 | ```
33 | src/mcp/tools/
34 | ├── simulator-workspace/ # iOS Simulator + Workspace tools
35 | ├── simulator-project/ # iOS Simulator + Project tools (re-exports)
36 | ├── simulator-shared/ # Shared simulator tools (canonical)
37 | ├── device-workspace/ # iOS Device + Workspace tools
38 | ├── device-project/ # iOS Device + Project tools (re-exports)
39 | ├── device-shared/ # Shared device tools (canonical)
40 | ├── macos-workspace/ # macOS + Workspace tools
41 | ├── macos-project/ # macOS + Project tools (re-exports)
42 | ├── macos-shared/ # Shared macOS tools (canonical)
43 | ├── swift-package/ # Swift Package Manager tools
44 | ├── ui-testing/ # UI automation tools
45 | ├── project-discovery/ # Project analysis tools
46 | ├── utilities/ # General utilities
47 | ├── doctor/ # System health check tools
48 | └── logging/ # Log capture tools
49 | ```
50 |
51 | ### Plugin Tool Types
52 |
53 | 1. **Canonical Workflows**: Standalone workflow groups (e.g., `swift-package`, `ui-testing`) defined as folders in the `src/mcp/tools/` directory
54 | 2. **Shared Tools**: Common tools in `*-shared` directories (not exposed to clients)
55 | 3. **Re-exported Tools**: Share tools to other workflow groups by re-exporting them
56 |
57 | ## Creating New Tools
58 |
59 | ### 1. Tool File Structure
60 |
61 | Every tool follows this standardized pattern:
62 |
63 | ```typescript
64 | // src/mcp/tools/my-workflow/my_tool.ts
65 | import { z } from 'zod';
66 | import { ToolResponse } from '../../../types/common.js';
67 | import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/command.js';
68 | import { log, validateRequiredParam, createTextResponse, createErrorResponse } from '../../../utils/index.js';
69 |
70 | // 1. Define parameters type for clarity
71 | type MyToolParams = {
72 | requiredParam: string;
73 | optionalParam?: string;
74 | };
75 |
76 | // 2. Implement the core logic in a separate, testable function
77 | export async function my_toolLogic(
78 | params: MyToolParams,
79 | executor: CommandExecutor,
80 | ): Promise<ToolResponse> {
81 | // 3. Validate required parameters
82 | const requiredValidation = validateRequiredParam('requiredParam', params.requiredParam);
83 | if (!requiredValidation.isValid) {
84 | return requiredValidation.errorResponse;
85 | }
86 |
87 | log('info', `Executing my_tool with param: ${params.requiredParam}`);
88 |
89 | try {
90 | // 4. Build and execute the command using the injected executor
91 | const command = ['my-command', '--param', params.requiredParam];
92 | if (params.optionalParam) {
93 | command.push('--optional', params.optionalParam);
94 | }
95 |
96 | const result = await executor(command, 'My Tool Operation');
97 |
98 | if (!result.success) {
99 | return createErrorResponse('My Tool operation failed', result.error);
100 | }
101 |
102 | return createTextResponse(`✅ Success: ${result.output}`);
103 | } catch (error) {
104 | const errorMessage = error instanceof Error ? error.message : String(error);
105 | log('error', `My Tool execution error: ${errorMessage}`);
106 | return createErrorResponse('Tool execution failed', errorMessage);
107 | }
108 | }
109 |
110 | // 5. Export the tool definition as the default export
111 | export default {
112 | name: 'my_tool',
113 | description: 'A brief description of what my_tool does, with a usage example. e.g. my_tool({ requiredParam: "value" })',
114 | schema: {
115 | requiredParam: z.string().describe('Description of the required parameter.'),
116 | optionalParam: z.string().optional().describe('Description of the optional parameter.'),
117 | },
118 | // The handler wraps the logic function with the default executor for production use
119 | handler: async (args: Record<string, unknown>): Promise<ToolResponse> => {
120 | return my_toolLogic(args as MyToolParams, getDefaultCommandExecutor());
121 | },
122 | };
123 | ```
124 |
125 | ### 2. Required Tool Plugin Properties
126 |
127 | Every tool plugin **must** export a default object with these properties:
128 |
129 | | Property | Type | Description |
130 | |----------|------|-------------|
131 | | `name` | `string` | Tool name (must match filename without extension) |
132 | | `description` | `string` | Clear description with usage examples |
133 | | `schema` | `Record<string, z.ZodTypeAny>` | Zod validation schema for parameters |
134 | | `handler` | `function` | Async function: `(args) => Promise<ToolResponse>` |
135 |
136 | ### 3. Naming Conventions
137 |
138 | Tools follow the pattern: `{action}_{target}_{specifier}_{projectType}`
139 |
140 | **Examples:**
141 | - `build_sim_id_ws` → Build + Simulator + ID + Workspace
142 | - `build_sim_name_proj` → Build + Simulator + Name + Project
143 | - `test_device_ws` → Test + Device + Workspace
144 | - `swift_package_build` → Swift Package + Build
145 |
146 | **Project Type Suffixes:**
147 | - `_ws` → Works with `.xcworkspace` files
148 | - `_proj` → Works with `.xcodeproj` files
149 | - No suffix → Generic or canonical tools
150 |
151 | ### 4. Parameter Validation Patterns
152 |
153 | Use utility functions for consistent validation:
154 |
155 | ```typescript
156 | // Required parameter validation
157 | const pathValidation = validateRequiredParam('workspacePath', params.workspacePath);
158 | if (!pathValidation.isValid) return pathValidation.errorResponse;
159 |
160 | // At-least-one parameter validation
161 | const identifierValidation = validateAtLeastOneParam(
162 | 'simulatorId', params.simulatorId,
163 | 'simulatorName', params.simulatorName
164 | );
165 | if (!identifierValidation.isValid) return identifierValidation.errorResponse;
166 |
167 | // File existence validation
168 | const fileValidation = validateFileExists(params.workspacePath as string);
169 | if (!fileValidation.isValid) return fileValidation.errorResponse;
170 | ```
171 |
172 | ### 5. Response Patterns
173 |
174 | Use utility functions for consistent responses:
175 |
176 | ```typescript
177 | // Success responses
178 | return createTextResponse('✅ Operation succeeded');
179 | return createTextResponse('Operation completed', false); // Not an error
180 |
181 | // Error responses
182 | return createErrorResponse('Operation failed', errorDetails);
183 | return createErrorResponse('Validation failed', errorMessage, 'ValidationError');
184 |
185 | // Complex responses
186 | return {
187 | content: [
188 | { type: 'text', text: '✅ Build succeeded' },
189 | { type: 'text', text: 'Next steps: Run install_app_sim...' }
190 | ],
191 | isError: false
192 | };
193 | ```
194 |
195 | ## Creating New Workflow Groups
196 |
197 | ### 1. Workflow Group Structure
198 |
199 | Each workflow group requires:
200 |
201 | 1. **Directory**: Following naming convention
202 | 2. **Workflow Metadata**: `index.ts` file with workflow export
203 | 3. **Tool Files**: Individual tool implementations
204 | 4. **Tests**: Comprehensive test coverage
205 |
206 | ### 2. Directory Naming Convention
207 |
208 | ```
209 | [platform]-[projectType]/ # e.g., simulator-workspace, device-project
210 | [platform]-shared/ # e.g., simulator-shared, macos-shared
211 | [workflow-name]/ # e.g., swift-package, ui-testing
212 | ```
213 |
214 | ### 3. Workflow Metadata (index.ts)
215 |
216 | **Required for all workflow groups:**
217 |
218 | ```typescript
219 | // Example: src/mcp/tools/simulator-workspace/index.ts
220 | export const workflow = {
221 | name: 'iOS Simulator Workspace Development',
222 | description: 'Complete iOS development workflow for .xcworkspace files including build, test, deploy, and debug capabilities',
223 | };
224 | ```
225 |
226 | **Required Properties:**
227 | - `name`: Human-readable workflow name
228 | - `description`: Clear description of workflow purpose
229 |
230 | ### 4. Tool Organization Patterns
231 |
232 | #### Canonical Workflow Groups
233 | Self-contained workflows that don't re-export from other groups:
234 |
235 | ```
236 | swift-package/
237 | ├── index.ts # Workflow metadata
238 | ├── swift_package_build.ts # Build tool
239 | ├── swift_package_test.ts # Test tool
240 | ├── swift_package_run.ts # Run tool
241 | └── __tests__/ # Test directory
242 | ├── index.test.ts # Workflow tests
243 | ├── swift_package_build.test.ts
244 | └── ...
245 | ```
246 |
247 | #### Shared Workflow Groups
248 | Provide canonical tools for re-export by project/workspace variants:
249 |
250 | ```
251 | simulator-shared/
252 | ├── boot_sim.ts # Canonical simulator boot tool
253 | ├── install_app_sim.ts # Canonical app install tool
254 | └── __tests__/ # Test directory
255 | ├── boot_sim.test.ts
256 | └── ...
257 | ```
258 |
259 | #### Project/Workspace Workflow Groups
260 | Re-export shared tools and add variant-specific tools:
261 |
262 | ```
263 | simulator-project/
264 | ├── index.ts # Workflow metadata
265 | ├── boot_sim.ts # Re-export: export { default } from '../simulator-shared/boot_sim.js';
266 | ├── build_sim_id_proj.ts # Project-specific build tool
267 | └── __tests__/ # Test directory
268 | ├── index.test.ts # Workflow tests
269 | ├── re-exports.test.ts # Re-export validation
270 | └── ...
271 | ```
272 |
273 | ### 5. Re-export Implementation
274 |
275 | For project/workspace groups that share tools:
276 |
277 | ```typescript
278 | // simulator-project/boot_sim.ts
279 | export { default } from '../simulator-shared/boot_sim.js';
280 | ```
281 |
282 | **Re-export Rules:**
283 | 1. Re-exports come from canonical `-shared` groups
284 | 2. No chained re-exports (re-exports from re-exports)
285 | 3. Each tool maintains project or workspace specificity
286 | 4. Implementation shared, interfaces remain unique
287 |
288 | ## Creating MCP Resources
289 |
290 | MCP Resources provide efficient URI-based data access for clients that support the MCP resource specification
291 |
292 | ### 1. Resource Structure
293 |
294 | Resources are located in `src/resources/` and follow this pattern:
295 |
296 | ```typescript
297 | // src/resources/example.ts
298 | import { log, getDefaultCommandExecutor, CommandExecutor } from '../../utils/index.js';
299 |
300 | // Testable resource logic separated from MCP handler
301 | export async function exampleResourceLogic(
302 | executor: CommandExecutor,
303 | ): Promise<{ contents: Array<{ text: string }> }> {
304 | try {
305 | log('info', 'Processing example resource request');
306 |
307 | // Use the executor to get data
308 | const result = await executor(['some', 'command'], 'Example Resource Operation');
309 |
310 | if (!result.success) {
311 | throw new Error(result.error || 'Failed to get resource data');
312 | }
313 |
314 | return {
315 | contents: [{ text: result.output || 'resource data' }]
316 | };
317 | } catch (error) {
318 | const errorMessage = error instanceof Error ? error.message : String(error);
319 | log('error', `Error in example resource handler: ${errorMessage}`);
320 |
321 | return {
322 | contents: [
323 | {
324 | text: `Error retrieving resource data: ${errorMessage}`,
325 | },
326 | ],
327 | };
328 | }
329 | }
330 |
331 | export default {
332 | uri: 'xcodebuildmcp://example',
333 | name: 'example',
334 | description: 'Description of the resource data',
335 | mimeType: 'text/plain',
336 | async handler(_uri: URL): Promise<{ contents: Array<{ text: string }> }> {
337 | return exampleResourceLogic(getDefaultCommandExecutor());
338 | },
339 | };
340 | ```
341 |
342 | ### 2. Resource Implementation Guidelines
343 |
344 | **Reuse Existing Logic**: Resources that mirror tools should reuse existing tool logic for consistency:
345 |
346 | ```typescript
347 | // src/mcp/resources/simulators.ts (simplified example)
348 | import { list_simsLogic } from '../tools/simulator-shared/list_sims.js';
349 |
350 | export default {
351 | uri: 'xcodebuildmcp://simulators',
352 | name: 'simulators'
353 | description: 'Available iOS simulators with UUIDs and states',
354 | mimeType: 'text/plain',
355 | async handler(uri: URL): Promise<{ contents: Array<{ text: string }> }> {
356 | const executor = getDefaultCommandExecutor();
357 | const result = await list_simsLogic({}, executor);
358 | return {
359 | contents: [{ text: result.content[0].text }]
360 | };
361 | }
362 | };
363 | ```
364 |
365 | As not all clients support resources it important that resource content that would be ideally be served by resources be mirroed as a tool as well. This ensurew clients that don't support this capability continue to will still have access to that resource data via a simple tool call.
366 |
367 | ### 3. Resource Testing
368 |
369 | Create tests in `src/mcp/resources/__tests__/`:
370 |
371 | ```typescript
372 | // src/mcp/resources/__tests__/example.test.ts
373 | import exampleResource, { exampleResourceLogic } from '../example.js';
374 | import { createMockExecutor } from '../../utils/test-common.js';
375 |
376 | describe('example resource', () => {
377 | describe('Export Field Validation', () => {
378 | it('should export correct uri', () => {
379 | expect(exampleResource.uri).toBe('xcodebuildmcp://example');
380 | });
381 |
382 | it('should export correct description', () => {
383 | expect(exampleResource.description).toBe('Description of the resource data');
384 | });
385 |
386 | it('should export correct mimeType', () => {
387 | expect(exampleResource.mimeType).toBe('text/plain');
388 | });
389 |
390 | it('should export handler function', () => {
391 | expect(typeof exampleResource.handler).toBe('function');
392 | });
393 | });
394 |
395 | describe('Resource Logic Functionality', () => {
396 | it('should return resource data successfully', async () => {
397 | const mockExecutor = createMockExecutor({
398 | success: true,
399 | output: 'test data'
400 | });
401 |
402 | // Test the logic function directly, not the handler
403 | const result = await exampleResourceLogic(mockExecutor);
404 |
405 | expect(result.contents).toHaveLength(1);
406 | expect(result.contents[0].text).toContain('expected data');
407 | });
408 |
409 | it('should handle command execution errors', async () => {
410 | const mockExecutor = createMockExecutor({
411 | success: false,
412 | error: 'Command failed'
413 | });
414 |
415 | const result = await exampleResourceLogic(mockExecutor);
416 |
417 | expect(result.contents[0].text).toContain('Error retrieving');
418 | });
419 | });
420 | });
421 | ```
422 |
423 | ### 4. Auto-Discovery
424 |
425 | Resources are automatically discovered and loaded by the build system. After creating a resource:
426 |
427 | 1. Run `npm run build` to regenerate resource loaders
428 | 2. The resource will be available at its URI for supported clients
429 |
430 | ## Auto-Discovery System
431 |
432 | ### How Auto-Discovery Works
433 |
434 | 1. **Filesystem Scan**: `loadPlugins()` scans `src/mcp/tools/` directory
435 | 2. **Workflow Loading**: Each subdirectory is treated as a potential workflow group
436 | 3. **Metadata Validation**: `index.ts` files provide workflow metadata
437 | 4. **Tool Discovery**: All `.ts` files (except tests and index) are loaded as tools
438 | 5. **Registration**: Tools are automatically registered with the MCP server
439 |
440 | ### Discovery Process
441 |
442 | ```typescript
443 | // Simplified discovery flow
444 | const plugins = await loadPlugins();
445 | for (const plugin of plugins.values()) {
446 | server.tool(plugin.name, plugin.description, plugin.schema, plugin.handler);
447 | }
448 | ```
449 |
450 | ### Selective Workflow Loading
451 |
452 | To limit which workflows are registered at startup, set `XCODEBUILDMCP_ENABLED_WORKFLOWS` to a comma-separated list of workflow directory names. The `session-management` workflow is always auto-included since other tools depend on it.
453 |
454 | Example:
455 | ```bash
456 | XCODEBUILDMCP_ENABLED_WORKFLOWS=simulator,device,project-discovery
457 | ```
458 |
459 | `XCODEBUILDMCP_DEBUG=true` can still be used to increase logging verbosity.
460 |
461 | ## Testing Guidelines
462 |
463 | ### Test Organization
464 |
465 | ```
466 | __tests__/
467 | ├── index.test.ts # Workflow metadata tests (canonical groups only)
468 | ├── re-exports.test.ts # Re-export validation (project/workspace groups)
469 | └── tool_name.test.ts # Individual tool tests
470 | ```
471 |
472 | ### Dependency Injection Testing
473 |
474 | **✅ CORRECT Pattern:**
475 | ```typescript
476 | import { createMockExecutor } from '../../../utils/test-common.js';
477 |
478 | describe('build_sim_name_ws', () => {
479 | it('should build successfully', async () => {
480 | const mockExecutor = createMockExecutor({
481 | success: true,
482 | output: 'BUILD SUCCEEDED'
483 | });
484 |
485 | const result = await build_sim_name_wsLogic(params, mockExecutor);
486 | expect(result.isError).toBe(false);
487 | });
488 | });
489 | ```
490 |
491 | **❌ FORBIDDEN Pattern (Vitest Mocking Banned):**
492 | ```typescript
493 | // ❌ ALL VITEST MOCKING IS COMPLETELY BANNED
494 | vi.mock('child_process');
495 | const mockSpawn = vi.fn();
496 | ```
497 |
498 | ### Three-Dimensional Testing
499 |
500 | Every tool test must cover:
501 |
502 | 1. **Input Validation**: Parameter schema validation and error cases
503 | 2. **Command Generation**: Verify correct CLI commands are built
504 | 3. **Output Processing**: Test response formatting and error handling
505 |
506 | ### Test Template
507 |
508 | ```typescript
509 | import { describe, it, expect } from 'vitest';
510 | import { createMockExecutor } from '../../../utils/test-common.js';
511 | import tool, { toolNameLogic } from '../tool_name.js';
512 |
513 | describe('tool_name', () => {
514 | describe('Export Validation', () => {
515 | it('should export correct name', () => {
516 | expect(tool.name).toBe('tool_name');
517 | });
518 |
519 | it('should export correct description', () => {
520 | expect(tool.description).toContain('Expected description');
521 | });
522 |
523 | it('should export handler function', () => {
524 | expect(typeof tool.handler).toBe('function');
525 | });
526 | });
527 |
528 | describe('Parameter Validation', () => {
529 | it('should validate required parameters', async () => {
530 | const mockExecutor = createMockExecutor({ success: true, output: '' });
531 |
532 | const result = await toolNameLogic({}, mockExecutor);
533 |
534 | expect(result.isError).toBe(true);
535 | expect(result.content[0].text).toContain("Required parameter");
536 | });
537 | });
538 |
539 | describe('Command Generation', () => {
540 | it('should generate correct command', async () => {
541 | const mockExecutor = createMockExecutor({ success: true, output: 'SUCCESS' });
542 |
543 | await toolNameLogic({ param: 'value' }, mockExecutor);
544 |
545 | expect(mockExecutor).toHaveBeenCalledWith(
546 | expect.arrayContaining(['expected', 'command']),
547 | expect.any(String),
548 | expect.any(Boolean)
549 | );
550 | });
551 | });
552 |
553 | describe('Response Processing', () => {
554 | it('should handle successful execution', async () => {
555 | const mockExecutor = createMockExecutor({ success: true, output: 'SUCCESS' });
556 |
557 | const result = await toolNameLogic({ param: 'value' }, mockExecutor);
558 |
559 | expect(result.isError).toBe(false);
560 | expect(result.content[0].text).toContain('✅');
561 | });
562 |
563 | it('should handle execution errors', async () => {
564 | const mockExecutor = createMockExecutor({ success: false, error: 'Command failed' });
565 |
566 | const result = await toolNameLogic({ param: 'value' }, mockExecutor);
567 |
568 | expect(result.isError).toBe(true);
569 | expect(result.content[0].text).toContain('Command failed');
570 | });
571 | });
572 | });
573 | ```
574 |
575 | ## Development Workflow
576 |
577 | ### Adding a New Tool
578 |
579 | 1. **Choose Directory**: Select appropriate workflow group or create new one
580 | 2. **Create Tool File**: Follow naming convention and structure
581 | 3. **Implement Logic**: Use dependency injection pattern
582 | 4. **Define Schema**: Add comprehensive Zod validation
583 | 5. **Write Tests**: Cover all three dimensions
584 | 6. **Test Integration**: Build and verify auto-discovery
585 |
586 | ### Step-by-Step Tool Creation
587 |
588 | ```bash
589 | # 1. Create tool file
590 | touch src/mcp/tools/simulator-workspace/my_new_tool_ws.ts
591 |
592 | # 2. Implement tool following patterns above
593 |
594 | # 3. Create test file
595 | touch src/mcp/tools/simulator-workspace/__tests__/my_new_tool_ws.test.ts
596 |
597 | # 4. Build project
598 | npm run build
599 |
600 | # 5. Verify tool is discovered (should appear in tools list)
601 | npm run inspect # Use MCP Inspector to verify
602 | ```
603 |
604 | ### Adding a New Workflow Group
605 |
606 | 1. **Create Directory**: Follow naming convention
607 | 2. **Add Workflow Metadata**: Create `index.ts` with workflow export
608 | 3. **Implement Tools**: Add tool files following patterns
609 | 4. **Create Tests**: Add comprehensive test coverage
610 | 5. **Verify Discovery**: Test auto-discovery and tool registration
611 |
612 | ### Step-by-Step Workflow Creation
613 |
614 | ```bash
615 | # 1. Create workflow directory
616 | mkdir src/mcp/tools/my-new-workflow
617 |
618 | # 2. Create workflow metadata
619 | cat > src/mcp/tools/my-new-workflow/index.ts << 'EOF'
620 | export const workflow = {
621 | name: 'My New Workflow',
622 | description: 'Description of workflow capabilities',
623 | };
624 | EOF
625 |
626 | # 3. Create tools directory and test directory
627 | mkdir src/mcp/tools/my-new-workflow/__tests__
628 |
629 | # 4. Implement tools following patterns
630 |
631 | # 5. Build and verify
632 | npm run build
633 | npm run inspect
634 | ```
635 |
636 | ## Best Practices
637 |
638 | ### Tool Design
639 |
640 | 1. **Single Responsibility**: Each tool should have one clear purpose
641 | 2. **Descriptive Names**: Follow naming conventions for discoverability
642 | 3. **Clear Descriptions**: Include usage examples in tool descriptions
643 | 4. **Comprehensive Validation**: Validate all parameters with helpful error messages
644 | 5. **Consistent Responses**: Use utility functions for response formatting
645 |
646 | ### Error Handling
647 |
648 | 1. **Graceful Failures**: Always return ToolResponse, never throw from handlers
649 | 2. **Descriptive Errors**: Provide actionable error messages
650 | 3. **Error Types**: Use appropriate error types for different scenarios
651 | 4. **Logging**: Log important events and errors for debugging
652 |
653 | ### Testing
654 |
655 | 1. **Dependency Injection**: Always test with mock executors
656 | 2. **Complete Coverage**: Test all input, command, and output scenarios
657 | 3. **Literal Assertions**: Use exact string expectations to catch changes
658 | 4. **Fast Execution**: Tests should complete quickly without real system calls
659 |
660 | ### Workflow Organization
661 |
662 | 1. **End-to-End Workflows**: Groups should provide complete functionality
663 | 2. **Logical Grouping**: Group related tools together
664 | 3. **Clear Capabilities**: Document what each workflow can accomplish
665 | 4. **Consistent Patterns**: Follow established patterns for maintainability
666 |
667 | ### Workflow Metadata Considerations
668 |
669 | 1. **Workflow Completeness**: Each group should be self-sufficient
670 | 2. **Clear Descriptions**: Keep the `description` concise and user-focused
671 |
672 | ## Updating TOOLS.md Documentation
673 |
674 | ### Critical Documentation Maintenance
675 |
676 | **Every time you add, change, move, edit, or delete a tool, you MUST review and update the [TOOLS.md](../TOOLS.md) file to reflect the current state of the codebase.**
677 |
678 | ### Documentation Update Process
679 |
680 | #### 1. Use Tree CLI for Accurate Discovery
681 |
682 | **Always use the `tree` command to get the actual filesystem representation of tools:**
683 |
684 | ```bash
685 | # Get the definitive source of truth for all workflow groups and tools
686 | tree src/mcp/tools/ -I "__tests__" -I "*.test.ts"
687 | ```
688 |
689 | This command:
690 | - Shows ALL workflow directories and their tools
691 | - Excludes test files (`__tests__` directories and `*.test.ts` files)
692 | - Provides the actual proof of what exists in the codebase
693 | - Gives an accurate count of tools per workflow group
694 |
695 | #### 2. Ignore Shared Groups in Documentation
696 |
697 | When updating [TOOLS.md](../TOOLS.md):
698 |
699 | - **Ignore `*-shared` directories** (e.g., `simulator-shared`, `device-shared`, `macos-shared`)
700 | - These are implementation details, not user-facing workflow groups
701 | - Only document the main workflow groups that users interact with
702 | - The group count should exclude shared groups
703 |
704 | #### 3. List Actual Tool Names
705 |
706 | Instead of using generic descriptions like "Additional Tools: Simulator management, logging, UI testing tools":
707 |
708 | **❌ Wrong:**
709 | ```markdown
710 | - **Additional Tools**: Simulator management, logging, UI testing tools
711 | ```
712 |
713 | **✅ Correct:**
714 | ```markdown
715 | - `boot_sim`, `install_app_sim`, `launch_app_sim`, `list_sims`, `open_sim`
716 | - `describe_ui`, `screenshot`, `start_sim_log_cap`, `stop_sim_log_cap`
717 | ```
718 |
719 | #### 4. Systematic Documentation Update Steps
720 |
721 | 1. **Run the tree command** to get current filesystem state
722 | 2. **Identify all non-shared workflow directories**
723 | 3. **Count actual tool files** in each directory (exclude `index.ts` and test files)
724 | 4. **List all tool names** explicitly in the documentation
725 | 5. **Update tool counts** to reflect actual numbers
726 | 6. **Verify consistency** between filesystem and documentation
727 |
728 | #### 5. Documentation Formatting Requirements
729 |
730 | **Format: One Tool Per Bullet Point with Description**
731 |
732 | Each tool must be listed individually with its actual description from the tool file:
733 |
734 | ```markdown
735 | ### 1. My Awesome Workflow (`my-awesome-workflow`)
736 | **Purpose**: A short description of what this workflow is for. (2 tools)
737 | - `my_tool_one` - Description for my_tool_one from its definition file.
738 | - `my_tool_two` - Description for my_tool_two from its definition file.
739 | ```
740 |
741 | **Description Sources:**
742 | - Use the actual `description` field from each tool's TypeScript file
743 | - Descriptions should be concise but informative for end users
744 | - Include platform/context information (iOS, macOS, simulator, device, etc.)
745 | - Mention required parameters when critical for usage
746 |
747 | #### 6. Validation Checklist
748 |
749 | After updating [TOOLS.md](../TOOLS.md):
750 |
751 | - [ ] Tool counts match actual filesystem counts (from tree command)
752 | - [ ] Each tool has its own bullet point (one tool per line)
753 | - [ ] Each tool includes its actual description from the tool file
754 | - [ ] No generic descriptions like "Additional Tools: X, Y, Z"
755 | - [ ] Descriptions are user-friendly and informative
756 | - [ ] Shared groups (`*-shared`) are not included in main workflow list
757 | - [ ] Workflow group count reflects only user-facing groups (15 groups)
758 | - [ ] Tree command output was used as source of truth
759 | - [ ] Documentation is user-focused, not implementation-focused
760 | - [ ] Tool names are in alphabetical order within each workflow group
761 |
762 | ### Why This Process Matters
763 |
764 | 1. **Accuracy**: Tree command provides definitive proof of current state
765 | 2. **Maintainability**: Systematic process prevents documentation drift
766 | 3. **User Experience**: Accurate documentation helps users understand available tools
767 | 4. **Development Confidence**: Developers can trust the documentation reflects reality
768 |
769 | **Remember**: The filesystem is the source of truth. Documentation must always reflect the actual codebase structure, and the tree command is the most reliable way to ensure accuracy.
770 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/ui-testing/__tests__/tap.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Tests for tap plugin
3 | */
4 |
5 | import { describe, it, expect, beforeEach } from 'vitest';
6 | import * as z from 'zod';
7 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
8 | import { sessionStore } from '../../../../utils/session-store.ts';
9 |
10 | import tapPlugin, { AxeHelpers, tapLogic } from '../tap.ts';
11 |
12 | // Helper function to create mock axe helpers
13 | function createMockAxeHelpers(): AxeHelpers {
14 | return {
15 | getAxePath: () => '/mocked/axe/path',
16 | getBundledAxeEnvironment: () => ({ SOME_ENV: 'value' }),
17 | createAxeNotAvailableResponse: () => ({
18 | content: [
19 | {
20 | type: 'text',
21 | 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.',
22 | },
23 | ],
24 | isError: true,
25 | }),
26 | };
27 | }
28 |
29 | // Helper function to create mock axe helpers with null path (for dependency error tests)
30 | function createMockAxeHelpersWithNullPath(): AxeHelpers {
31 | return {
32 | getAxePath: () => null,
33 | getBundledAxeEnvironment: () => ({ SOME_ENV: 'value' }),
34 | createAxeNotAvailableResponse: () => ({
35 | content: [
36 | {
37 | type: 'text',
38 | 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.',
39 | },
40 | ],
41 | isError: true,
42 | }),
43 | };
44 | }
45 |
46 | describe('Tap Plugin', () => {
47 | beforeEach(() => {
48 | sessionStore.clear();
49 | });
50 |
51 | describe('Export Field Validation (Literal)', () => {
52 | it('should have correct name', () => {
53 | expect(tapPlugin.name).toBe('tap');
54 | });
55 |
56 | it('should have correct description', () => {
57 | expect(tapPlugin.description).toBe(
58 | "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.",
59 | );
60 | });
61 |
62 | it('should have handler function', () => {
63 | expect(typeof tapPlugin.handler).toBe('function');
64 | });
65 |
66 | it('should validate schema fields with safeParse', () => {
67 | const schema = z.object(tapPlugin.schema);
68 |
69 | expect(schema.safeParse({ x: 100, y: 200 }).success).toBe(true);
70 |
71 | expect(schema.safeParse({ id: 'loginButton' }).success).toBe(true);
72 |
73 | expect(schema.safeParse({ label: 'Log in' }).success).toBe(true);
74 |
75 | expect(schema.safeParse({ x: 100, y: 200, id: 'loginButton' }).success).toBe(true);
76 |
77 | expect(schema.safeParse({ x: 100, y: 200, id: 'loginButton', label: 'Log in' }).success).toBe(
78 | true,
79 | );
80 |
81 | expect(
82 | schema.safeParse({
83 | x: 100,
84 | y: 200,
85 | preDelay: 0.5,
86 | postDelay: 1,
87 | }).success,
88 | ).toBe(true);
89 |
90 | expect(
91 | schema.safeParse({
92 | x: 3.14,
93 | y: 200,
94 | }).success,
95 | ).toBe(false);
96 |
97 | expect(
98 | schema.safeParse({
99 | x: 100,
100 | y: 3.14,
101 | }).success,
102 | ).toBe(false);
103 |
104 | expect(
105 | schema.safeParse({
106 | x: 100,
107 | y: 200,
108 | preDelay: -1,
109 | }).success,
110 | ).toBe(false);
111 |
112 | expect(
113 | schema.safeParse({
114 | x: 100,
115 | y: 200,
116 | postDelay: -1,
117 | }).success,
118 | ).toBe(false);
119 |
120 | const withSimId = schema.safeParse({
121 | simulatorId: '12345678-1234-4234-8234-123456789012',
122 | x: 100,
123 | y: 200,
124 | });
125 | expect(withSimId.success).toBe(true);
126 | expect('simulatorId' in (withSimId.data as Record<string, unknown>)).toBe(false);
127 | });
128 | });
129 |
130 | describe('Command Generation', () => {
131 | let callHistory: Array<{
132 | command: string[];
133 | logPrefix?: string;
134 | useShell?: boolean;
135 | opts?: { env?: Record<string, string>; cwd?: string };
136 | }>;
137 |
138 | beforeEach(() => {
139 | callHistory = [];
140 | });
141 |
142 | it('should generate correct axe command with minimal parameters', async () => {
143 | const mockExecutor = createMockExecutor({
144 | success: true,
145 | output: 'Tap completed',
146 | });
147 |
148 | const wrappedExecutor = async (
149 | command: string[],
150 | logPrefix?: string,
151 | useShell?: boolean,
152 | opts?: { env?: Record<string, string>; cwd?: string },
153 | ) => {
154 | callHistory.push({ command, logPrefix, useShell, opts });
155 | return mockExecutor(command, logPrefix, useShell, opts);
156 | };
157 |
158 | const mockAxeHelpers = createMockAxeHelpers();
159 |
160 | await tapLogic(
161 | {
162 | simulatorId: '12345678-1234-4234-8234-123456789012',
163 | x: 100,
164 | y: 200,
165 | },
166 | wrappedExecutor,
167 | mockAxeHelpers,
168 | );
169 |
170 | expect(callHistory).toHaveLength(1);
171 | expect(callHistory[0]).toEqual({
172 | command: [
173 | '/mocked/axe/path',
174 | 'tap',
175 | '-x',
176 | '100',
177 | '-y',
178 | '200',
179 | '--udid',
180 | '12345678-1234-4234-8234-123456789012',
181 | ],
182 | logPrefix: '[AXe]: tap',
183 | useShell: false,
184 | opts: { env: { SOME_ENV: 'value' } },
185 | });
186 | });
187 |
188 | it('should generate correct axe command with element id target', async () => {
189 | const mockExecutor = createMockExecutor({
190 | success: true,
191 | output: 'Tap completed',
192 | });
193 |
194 | const wrappedExecutor = async (
195 | command: string[],
196 | logPrefix?: string,
197 | useShell?: boolean,
198 | opts?: { env?: Record<string, string>; cwd?: string },
199 | ) => {
200 | callHistory.push({ command, logPrefix, useShell, opts });
201 | return mockExecutor(command, logPrefix, useShell, opts);
202 | };
203 |
204 | const mockAxeHelpers = createMockAxeHelpers();
205 |
206 | await tapLogic(
207 | {
208 | simulatorId: '12345678-1234-4234-8234-123456789012',
209 | id: 'loginButton',
210 | },
211 | wrappedExecutor,
212 | mockAxeHelpers,
213 | );
214 |
215 | expect(callHistory).toHaveLength(1);
216 | expect(callHistory[0]).toEqual({
217 | command: [
218 | '/mocked/axe/path',
219 | 'tap',
220 | '--id',
221 | 'loginButton',
222 | '--udid',
223 | '12345678-1234-4234-8234-123456789012',
224 | ],
225 | logPrefix: '[AXe]: tap',
226 | useShell: false,
227 | opts: { env: { SOME_ENV: 'value' } },
228 | });
229 | });
230 |
231 | it('should generate correct axe command with element label target', async () => {
232 | const mockExecutor = createMockExecutor({
233 | success: true,
234 | output: 'Tap completed',
235 | });
236 |
237 | const wrappedExecutor = async (
238 | command: string[],
239 | logPrefix?: string,
240 | useShell?: boolean,
241 | opts?: { env?: Record<string, string>; cwd?: string },
242 | ) => {
243 | callHistory.push({ command, logPrefix, useShell, opts });
244 | return mockExecutor(command, logPrefix, useShell, opts);
245 | };
246 |
247 | const mockAxeHelpers = createMockAxeHelpers();
248 |
249 | await tapLogic(
250 | {
251 | simulatorId: '12345678-1234-4234-8234-123456789012',
252 | label: 'Log in',
253 | },
254 | wrappedExecutor,
255 | mockAxeHelpers,
256 | );
257 |
258 | expect(callHistory).toHaveLength(1);
259 | expect(callHistory[0]).toEqual({
260 | command: [
261 | '/mocked/axe/path',
262 | 'tap',
263 | '--label',
264 | 'Log in',
265 | '--udid',
266 | '12345678-1234-4234-8234-123456789012',
267 | ],
268 | logPrefix: '[AXe]: tap',
269 | useShell: false,
270 | opts: { env: { SOME_ENV: 'value' } },
271 | });
272 | });
273 |
274 | it('should prefer coordinates over id/label when both are provided', async () => {
275 | const mockExecutor = createMockExecutor({
276 | success: true,
277 | output: 'Tap completed',
278 | });
279 |
280 | const wrappedExecutor = async (
281 | command: string[],
282 | logPrefix?: string,
283 | useShell?: boolean,
284 | opts?: { env?: Record<string, string>; cwd?: string },
285 | ) => {
286 | callHistory.push({ command, logPrefix, useShell, opts });
287 | return mockExecutor(command, logPrefix, useShell, opts);
288 | };
289 |
290 | const mockAxeHelpers = createMockAxeHelpers();
291 |
292 | await tapLogic(
293 | {
294 | simulatorId: '12345678-1234-4234-8234-123456789012',
295 | x: 120,
296 | y: 240,
297 | id: 'loginButton',
298 | },
299 | wrappedExecutor,
300 | mockAxeHelpers,
301 | );
302 |
303 | expect(callHistory).toHaveLength(1);
304 | expect(callHistory[0]).toEqual({
305 | command: [
306 | '/mocked/axe/path',
307 | 'tap',
308 | '-x',
309 | '120',
310 | '-y',
311 | '240',
312 | '--udid',
313 | '12345678-1234-4234-8234-123456789012',
314 | ],
315 | logPrefix: '[AXe]: tap',
316 | useShell: false,
317 | opts: { env: { SOME_ENV: 'value' } },
318 | });
319 | });
320 |
321 | it('should generate correct axe command with pre-delay', async () => {
322 | const mockExecutor = createMockExecutor({
323 | success: true,
324 | output: 'Tap completed',
325 | });
326 |
327 | const wrappedExecutor = async (
328 | command: string[],
329 | logPrefix?: string,
330 | useShell?: boolean,
331 | opts?: { env?: Record<string, string>; cwd?: string },
332 | ) => {
333 | callHistory.push({ command, logPrefix, useShell, opts });
334 | return mockExecutor(command, logPrefix, useShell, opts);
335 | };
336 |
337 | const mockAxeHelpers = createMockAxeHelpers();
338 |
339 | await tapLogic(
340 | {
341 | simulatorId: '12345678-1234-4234-8234-123456789012',
342 | x: 150,
343 | y: 300,
344 | preDelay: 0.5,
345 | },
346 | wrappedExecutor,
347 | mockAxeHelpers,
348 | );
349 |
350 | expect(callHistory).toHaveLength(1);
351 | expect(callHistory[0]).toEqual({
352 | command: [
353 | '/mocked/axe/path',
354 | 'tap',
355 | '-x',
356 | '150',
357 | '-y',
358 | '300',
359 | '--pre-delay',
360 | '0.5',
361 | '--udid',
362 | '12345678-1234-4234-8234-123456789012',
363 | ],
364 | logPrefix: '[AXe]: tap',
365 | useShell: false,
366 | opts: { env: { SOME_ENV: 'value' } },
367 | });
368 | });
369 |
370 | it('should generate correct axe command with post-delay', async () => {
371 | const mockExecutor = createMockExecutor({
372 | success: true,
373 | output: 'Tap completed',
374 | });
375 |
376 | const wrappedExecutor = async (
377 | command: string[],
378 | logPrefix?: string,
379 | useShell?: boolean,
380 | opts?: { env?: Record<string, string>; cwd?: string },
381 | ) => {
382 | callHistory.push({ command, logPrefix, useShell, opts });
383 | return mockExecutor(command, logPrefix, useShell, opts);
384 | };
385 |
386 | const mockAxeHelpers = createMockAxeHelpers();
387 |
388 | await tapLogic(
389 | {
390 | simulatorId: '12345678-1234-4234-8234-123456789012',
391 | x: 250,
392 | y: 400,
393 | postDelay: 1.0,
394 | },
395 | wrappedExecutor,
396 | mockAxeHelpers,
397 | );
398 |
399 | expect(callHistory).toHaveLength(1);
400 | expect(callHistory[0]).toEqual({
401 | command: [
402 | '/mocked/axe/path',
403 | 'tap',
404 | '-x',
405 | '250',
406 | '-y',
407 | '400',
408 | '--post-delay',
409 | '1',
410 | '--udid',
411 | '12345678-1234-4234-8234-123456789012',
412 | ],
413 | logPrefix: '[AXe]: tap',
414 | useShell: false,
415 | opts: { env: { SOME_ENV: 'value' } },
416 | });
417 | });
418 |
419 | it('should generate correct axe command with both delays', async () => {
420 | const mockExecutor = createMockExecutor({
421 | success: true,
422 | output: 'Tap completed',
423 | });
424 |
425 | const wrappedExecutor = async (
426 | command: string[],
427 | logPrefix?: string,
428 | useShell?: boolean,
429 | opts?: { env?: Record<string, string>; cwd?: string },
430 | ) => {
431 | callHistory.push({ command, logPrefix, useShell, opts });
432 | return mockExecutor(command, logPrefix, useShell, opts);
433 | };
434 |
435 | const mockAxeHelpers = createMockAxeHelpers();
436 |
437 | await tapLogic(
438 | {
439 | simulatorId: '12345678-1234-4234-8234-123456789012',
440 | x: 350,
441 | y: 500,
442 | preDelay: 0.3,
443 | postDelay: 0.7,
444 | },
445 | wrappedExecutor,
446 | mockAxeHelpers,
447 | );
448 |
449 | expect(callHistory).toHaveLength(1);
450 | expect(callHistory[0]).toEqual({
451 | command: [
452 | '/mocked/axe/path',
453 | 'tap',
454 | '-x',
455 | '350',
456 | '-y',
457 | '500',
458 | '--pre-delay',
459 | '0.3',
460 | '--post-delay',
461 | '0.7',
462 | '--udid',
463 | '12345678-1234-4234-8234-123456789012',
464 | ],
465 | logPrefix: '[AXe]: tap',
466 | useShell: false,
467 | opts: { env: { SOME_ENV: 'value' } },
468 | });
469 | });
470 | });
471 |
472 | describe('Success Response Processing', () => {
473 | it('should return successful response for basic tap', async () => {
474 | const mockExecutor = createMockExecutor({
475 | success: true,
476 | output: 'Tap completed',
477 | });
478 |
479 | const mockAxeHelpers = createMockAxeHelpers();
480 |
481 | const result = await tapLogic(
482 | {
483 | simulatorId: '12345678-1234-4234-8234-123456789012',
484 | x: 100,
485 | y: 200,
486 | },
487 | mockExecutor,
488 | mockAxeHelpers,
489 | );
490 |
491 | expect(result).toEqual({
492 | content: [
493 | {
494 | type: 'text',
495 | text: 'Tap at (100, 200) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
496 | },
497 | ],
498 | isError: false,
499 | });
500 | });
501 |
502 | it('should return successful response with coordinate warning when describe_ui not called', async () => {
503 | const mockExecutor = createMockExecutor({
504 | success: true,
505 | output: 'Tap completed',
506 | });
507 |
508 | const mockAxeHelpers = createMockAxeHelpers();
509 |
510 | const result = await tapLogic(
511 | {
512 | simulatorId: '87654321-4321-4321-4321-210987654321',
513 | x: 150,
514 | y: 300,
515 | },
516 | mockExecutor,
517 | mockAxeHelpers,
518 | );
519 |
520 | expect(result).toEqual({
521 | content: [
522 | {
523 | type: 'text',
524 | text: 'Tap at (150, 300) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
525 | },
526 | ],
527 | isError: false,
528 | });
529 | });
530 |
531 | it('should return successful response with delays', async () => {
532 | const mockExecutor = createMockExecutor({
533 | success: true,
534 | output: 'Tap completed',
535 | });
536 |
537 | const mockAxeHelpers = createMockAxeHelpers();
538 |
539 | const result = await tapLogic(
540 | {
541 | simulatorId: '12345678-1234-4234-8234-123456789012',
542 | x: 250,
543 | y: 400,
544 | preDelay: 0.5,
545 | postDelay: 1.0,
546 | },
547 | mockExecutor,
548 | mockAxeHelpers,
549 | );
550 |
551 | expect(result).toEqual({
552 | content: [
553 | {
554 | type: 'text',
555 | text: 'Tap at (250, 400) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
556 | },
557 | ],
558 | isError: false,
559 | });
560 | });
561 |
562 | it('should return successful response with integer coordinates', async () => {
563 | const mockExecutor = createMockExecutor({
564 | success: true,
565 | output: 'Tap completed',
566 | });
567 |
568 | const mockAxeHelpers = createMockAxeHelpers();
569 |
570 | const result = await tapLogic(
571 | {
572 | simulatorId: '12345678-1234-4234-8234-123456789012',
573 | x: 0,
574 | y: 0,
575 | },
576 | mockExecutor,
577 | mockAxeHelpers,
578 | );
579 |
580 | expect(result).toEqual({
581 | content: [
582 | {
583 | type: 'text',
584 | text: 'Tap at (0, 0) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
585 | },
586 | ],
587 | isError: false,
588 | });
589 | });
590 |
591 | it('should return successful response with large coordinates', async () => {
592 | const mockExecutor = createMockExecutor({
593 | success: true,
594 | output: 'Tap completed',
595 | });
596 |
597 | const mockAxeHelpers = createMockAxeHelpers();
598 |
599 | const result = await tapLogic(
600 | {
601 | simulatorId: '12345678-1234-4234-8234-123456789012',
602 | x: 1920,
603 | y: 1080,
604 | },
605 | mockExecutor,
606 | mockAxeHelpers,
607 | );
608 |
609 | expect(result).toEqual({
610 | content: [
611 | {
612 | type: 'text',
613 | text: 'Tap at (1920, 1080) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
614 | },
615 | ],
616 | isError: false,
617 | });
618 | });
619 |
620 | it('should return successful response for element id target', async () => {
621 | const mockExecutor = createMockExecutor({
622 | success: true,
623 | output: 'Tap completed',
624 | });
625 |
626 | const mockAxeHelpers = createMockAxeHelpers();
627 |
628 | const result = await tapLogic(
629 | {
630 | simulatorId: '12345678-1234-4234-8234-123456789012',
631 | id: 'loginButton',
632 | },
633 | mockExecutor,
634 | mockAxeHelpers,
635 | );
636 |
637 | expect(result).toEqual({
638 | content: [
639 | {
640 | type: 'text',
641 | text: 'Tap on element id "loginButton" simulated successfully.',
642 | },
643 | ],
644 | isError: false,
645 | });
646 | });
647 |
648 | it('should return successful response for element label target', async () => {
649 | const mockExecutor = createMockExecutor({
650 | success: true,
651 | output: 'Tap completed',
652 | });
653 |
654 | const mockAxeHelpers = createMockAxeHelpers();
655 |
656 | const result = await tapLogic(
657 | {
658 | simulatorId: '12345678-1234-4234-8234-123456789012',
659 | label: 'Log in',
660 | },
661 | mockExecutor,
662 | mockAxeHelpers,
663 | );
664 |
665 | expect(result).toEqual({
666 | content: [
667 | {
668 | type: 'text',
669 | text: 'Tap on element label "Log in" simulated successfully.',
670 | },
671 | ],
672 | isError: false,
673 | });
674 | });
675 | });
676 |
677 | describe('Plugin Handler Validation', () => {
678 | it('should require simulatorId session default when not provided', async () => {
679 | const result = await tapPlugin.handler({
680 | x: 100,
681 | y: 200,
682 | });
683 |
684 | expect(result.isError).toBe(true);
685 | const message = result.content[0].text;
686 | expect(message).toContain('Missing required session defaults');
687 | expect(message).toContain('simulatorId is required');
688 | expect(message).toContain('session-set-defaults');
689 | });
690 |
691 | it('should return validation error for missing x coordinate', async () => {
692 | sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });
693 |
694 | const result = await tapPlugin.handler({
695 | y: 200,
696 | });
697 |
698 | expect(result.isError).toBe(true);
699 | const message = result.content[0].text;
700 | expect(message).toContain('Parameter validation failed');
701 | expect(message).toContain('x: X coordinate is required when y is provided.');
702 | });
703 |
704 | it('should return validation error for missing y coordinate', async () => {
705 | sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });
706 |
707 | const result = await tapPlugin.handler({
708 | x: 100,
709 | });
710 |
711 | expect(result.isError).toBe(true);
712 | const message = result.content[0].text;
713 | expect(message).toContain('Parameter validation failed');
714 | expect(message).toContain('y: Y coordinate is required when x is provided.');
715 | });
716 |
717 | it('should return validation error when both id and label are provided without coordinates', async () => {
718 | sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });
719 |
720 | const result = await tapPlugin.handler({
721 | id: 'loginButton',
722 | label: 'Log in',
723 | });
724 |
725 | expect(result.isError).toBe(true);
726 | const message = result.content[0].text;
727 | expect(message).toContain('Parameter validation failed');
728 | expect(message).toContain('id: Provide either id or label, not both.');
729 | });
730 |
731 | it('should return validation error for non-integer x coordinate', async () => {
732 | sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });
733 |
734 | const result = await tapPlugin.handler({
735 | x: 3.14,
736 | y: 200,
737 | });
738 |
739 | expect(result.isError).toBe(true);
740 | const message = result.content[0].text;
741 | expect(message).toContain('Parameter validation failed');
742 | expect(message).toContain('x: X coordinate must be an integer');
743 | });
744 |
745 | it('should return validation error for non-integer y coordinate', async () => {
746 | sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });
747 |
748 | const result = await tapPlugin.handler({
749 | x: 100,
750 | y: 3.14,
751 | });
752 |
753 | expect(result.isError).toBe(true);
754 | const message = result.content[0].text;
755 | expect(message).toContain('Parameter validation failed');
756 | expect(message).toContain('y: Y coordinate must be an integer');
757 | });
758 |
759 | it('should return validation error for negative preDelay', async () => {
760 | sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });
761 |
762 | const result = await tapPlugin.handler({
763 | x: 100,
764 | y: 200,
765 | preDelay: -1,
766 | });
767 |
768 | expect(result.isError).toBe(true);
769 | const message = result.content[0].text;
770 | expect(message).toContain('Parameter validation failed');
771 | expect(message).toContain('preDelay: Pre-delay must be non-negative');
772 | });
773 |
774 | it('should return validation error for negative postDelay', async () => {
775 | sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });
776 |
777 | const result = await tapPlugin.handler({
778 | x: 100,
779 | y: 200,
780 | postDelay: -1,
781 | });
782 |
783 | expect(result.isError).toBe(true);
784 | const message = result.content[0].text;
785 | expect(message).toContain('Parameter validation failed');
786 | expect(message).toContain('postDelay: Post-delay must be non-negative');
787 | });
788 | });
789 |
790 | describe('Handler Behavior (Complete Literal Returns)', () => {
791 | it('should return DependencyError when axe binary is not found', async () => {
792 | const mockExecutor = createMockExecutor({
793 | success: true,
794 | output: 'Tap completed',
795 | error: undefined,
796 | });
797 |
798 | const mockAxeHelpers = createMockAxeHelpersWithNullPath();
799 |
800 | const result = await tapLogic(
801 | {
802 | simulatorId: '12345678-1234-4234-8234-123456789012',
803 | x: 100,
804 | y: 200,
805 | preDelay: 0.5,
806 | postDelay: 1.0,
807 | },
808 | mockExecutor,
809 | mockAxeHelpers,
810 | );
811 |
812 | expect(result).toEqual({
813 | content: [
814 | {
815 | type: 'text',
816 | 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.',
817 | },
818 | ],
819 | isError: true,
820 | });
821 | });
822 |
823 | it('should handle DependencyError when axe binary not found (second test)', async () => {
824 | const mockExecutor = createMockExecutor({
825 | success: false,
826 | output: '',
827 | error: 'Coordinates out of bounds',
828 | });
829 |
830 | const mockAxeHelpers = createMockAxeHelpersWithNullPath();
831 |
832 | const result = await tapLogic(
833 | {
834 | simulatorId: '12345678-1234-4234-8234-123456789012',
835 | x: 100,
836 | y: 200,
837 | },
838 | mockExecutor,
839 | mockAxeHelpers,
840 | );
841 |
842 | expect(result).toEqual({
843 | content: [
844 | {
845 | type: 'text',
846 | 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.',
847 | },
848 | ],
849 | isError: true,
850 | });
851 | });
852 |
853 | it('should handle DependencyError when axe binary not found (third test)', async () => {
854 | const mockExecutor = createMockExecutor({
855 | success: false,
856 | output: '',
857 | error: 'System error occurred',
858 | });
859 |
860 | const mockAxeHelpers = createMockAxeHelpersWithNullPath();
861 |
862 | const result = await tapLogic(
863 | {
864 | simulatorId: '12345678-1234-4234-8234-123456789012',
865 | x: 100,
866 | y: 200,
867 | },
868 | mockExecutor,
869 | mockAxeHelpers,
870 | );
871 |
872 | expect(result).toEqual({
873 | content: [
874 | {
875 | type: 'text',
876 | 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.',
877 | },
878 | ],
879 | isError: true,
880 | });
881 | });
882 |
883 | it('should handle DependencyError when axe binary not found (fourth test)', async () => {
884 | const mockExecutor = async () => {
885 | throw new Error('ENOENT: no such file or directory');
886 | };
887 |
888 | const mockAxeHelpers = createMockAxeHelpersWithNullPath();
889 |
890 | const result = await tapLogic(
891 | {
892 | simulatorId: '12345678-1234-4234-8234-123456789012',
893 | x: 100,
894 | y: 200,
895 | },
896 | mockExecutor,
897 | mockAxeHelpers,
898 | );
899 |
900 | expect(result).toEqual({
901 | content: [
902 | {
903 | type: 'text',
904 | 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.',
905 | },
906 | ],
907 | isError: true,
908 | });
909 | });
910 |
911 | it('should handle DependencyError when axe binary not found (fifth test)', async () => {
912 | const mockExecutor = async () => {
913 | throw new Error('Unexpected error');
914 | };
915 |
916 | const mockAxeHelpers = createMockAxeHelpersWithNullPath();
917 |
918 | const result = await tapLogic(
919 | {
920 | simulatorId: '12345678-1234-4234-8234-123456789012',
921 | x: 100,
922 | y: 200,
923 | },
924 | mockExecutor,
925 | mockAxeHelpers,
926 | );
927 |
928 | expect(result).toEqual({
929 | content: [
930 | {
931 | type: 'text',
932 | 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.',
933 | },
934 | ],
935 | isError: true,
936 | });
937 | });
938 |
939 | it('should handle DependencyError when axe binary not found (sixth test)', async () => {
940 | const mockExecutor = async () => {
941 | throw 'String error';
942 | };
943 |
944 | const mockAxeHelpers = createMockAxeHelpersWithNullPath();
945 |
946 | const result = await tapLogic(
947 | {
948 | simulatorId: '12345678-1234-4234-8234-123456789012',
949 | x: 100,
950 | y: 200,
951 | },
952 | mockExecutor,
953 | mockAxeHelpers,
954 | );
955 |
956 | expect(result).toEqual({
957 | content: [
958 | {
959 | type: 'text',
960 | 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.',
961 | },
962 | ],
963 | isError: true,
964 | });
965 | });
966 | });
967 | });
968 |
```
--------------------------------------------------------------------------------
/docs/dev/ZOD_MIGRATION_GUIDE.md:
--------------------------------------------------------------------------------
```markdown
1 | # Migration guide
2 |
3 | import { Callout } from "fumadocs-ui/components/callout";
4 | import { Tabs, Tab } from "fumadocs-ui/components/tabs";
5 |
6 | This migration guide aims to list the breaking changes in Zod 4 in order of highest to lowest impact. To learn more about the performance enhancements and new features of Zod 4, read the [introductory post](/v4).
7 |
8 | {/* To give the ecosystem time to migrate, Zod 4 will initially be published alongside Zod v3.25. To use Zod 4, upgrade to `[email protected]` or later: */}
9 |
10 | ```
11 | npm install zod@^4.0.0
12 | ```
13 |
14 | {/* Zod 4 is available at the `"/v4"` subpath:
15 |
16 | ```ts
17 | import * as z from "zod";
18 | ``` */}
19 |
20 | Many of Zod's behaviors and APIs have been made more intuitive and cohesive. The breaking changes described in this document often represent major quality-of-life improvements for Zod users. I strongly recommend reading this guide thoroughly.
21 |
22 | <Callout>
23 | **Note** — Zod 3 exported a number of undocumented quasi-internal utility types and functions that are not considered part of the public API. Changes to those are not documented here.
24 | </Callout>
25 |
26 | <Callout>
27 | **Unofficial codemod** — A community-maintained codemod [`zod-v3-to-v4`](https://github.com/nicoespeon/zod-v3-to-v4) is available.
28 | </Callout>
29 |
30 | ## Error customization
31 |
32 | Zod 4 standardizes the APIs for error customization under a single, unified `error` param. Previously Zod's error customization APIs were fragmented and inconsistent. This is cleaned up in Zod 4.
33 |
34 | ### deprecates `message`
35 |
36 | Replaces `message` with `error`. The `message` parameter is still supported but deprecated.
37 |
38 | <Tabs groupId="error-message" items={["Zod 4", "Zod 3"]} persist>
39 | <Tab value="Zod 4">
40 | ```ts
41 | z.string().min(5, { error: "Too short." });
42 | ```
43 | </Tab>
44 |
45 | <Tab value="Zod 3">
46 | ```ts
47 | z.string().min(5, { message: "Too short." });
48 | ```
49 | </Tab>
50 | </Tabs>
51 |
52 | ### drops `invalid_type_error` and `required_error`
53 |
54 | The `invalid_type_error` / `required_error` params have been dropped. These were hastily added years ago as a way to customize errors that was less verbose than `errorMap`. They came with all sorts of footguns (they can't be used in conjunction with `errorMap`) and do not align with Zod's actual issue codes (there is no `required` issue code).
55 |
56 | These can now be cleanly represented with the new `error` parameter.
57 |
58 | <Tabs groupId="error-type" items={["Zod 4", "Zod 3"]} persist>
59 | <Tab value="Zod 4">
60 | ```ts
61 | z.string({
62 | error: (issue) => issue.input === undefined
63 | ? "This field is required"
64 | : "Not a string"
65 | });
66 | ```
67 | </Tab>
68 |
69 | <Tab value="Zod 3">
70 | ```ts
71 | z.string({
72 | required_error: "This field is required",
73 | invalid_type_error: "Not a string",
74 | });
75 | ```
76 | </Tab>
77 | </Tabs>
78 |
79 | ### drops `errorMap`
80 |
81 | This is renamed to `error`.
82 |
83 | Error maps can also now return a plain `string` (instead of `{message: string}`). They can also return `undefined`, which tells Zod to yield control to the next error map in the chain.
84 |
85 | <Tabs groupId="error-map" items={["Zod 4", "Zod 3"]} persist>
86 | <Tab value="Zod 4">
87 | ```ts
88 | z.string().min(5, {
89 | error: (issue) => {
90 | if (issue.code === "too_small") {
91 | return `Value must be >${issue.minimum}`
92 | }
93 | },
94 | });
95 | ```
96 | </Tab>
97 |
98 | <Tab value="Zod 3">
99 | ```ts
100 | z.string({
101 | errorMap: (issue, ctx) => {
102 | if (issue.code === "too_small") {
103 | return { message: `Value must be >${issue.minimum}` };
104 | }
105 | return { message: ctx.defaultError };
106 | },
107 | });
108 | ```
109 | </Tab>
110 | </Tabs>
111 |
112 | {/* ## `.safeParse()`
113 |
114 | For performance reasons, the errors returned by `.safeParse()` and `.safeParseAsync()` no longer extend `Error`.
115 |
116 | ```ts
117 | const result = z.string().safeParse(12);
118 | result.error! instanceof Error; // => false
119 | ```
120 |
121 | It is very slow to instantiate `Error` instances in JavaScript, as the initialization process snapshots the call stack. In the case of Zod's "safe" parse methods, it's expected that you will handle errors at the point of parsing, so instantiating a true `Error` object adds little value anyway.
122 |
123 | > Pro tip: prefer `.safeParse()` over `try/catch` in performance-sensitive code.
124 |
125 | By contrast the errors thrown by `.parse()` and `.parseAsync()` still extend `Error`. Aside from the prototype difference, the error classes are identical.
126 |
127 | ```ts
128 | try {
129 | z.string().parse(12);
130 | } catch (err) {
131 | console.log(err instanceof Error); // => true
132 | }
133 | ```
134 | */}
135 |
136 | ## `ZodError`
137 |
138 | {/*
139 | ### changes to `.message`
140 |
141 | Previously the `.message` property on `ZodError` was a JSON.stringified copy of the `.issues` array. This was redundant, confusing, and a bit of an abuse of the `.message` property. Also due to the [`Error` prototype changes](#safeparse) (and inconsistencies in how Node.js logs `Error` subclasses vs other objects) the logging of a multi-line `.message` property got a lot uglier:
142 |
143 | ```sh
144 | $ tsx index.ts
145 | ZodError {
146 | message: '[\n' +
147 | ' {\n' +
148 | ' "expected": "string",\n' +
149 | ' "code": "invalid_type",\n' +
150 | ' "path": [],\n' +
151 | ' "message": "Invalid input: expected string, received number"\n' +
152 | ' }\n' +
153 | ']'
154 | }
155 | ```
156 |
157 |
158 | For these reasons, the `.message` property is left empty and the `.issues` array is marked as enumerable. This keeps error logging consistent and pretty:
159 |
160 | ```sh
161 | $ tsx index.ts
162 | z.string().parse(234);
163 |
164 | ZodError {
165 | issues: [
166 | {
167 | expected: 'string',
168 | code: 'invalid_type',
169 | path: [],
170 | message: 'Invalid input: expected string, received number'
171 | }
172 | ]
173 | }
174 | ```
175 |
176 | Vitest uses special handling for `Error` subclasses that ignores enumerable properties. */}
177 |
178 | ### updates issue formats
179 |
180 | The issue formats have been dramatically streamlined.
181 |
182 | ```ts
183 | import * as z from "zod"; // v4
184 |
185 | type IssueFormats =
186 | | z.core.$ZodIssueInvalidType
187 | | z.core.$ZodIssueTooBig
188 | | z.core.$ZodIssueTooSmall
189 | | z.core.$ZodIssueInvalidStringFormat
190 | | z.core.$ZodIssueNotMultipleOf
191 | | z.core.$ZodIssueUnrecognizedKeys
192 | | z.core.$ZodIssueInvalidValue
193 | | z.core.$ZodIssueInvalidUnion
194 | | z.core.$ZodIssueInvalidKey // new: used for z.record/z.map
195 | | z.core.$ZodIssueInvalidElement // new: used for z.map/z.set
196 | | z.core.$ZodIssueCustom;
197 | ```
198 |
199 | Below is the list of Zod 3 issues types and their Zod 4 equivalent:
200 |
201 | ```ts
202 | import * as z from "zod"; // v3
203 |
204 | export type IssueFormats =
205 | | z.ZodInvalidTypeIssue // ♻️ renamed to z.core.$ZodIssueInvalidType
206 | | z.ZodTooBigIssue // ♻️ renamed to z.core.$ZodIssueTooBig
207 | | z.ZodTooSmallIssue // ♻️ renamed to z.core.$ZodIssueTooSmall
208 | | z.ZodInvalidStringIssue // ♻️ z.core.$ZodIssueInvalidStringFormat
209 | | z.ZodNotMultipleOfIssue // ♻️ renamed to z.core.$ZodIssueNotMultipleOf
210 | | z.ZodUnrecognizedKeysIssue // ♻️ renamed to z.core.$ZodIssueUnrecognizedKeys
211 | | z.ZodInvalidUnionIssue // ♻️ renamed to z.core.$ZodIssueInvalidUnion
212 | | z.ZodCustomIssue // ♻️ renamed to z.core.$ZodIssueCustom
213 | | z.ZodInvalidEnumValueIssue // ❌ merged in z.core.$ZodIssueInvalidValue
214 | | z.ZodInvalidLiteralIssue // ❌ merged into z.core.$ZodIssueInvalidValue
215 | | z.ZodInvalidUnionDiscriminatorIssue // ❌ throws an Error at schema creation time
216 | | z.ZodInvalidArgumentsIssue // ❌ z.function throws ZodError directly
217 | | z.ZodInvalidReturnTypeIssue // ❌ z.function throws ZodError directly
218 | | z.ZodInvalidDateIssue // ❌ merged into invalid_type
219 | | z.ZodInvalidIntersectionTypesIssue // ❌ removed (throws regular Error)
220 | | z.ZodNotFiniteIssue // ❌ infinite values no longer accepted (invalid_type)
221 | ```
222 |
223 | While certain Zod 4 issue types have been merged, dropped, and modified, each issue remains structurally similar to Zod 3 counterpart (identical, in most cases). All issues still conform to the same base interface as Zod 3, so most common error handling logic will work without modification.
224 |
225 | ```ts
226 | export interface $ZodIssueBase {
227 | readonly code?: string;
228 | readonly input?: unknown;
229 | readonly path: PropertyKey[];
230 | readonly message: string;
231 | }
232 | ```
233 |
234 | ### changes error map precedence
235 |
236 | The error map precedence has been changed to be more consistent. Specifically, an error map passed into `.parse()` *no longer* takes precedence over a schema-level error map.
237 |
238 | ```ts
239 | const mySchema = z.string({ error: () => "Schema-level error" });
240 |
241 | // in Zod 3
242 | mySchema.parse(12, { error: () => "Contextual error" }); // => "Contextual error"
243 |
244 | // in Zod 4
245 | mySchema.parse(12, { error: () => "Contextual error" }); // => "Schema-level error"
246 | ```
247 |
248 | ### deprecates `.format()`
249 |
250 | The `.format()` method on `ZodError` has been deprecated. Instead use the top-level `z.treeifyError()` function. Read the [Formatting errors docs](/error-formatting) for more information.
251 |
252 | ### deprecates `.flatten()`
253 |
254 | The `.flatten()` method on `ZodError` has also been deprecated. Instead use the top-level `z.treeifyError()` function. Read the [Formatting errors docs](/error-formatting) for more information.
255 |
256 | ### drops `.formErrors`
257 |
258 | This API was identical to `.flatten()`. It exists for historical reasons and isn't documented.
259 |
260 | ### deprecates `.addIssue()` and `.addIssues()`
261 |
262 | Directly push to `err.issues` array instead, if necessary.
263 |
264 | ```ts
265 | myError.issues.push({
266 | // new issue
267 | });
268 | ```
269 |
270 | {/* ## `.and()` dropped
271 |
272 | The `.and()` method on `ZodType` has been dropped in favor of `z.intersection(A, B)`. Not only is this method rarely used, there are few good reasons to use intersections at all. The `.and()` API prevented bundlers from treeshaking `ZodIntersection`, a fairly large and complex class.
273 |
274 | ```ts
275 | z.object({ a: z.string() }).and(z.object({ b: z.number() })); // ❌
276 |
277 | // use z.intersection
278 | z.intersection(z.object({ a: z.string() }), z.object({ b: z.number() })); // ✅
279 | // or .extend() when possible
280 | z.object({ a: z.string() }).extend(z.object({ b: z.number() })); // ✅
281 | ``` */}
282 |
283 | ## `z.number()`
284 |
285 | ### no infinite values
286 |
287 | `POSITIVE_INFINITY` and `NEGATIVE_INFINITY` are no longer considered valid values for `z.number()`.
288 |
289 | ### `.safe()` no longer accepts floats
290 |
291 | In Zod 3, `z.number().safe()` is deprecated. It now behaves identically to `.int()` (see below). Importantly, that means it no longer accepts floats.
292 |
293 | ### `.int()` accepts safe integers only
294 |
295 | The `z.number().int()` API no longer accepts unsafe integers (outside the range of `Number.MIN_SAFE_INTEGER` and `Number.MAX_SAFE_INTEGER`). Using integers out of this range causes spontaneous rounding errors. (Also: You should switch to `z.int()`.)
296 |
297 | ## `z.string()` updates
298 |
299 | ### deprecates `.email()` etc
300 |
301 | String formats are now represented as *subclasses* of `ZodString`, instead of simple internal refinements. As such, these APIs have been moved to the top-level `z` namespace. Top-level APIs are also less verbose and more tree-shakable.
302 |
303 | ```ts
304 | z.email();
305 | z.uuid();
306 | z.url();
307 | z.emoji(); // validates a single emoji character
308 | z.base64();
309 | z.base64url();
310 | z.nanoid();
311 | z.cuid();
312 | z.cuid2();
313 | z.ulid();
314 | z.ipv4();
315 | z.ipv6();
316 | z.cidrv4(); // ip range
317 | z.cidrv6(); // ip range
318 | z.iso.date();
319 | z.iso.time();
320 | z.iso.datetime();
321 | z.iso.duration();
322 | ```
323 |
324 | The method forms (`z.string().email()`) still exist and work as before, but are now deprecated.
325 |
326 | ```ts
327 | z.string().email(); // ❌ deprecated
328 | z.email(); // ✅
329 | ```
330 |
331 | ### stricter `.uuid()`
332 |
333 | The `z.uuid()` now validates UUIDs more strictly against the RFC 9562/4122 specification; specifically, the variant bits must be `10` per the spec. For a more permissive "UUID-like" validator, use `z.guid()`.
334 |
335 | ```ts
336 | z.uuid(); // RFC 9562/4122 compliant UUID
337 | z.guid(); // any 8-4-4-4-12 hex pattern
338 | ```
339 |
340 | ### no padding in `.base64url()`
341 |
342 | Padding is no longer allowed in `z.base64url()` (formerly `z.string().base64url()`). Generally it's desirable for base64url strings to be unpadded and URL-safe.
343 |
344 | ### drops `z.string().ip()`
345 |
346 | This has been replaced with separate `.ipv4()` and `.ipv6()` methods. Use `z.union()` to combine them if you need to accept both.
347 |
348 | ```ts
349 | z.string().ip() // ❌
350 | z.ipv4() // ✅
351 | z.ipv6() // ✅
352 | ```
353 |
354 | ### updates `z.string().ipv6()`
355 |
356 | Validation now happens using the `new URL()` constructor, which is far more robust than the old regular expression approach. Some invalid values that passed validation previously may now fail.
357 |
358 | ### drops `z.string().cidr()`
359 |
360 | Similarly, this has been replaced with separate `.cidrv4()` and `.cidrv6()` methods. Use `z.union()` to combine them if you need to accept both.
361 |
362 | ```ts
363 | z.string().cidr() // ❌
364 | z.cidrv4() // ✅
365 | z.cidrv6() // ✅
366 | ```
367 |
368 | ## `z.coerce` updates
369 |
370 | The input type of all `z.coerce` schemas is now `unknown`.
371 |
372 | ```ts
373 | const schema = z.coerce.string();
374 | type schemaInput = z.input<typeof schema>;
375 |
376 | // Zod 3: string;
377 | // Zod 4: unknown;
378 | ```
379 |
380 | ## `.default()` updates
381 |
382 | The application of `.default()` has changed in a subtle way. If the input is `undefined`, `ZodDefault` short-circuits the parsing process and returns the default value. The default value must be assignable to the *output type*.
383 |
384 | ```ts
385 | const schema = z.string()
386 | .transform(val => val.length)
387 | .default(0); // should be a number
388 | schema.parse(undefined); // => 0
389 | ```
390 |
391 | In Zod 3, `.default()` expected a value that matched the *input type*. `ZodDefault` would parse the default value, instead of short-circuiting. As such, the default value must be assignable to the *input type* of the schema.
392 |
393 | ```ts
394 | // Zod 3
395 | const schema = z.string()
396 | .transform(val => val.length)
397 | .default("tuna");
398 | schema.parse(undefined); // => 4
399 | ```
400 |
401 | To replicate the old behavior, Zod implements a new `.prefault()` API. This is short for "pre-parse default".
402 |
403 | ```ts
404 | // Zod 3
405 | const schema = z.string()
406 | .transform(val => val.length)
407 | .prefault("tuna");
408 | schema.parse(undefined); // => 4
409 | ```
410 |
411 | ## `z.object()`
412 |
413 | ### defaults applied within optional fields
414 |
415 | Defaults inside your properties are applied, even within optional fields. This aligns better with expectations and resolves a long-standing usability issue with Zod 3. This is a subtle change that may cause breakage in code paths that rely on key existence, etc.
416 |
417 | ```ts
418 | const schema = z.object({
419 | a: z.string().default("tuna").optional(),
420 | });
421 |
422 | schema.parse({});
423 | // Zod 4: { a: "tuna" }
424 | // Zod 3: {}
425 | ```
426 |
427 | ### deprecates `.strict()` and `.passthrough()`
428 |
429 | These methods are generally no longer necessary. Instead use the top-level `z.strictObject()` and `z.looseObject()` functions.
430 |
431 | ```ts
432 | // Zod 3
433 | z.object({ name: z.string() }).strict();
434 | z.object({ name: z.string() }).passthrough();
435 |
436 | // Zod 4
437 | z.strictObject({ name: z.string() });
438 | z.looseObject({ name: z.string() });
439 | ```
440 |
441 | > These methods are still available for backwards compatibility, and they will not be removed. They are considered legacy.
442 |
443 | ### deprecates `.strip()`
444 |
445 | This was never particularly useful, as it was the default behavior of `z.object()`. To convert a strict object to a "regular" one, use `z.object(A.shape)`.
446 |
447 | ### drops `.nonstrict()`
448 |
449 | This long-deprecated alias for `.strip()` has been removed.
450 |
451 | ### drops `.deepPartial()`
452 |
453 | This has been long deprecated in Zod 3 and it now removed in Zod 4. There is no direct alternative to this API. There were lots of footguns in its implementation, and its use is generally an anti-pattern.
454 |
455 | ### changes `z.unknown()` optionality
456 |
457 | The `z.unknown()` and `z.any()` types are no longer marked as "key optional" in the inferred types.
458 |
459 | ```ts
460 | const mySchema = z.object({
461 | a: z.any(),
462 | b: z.unknown()
463 | });
464 | // Zod 3: { a?: any; b?: unknown };
465 | // Zod 4: { a: any; b: unknown };
466 | ```
467 |
468 | ### deprecates `.merge()`
469 |
470 | The `.merge()` method on `ZodObject` has been deprecated in favor of `.extend()`. The `.extend()` method provides the same functionality, avoids ambiguity around strictness inheritance, and has better TypeScript performance.
471 |
472 | ```ts
473 | // .merge (deprecated)
474 | const ExtendedSchema = BaseSchema.merge(AdditionalSchema);
475 |
476 | // .extend (recommended)
477 | const ExtendedSchema = BaseSchema.extend(AdditionalSchema.shape);
478 |
479 | // or use destructuring (best tsc performance)
480 | const ExtendedSchema = z.object({
481 | ...BaseSchema.shape,
482 | ...AdditionalSchema.shape,
483 | });
484 | ```
485 |
486 | > **Note**: For even better TypeScript performance, consider using object destructuring instead of `.extend()`. See the [API documentation](/api?id=extend) for more details.
487 |
488 | ## `z.nativeEnum()` deprecated
489 |
490 | The `z.nativeEnum()` function is now deprecated in favor of just `z.enum()`. The `z.enum()` API has been overloaded to support an enum-like input.
491 |
492 | ```ts
493 | enum Color {
494 | Red = "red",
495 | Green = "green",
496 | Blue = "blue",
497 | }
498 |
499 | const ColorSchema = z.enum(Color); // ✅
500 | ```
501 |
502 | As part of this refactor of `ZodEnum`, a number of long-deprecated and redundant features have been removed. These were all identical and only existed for historical reasons.
503 |
504 | ```ts
505 | ColorSchema.enum.Red; // ✅ => "Red" (canonical API)
506 | ColorSchema.Enum.Red; // ❌ removed
507 | ColorSchema.Values.Red; // ❌ removed
508 | ```
509 |
510 | ## `z.array()`
511 |
512 | ### changes `.nonempty()` type
513 |
514 | This now behaves identically to `z.array().min(1)`. The inferred type does not change.
515 |
516 | ```ts
517 | const NonEmpty = z.array(z.string()).nonempty();
518 |
519 | type NonEmpty = z.infer<typeof NonEmpty>;
520 | // Zod 3: [string, ...string[]]
521 | // Zod 4: string[]
522 | ```
523 |
524 | The old behavior is now better represented with `z.tuple()` and a "rest" argument. This aligns more closely to TypeScript's type system.
525 |
526 | ```ts
527 | z.tuple([z.string()], z.string());
528 | // => [string, ...string[]]
529 | ```
530 |
531 | ## `z.promise()` deprecated
532 |
533 | There's rarely a reason to use `z.promise()`. If you have an input that may be a `Promise`, just `await` it before parsing it with Zod.
534 |
535 | > If you are using `z.promise` to define an async function with `z.function()`, that's no longer necessary either; see the [`ZodFunction`](#function) section below.
536 |
537 | ## `z.function()`
538 |
539 | The result of `z.function()` is no longer a Zod schema. Instead, it acts as a standalone "function factory" for defining Zod-validated functions. The API has also changed; you define an `input` and `output` schema upfront, instead of using `args()` and `.returns()` methods.
540 |
541 | <Tabs groupId="lib" items={["Zod 4", "Zod 3"]} persist>
542 | <Tab value="Zod 4">
543 | ```ts
544 | const myFunction = z.function({
545 | input: [z.object({
546 | name: z.string(),
547 | age: z.number().int(),
548 | })],
549 | output: z.string(),
550 | });
551 |
552 | myFunction.implement((input) => {
553 | return `Hello ${input.name}, you are ${input.age} years old.`;
554 | });
555 | ```
556 | </Tab>
557 |
558 | <Tab value="Zod 3">
559 | ```ts
560 | const myFunction = z.function()
561 | .args(z.object({
562 | name: z.string(),
563 | age: z.number().int(),
564 | }))
565 | .returns(z.string());
566 |
567 | myFunction.implement((input) => {
568 | return `Hello ${input.name}, you are ${input.age} years old.`;
569 | });
570 | ```
571 | </Tab>
572 | </Tabs>
573 |
574 | If you have a desperate need for a Zod schema with a function type, consider [this workaround](https://github.com/colinhacks/zod/issues/4143#issuecomment-2845134912).
575 |
576 | ### adds `.implementAsync()`
577 |
578 | To define an async function, use `implementAsync()` instead of `implement()`.
579 |
580 | ```ts
581 | myFunction.implementAsync(async (input) => {
582 | return `Hello ${input.name}, you are ${input.age} years old.`;
583 | });
584 | ```
585 |
586 | ## `.refine()`
587 |
588 | ### ignores type predicates
589 |
590 | In Zod 3, passing a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) as a refinement functions could still narrow the type of a schema. This wasn't documented but was discussed in some issues. This is no longer the case.
591 |
592 | ```ts
593 | const mySchema = z.unknown().refine((val): val is string => {
594 | return typeof val === "string"
595 | });
596 |
597 | type MySchema = z.infer<typeof mySchema>;
598 | // Zod 3: `string`
599 | // Zod 4: still `unknown`
600 | ```
601 |
602 | ### drops `ctx.path`
603 |
604 | Zod's new parsing architecture does not eagerly evaluate the `path` array. This was a necessary change that unlocks Zod 4's dramatic performance improvements.
605 |
606 | ```ts
607 | z.string().superRefine((val, ctx) => {
608 | ctx.path; // ❌ no longer available
609 | });
610 | ```
611 |
612 | ### drops function as second argument
613 |
614 | The following horrifying overload has been removed.
615 |
616 | ```ts
617 | const longString = z.string().refine(
618 | (val) => val.length > 10,
619 | (val) => ({ message: `${val} is not more than 10 characters` })
620 | );
621 | ```
622 |
623 | {/* ## `.superRefine()` deprecated
624 |
625 | The `.superRefine()` method has been deprecated in favor of `.check()`. The `.check()` method provides the same functionality with a cleaner API. The `.check()` method is also available on Zod and Zod Mini schemas.
626 |
627 | ```ts
628 | const UniqueStringArray = z.array(z.string()).check((ctx) => {
629 | if (ctx.value.length > 3) {
630 | ctx.issues.push({
631 | code: "too_big",
632 | maximum: 3,
633 | origin: "array",
634 | inclusive: true,
635 | message: "Too many items 😡",
636 | input: ctx.value
637 | });
638 | }
639 |
640 | if (ctx.value.length !== new Set(ctx.value).size) {
641 | ctx.issues.push({
642 | code: "custom",
643 | message: `No duplicates allowed.`,
644 | input: ctx.value
645 | });
646 | }
647 | });
648 | ``` */}
649 |
650 | ## `z.ostring()`, etc dropped
651 |
652 | The undocumented convenience methods `z.ostring()`, `z.onumber()`, etc. have been removed. These were shorthand methods for defining optional string schemas.
653 |
654 | ## `z.literal()`
655 |
656 | ### drops `symbol` support
657 |
658 | Symbols aren't considered literal values, nor can they be simply compared with `===`. This was an oversight in Zod 3.
659 |
660 | ## static `.create()` factories dropped
661 |
662 | Previously all Zod classes defined a static `.create()` method. These are now implemented as standalone factory functions.
663 |
664 | ```ts
665 | z.ZodString.create(); // ❌
666 | ```
667 |
668 | ## `z.record()`
669 |
670 | ### drops single argument usage
671 |
672 | Before, `z.record()` could be used with a single argument. This is no longer supported.
673 |
674 | ```ts
675 | // Zod 3
676 | z.record(z.string()); // ✅
677 |
678 | // Zod 4
679 | z.record(z.string()); // ❌
680 | z.record(z.string(), z.string()); // ✅
681 | ```
682 |
683 | ### improves enum support
684 |
685 | Records have gotten a lot smarter. In Zod 3, passing an enum into `z.record()` as a key schema would result in a partial type
686 |
687 | ```ts
688 | const myRecord = z.record(z.enum(["a", "b", "c"]), z.number());
689 | // { a?: number; b?: number; c?: number; }
690 | ```
691 |
692 | In Zod 4, this is no longer the case. The inferred type is what you'd expect, and Zod ensures exhaustiveness; that is, it makes sure all enum keys exist in the input during parsing.
693 |
694 | ```ts
695 | const myRecord = z.record(z.enum(["a", "b", "c"]), z.number());
696 | // { a: number; b: number; c: number; }
697 | ```
698 |
699 | To replicate the old behavior with optional keys, use `z.partialRecord()`:
700 |
701 | ```ts
702 | const myRecord = z.partialRecord(z.enum(["a", "b", "c"]), z.number());
703 | // { a?: number; b?: number; c?: number; }
704 | ```
705 |
706 | ## `z.intersection()`
707 |
708 | ### throws `Error` on merge conflict
709 |
710 | Zod intersection parses the input against two schemas, then attempts to merge the results. In Zod 3, when the results were unmergable, Zod threw a `ZodError` with a special `"invalid_intersection_types"` issue.
711 |
712 | In Zod 4, this will throw a regular `Error` instead. The existence of unmergable results indicates a structural problem with the schema: an intersection of two incompatible types. Thus, a regular error is more appropriate than a validation error.
713 |
714 | ## Internal changes
715 |
716 | > The typical user of Zod can likely ignore everything below this line. These changes do not impact the user-facing `z` APIs.
717 |
718 | There are too many internal changes to list here, but some may be relevant to regular users who are (intentionally or not) relying on certain implementation details. These changes will be of particular interest to library authors building tools on top of Zod.
719 |
720 | ### updates generics
721 |
722 | The generic structure of several classes has changed. Perhaps most significant is the change to the `ZodType` base class:
723 |
724 | ```ts
725 | // Zod 3
726 | class ZodType<Output, Def extends z.ZodTypeDef, Input = Output> {
727 | // ...
728 | }
729 |
730 | // Zod 4
731 | class ZodType<Output = unknown, Input = unknown> {
732 | // ...
733 | }
734 | ```
735 |
736 | The second generic `Def` has been entirely removed. Instead the base class now only tracks `Output` and `Input`. While previously the `Input` value defaulted to `Output`, it now defaults to `unknown`. This allows generic functions involving `z.ZodType` to behave more intuitively in many cases.
737 |
738 | ```ts
739 | function inferSchema<T extends z.ZodType>(schema: T): T {
740 | return schema;
741 | };
742 |
743 | inferSchema(z.string()); // z.ZodString
744 | ```
745 |
746 | The need for `z.ZodTypeAny` has been eliminated; just use `z.ZodType` instead.
747 |
748 | ### adds `z.core`
749 |
750 | Many utility functions and types have been moved to the new `zod/v4/core` sub-package, to facilitate code sharing between Zod and Zod Mini.
751 |
752 | ```ts
753 | import * as z from "zod/v4/core";
754 |
755 | function handleError(iss: z.$ZodError) {
756 | // do stuff
757 | }
758 | ```
759 |
760 | For convenience, the contents of `zod/v4/core` are also re-exported from `zod` and `zod/mini` under the `z.core` namespace.
761 |
762 | ```ts
763 | import * as z from "zod";
764 |
765 | function handleError(iss: z.core.$ZodError) {
766 | // do stuff
767 | }
768 | ```
769 |
770 | Refer to the [Zod Core](/packages/core) docs for more information on the contents of the core sub-library.
771 |
772 | ### moves `._def`
773 |
774 | The `._def` property is now moved to `._zod.def`. The structure of all internal defs is subject to change; this is relevant to library authors but won't be comprehensively documented here.
775 |
776 | ### drops `ZodEffects`
777 |
778 | This doesn't affect the user-facing APIs, but it's an internal change worth highlighting. It's part of a larger restructure of how Zod handles *refinements*.
779 |
780 | Previously both refinements and transformations lived inside a wrapper class called `ZodEffects`. That means adding either one to a schema would wrap the original schema in a `ZodEffects` instance. In Zod 4, refinements now live inside the schemas themselves. More accurately, each schema contains an array of "checks"; the concept of a "check" is new in Zod 4 and generalizes the concept of a refinement to include potentially side-effectful transforms like `z.toLowerCase()`.
781 |
782 | This is particularly apparent in the Zod Mini API, which heavily relies on the `.check()` method to compose various validations together.
783 |
784 | ```ts
785 | import * as z from "zod/mini";
786 |
787 | z.string().check(
788 | z.minLength(10),
789 | z.maxLength(100),
790 | z.toLowerCase(),
791 | z.trim(),
792 | );
793 | ```
794 |
795 | ### adds `ZodTransform`
796 |
797 | Meanwhile, transforms have been moved into a dedicated `ZodTransform` class. This schema class represents an input transform; in fact, you can actually define standalone transformations now:
798 |
799 | ```ts
800 | import * as z from "zod";
801 |
802 | const schema = z.transform(input => String(input));
803 |
804 | schema.parse(12); // => "12"
805 | ```
806 |
807 | This is primarily used in conjunction with `ZodPipe`. The `.transform()` method now returns an instance of `ZodPipe`.
808 |
809 | ```ts
810 | z.string().transform(val => val); // ZodPipe<ZodString, ZodTransform>
811 | ```
812 |
813 | ### drops `ZodPreprocess`
814 |
815 | As with `.transform()`, the `z.preprocess()` function now returns a `ZodPipe` instance instead of a dedicated `ZodPreprocess` instance.
816 |
817 | ```ts
818 | z.preprocess(val => val, z.string()); // ZodPipe<ZodTransform, ZodString>
819 | ```
820 |
821 | ### drops `ZodBranded`
822 |
823 | Branding is now handled with a direct modification to the inferred type, instead of a dedicated `ZodBranded` class. The user-facing APIs remain the same.
824 |
825 | {/* - Dropping support for ES5
826 | - Zod relies on `Set` internally */}
827 |
828 | {/* - `z.keyof` now returns `ZodEnum` instead of `ZodLiteral` */}
829 |
830 | {/* ## Changed: `.refine()`
831 |
832 | The `.refine()` method used to accept a function as the second argument.
833 |
834 | ```ts
835 | // no longer supported
836 | const longString = z.string().refine(
837 | (val) => val.length > 10,
838 | (val) => ({ message: `${val} is not more than 10 characters` })
839 | );
840 | ```
841 |
842 | This can be better represented with the new `error` parameter, so this overload has been removed.
843 |
844 | ```ts
845 | const longString = z.string().refine((val) => val.length > 10, {
846 | error: (issue) => `${issue.input} is not more than 10 characters`,
847 | });
848 | ``
849 | */}
850 |
851 | {/*
852 | - No support for `null` or `undefined` in `z.literal`
853 | - `z.literal(null)`
854 | - `z.literal(undefined)`
855 | - this was never documented */}
856 |
857 | {/* - Array min/max/length checks now run after parsing. This means they won't run if the parse has already aborted. */}
858 |
859 | {/* - Drops single-argument `z.record()` */}
860 |
861 | {/* - Smarter `z.record`: no longer Partial by default */}
862 |
863 | {/* - Intersection merge errors are now thrown as Error not ZodError
864 | - These usually do not reflect a parse error but a structural problem with the schema */}
865 |
866 | {/* - Consolidates `unknownKeys` and `catchall` in ZodObject */}
867 |
868 | {/* - Dropping
869 | - `ZodBranded`: purely a static-domain annotation
870 | - `ZodFunction` */}
871 |
872 | {/* - The `description` is now stored in `z.defaultRegistry`, not the def
873 | - No support for `description` in factory params
874 | - Descriptions do not cascade in `.optional()`, etc */}
875 |
876 | {/* - Enums:
877 | - ZodEnum and ZodNativeEnum are merged
878 | - `.Values` and `.Enum` are removed. Use `.enum` instead.
879 | - `.options` is removed */}
880 |
```
--------------------------------------------------------------------------------
/docs/dev/MANUAL_TESTING.md:
--------------------------------------------------------------------------------
```markdown
1 | # XcodeBuildMCP Manual Testing Guidelines
2 |
3 | This document provides comprehensive guidelines for manual black-box testing of XcodeBuildMCP using Reloaderoo inspect commands. This is the authoritative guide for validating all tools through the Model Context Protocol interface.
4 |
5 | ## Table of Contents
6 |
7 | 1. [Testing Philosophy](#testing-philosophy)
8 | 2. [Black Box Testing via Reloaderoo](#black-box-testing-via-reloaderoo)
9 | 3. [Testing Psychology & Bias Prevention](#testing-psychology--bias-prevention)
10 | 4. [Tool Dependency Graph Testing Strategy](#tool-dependency-graph-testing-strategy)
11 | 5. [Prerequisites](#prerequisites)
12 | 6. [Step-by-Step Testing Process](#step-by-step-testing-process)
13 | 7. [Error Testing](#error-testing)
14 | 8. [Testing Report Generation](#testing-report-generation)
15 | 9. [Troubleshooting](#troubleshooting)
16 |
17 | ## Testing Philosophy
18 |
19 | ### 🚨 CRITICAL: THOROUGHNESS OVER EFFICIENCY - NO SHORTCUTS ALLOWED
20 |
21 | **ABSOLUTE PRINCIPLE: EVERY TOOL MUST BE TESTED INDIVIDUALLY**
22 |
23 | **🚨 MANDATORY TESTING SCOPE - NO EXCEPTIONS:**
24 | - **EVERY SINGLE TOOL** - All tools must be tested individually, one by one
25 | - **NO REPRESENTATIVE SAMPLING** - Testing similar tools does NOT validate other tools
26 | - **NO PATTERN RECOGNITION SHORTCUTS** - Similar-looking tools may have different behaviors
27 | - **NO EFFICIENCY OPTIMIZATIONS** - Thoroughness is more important than speed
28 | - **NO TIME CONSTRAINTS** - This is a long-running task with no deadline pressure
29 |
30 | **❌ FORBIDDEN EFFICIENCY SHORTCUTS:**
31 | - **NEVER** assume testing `build_sim_id_proj` validates `build_sim_name_proj`
32 | - **NEVER** skip tools because they "look similar" to tested ones
33 | - **NEVER** use representative sampling instead of complete coverage
34 | - **NEVER** stop testing due to time concerns or perceived redundancy
35 | - **NEVER** group tools together for batch testing
36 | - **NEVER** make assumptions about untested tools based on tested patterns
37 |
38 | **✅ REQUIRED COMPREHENSIVE APPROACH:**
39 | 1. **Individual Tool Testing**: Each tool gets its own dedicated test execution
40 | 2. **Complete Documentation**: Every tool result must be recorded, regardless of outcome
41 | 3. **Systematic Progress**: Use TodoWrite to track every single tool as tested/untested
42 | 4. **Failure Documentation**: Test tools that cannot work and mark them as failed/blocked
43 | 5. **No Assumptions**: Treat each tool as potentially unique requiring individual validation
44 |
45 | **TESTING COMPLETENESS VALIDATION:**
46 | - **Start Count**: Record exact number of tools discovered using `npm run tools`
47 | - **End Count**: Verify same number of tools have been individually tested
48 | - **Missing Tools = Testing Failure**: If any tools remain untested, the testing is incomplete
49 | - **TodoWrite Tracking**: Every tool must appear in todo list and be marked completed
50 |
51 | ## Black Box Testing via Reloaderoo
52 |
53 | ### 🚨 CRITICAL: Black Box Testing via Reloaderoo Inspect
54 |
55 | **DEFINITION: Black Box Testing**
56 | Black Box Testing means testing ONLY through external interfaces without any knowledge of internal implementation. For XcodeBuildMCP, this means testing exclusively through the Model Context Protocol (MCP) interface using Reloaderoo as the MCP client.
57 |
58 | **🚨 MANDATORY: RELOADEROO INSPECT IS THE ONLY ALLOWED TESTING METHOD**
59 |
60 | **ABSOLUTE TESTING RULES - NO EXCEPTIONS:**
61 |
62 | 1. **✅ ONLY ALLOWED: Reloaderoo Inspect Commands**
63 | - `npx reloaderoo@latest inspect call-tool "TOOL_NAME" --params 'JSON' -- node build/index.js`
64 | - `npx reloaderoo@latest inspect list-tools -- node build/index.js`
65 | - `npx reloaderoo@latest inspect read-resource "URI" -- node build/index.js`
66 | - `npx reloaderoo@latest inspect server-info -- node build/index.js`
67 | - `npx reloaderoo@latest inspect ping -- node build/index.js`
68 |
69 | 2. **❌ COMPLETELY FORBIDDEN ACTIONS:**
70 | - **NEVER** call `mcp__XcodeBuildMCP__tool_name()` functions directly
71 | - **NEVER** use MCP server tools as if they were native functions
72 | - **NEVER** access internal server functionality
73 | - **NEVER** read source code to understand how tools work
74 | - **NEVER** examine implementation files during testing
75 | - **NEVER** diagnose internal server issues or registration problems
76 | - **NEVER** suggest code fixes or implementation changes
77 |
78 | 3. **🚨 CRITICAL VIOLATION EXAMPLES:**
79 | ```typescript
80 | // ❌ FORBIDDEN - Direct MCP tool calls
81 | await mcp__XcodeBuildMCP__list_devices();
82 | await mcp__XcodeBuildMCP__build_sim_id_proj({ ... });
83 |
84 | // ❌ FORBIDDEN - Using tools as native functions
85 | const devices = await list_devices();
86 | const result = await doctor();
87 |
88 | // ✅ CORRECT - Only through Reloaderoo inspect
89 | npx reloaderoo@latest inspect call-tool "list_devices" --params '{}' -- node build/index.js
90 | npx reloaderoo@latest inspect call-tool "doctor" --params '{}' -- node build/index.js
91 | ```
92 |
93 | **WHY RELOADEROO INSPECT IS MANDATORY:**
94 | - **Higher Fidelity**: Provides clear input/output visibility for each tool call
95 | - **Real-world Simulation**: Tests exactly how MCP clients interact with the server
96 | - **Interface Validation**: Ensures MCP protocol compliance and proper JSON formatting
97 | - **Black Box Enforcement**: Prevents accidental access to internal implementation details
98 | - **Clean State**: Each tool call runs with a fresh MCP server instance, preventing cross-contamination
99 |
100 | **IMPORTANT: STATEFUL TOOL LIMITATIONS**
101 |
102 | **Reloaderoo Inspect Behavior:**
103 | Reloaderoo starts a fresh MCP server instance for each individual tool call and terminates it immediately after the response. This ensures:
104 | - ✅ **Clean Testing Environment**: No state contamination between tool calls
105 | - ✅ **Isolated Testing**: Each tool test is independent and repeatable
106 | - ✅ **Real-world Accuracy**: Simulates how most MCP clients interact with servers
107 |
108 | **Expected False Negatives:**
109 | Some tools rely on in-memory state within the MCP server and will fail when tested via Reloaderoo inspect. These failures are **expected and acceptable** as false negatives:
110 |
111 | - **`swift_package_stop`** - Requires in-memory process tracking from `swift_package_run`
112 | - **`stop_app_device`** - Requires in-memory process tracking from `launch_app_device`
113 | - **`stop_app_sim`** - Requires in-memory process tracking from `launch_app_sim`
114 | - **`stop_device_log_cap`** - Requires in-memory session tracking from `start_device_log_cap`
115 | - **`stop_sim_log_cap`** - Requires in-memory session tracking from `start_sim_log_cap`
116 | - **`stop_mac_app`** - Requires in-memory process tracking from `launch_mac_app`
117 |
118 | **Testing Protocol for Stateful Tools:**
119 | 1. **Test the tool anyway** - Execute the Reloaderoo inspect command
120 | 2. **Expect failure** - Tool will likely fail due to missing state
121 | 3. **Mark as false negative** - Document the failure as expected due to stateful limitations
122 | 4. **Continue testing** - Do not attempt to fix or investigate the failure
123 | 5. **Report as finding** - Note in testing report that stateful tools failed as expected
124 |
125 | **COMPLETE COVERAGE REQUIREMENTS:**
126 | - ✅ **Test ALL tools individually** - No exceptions, every tool gets manual verification
127 | - ✅ **Follow dependency graphs** - Test tools in correct order based on data dependencies
128 | - ✅ **Capture key outputs** - Record UUIDs, paths, schemes needed by dependent tools
129 | - ✅ **Test real workflows** - Complete end-to-end workflows from discovery to execution
130 | - ✅ **Use tool-summary.js script** - Accurate tool/resource counting and discovery
131 | - ✅ **Document all observations** - Record exactly what you see via testing
132 | - ✅ **Report discrepancies as findings** - Note unexpected results without investigation
133 |
134 | **MANDATORY INDIVIDUAL TOOL TESTING PROTOCOL:**
135 |
136 | **Step 1: Create Complete Tool Inventory**
137 | ```bash
138 | # Use the official tool summary script to get accurate tool count and list
139 | npm run tools > /tmp/summary_output.txt
140 | TOTAL_TOOLS=$(grep "Tools:" /tmp/summary_output.txt | awk '{print $2}')
141 | echo "TOTAL TOOLS TO TEST: $TOTAL_TOOLS"
142 |
143 | # Generate detailed tool list and extract tool names
144 | npm run tools:list > /tmp/tools_detailed.txt
145 | grep "^ • " /tmp/tools_detailed.txt | sed 's/^ • //' > /tmp/tool_names.txt
146 | ```
147 |
148 | **Step 2: Create TodoWrite Task List for Every Tool**
149 | ```bash
150 | # Create individual todo items for each tool discovered
151 | # Use the actual tool count from step 1
152 | # Example for first few tools:
153 | # 1. [ ] Test tool: doctor
154 | # 2. [ ] Test tool: list_devices
155 | # 3. [ ] Test tool: list_sims
156 | # ... (continue for ALL $TOTAL_TOOLS tools)
157 | ```
158 |
159 | **Step 3: Test Each Tool Individually**
160 | For EVERY tool in the list:
161 | ```bash
162 | # Test each tool individually - NO BATCHING
163 | npx reloaderoo@latest inspect call-tool "TOOL_NAME" --params 'APPROPRIATE_PARAMS' -- node build/index.js
164 |
165 | # Mark tool as completed in TodoWrite IMMEDIATELY after testing
166 | # Record result (success/failure/blocked) for each tool
167 | ```
168 |
169 | **Step 4: Validate Complete Coverage**
170 | ```bash
171 | # Verify all tools tested
172 | COMPLETED_TOOLS=$(count completed todo items)
173 | if [ $COMPLETED_TOOLS -ne $TOTAL_TOOLS ]; then
174 | echo "ERROR: Testing incomplete. $COMPLETED_TOOLS/$TOTAL_TOOLS tested"
175 | exit 1
176 | fi
177 | ```
178 |
179 | **CRITICAL: NO TOOL LEFT UNTESTED**
180 | - **Every tool name from the JSON list must be individually tested**
181 | - **Every tool must have a TodoWrite entry that gets marked completed**
182 | - **Tools that fail due to missing parameters should be tested anyway and marked as blocked**
183 | - **Tools that require setup (like running processes) should be tested and documented as requiring dependencies**
184 | - **NO ASSUMPTIONS**: Test tools even if they seem redundant or similar to others
185 |
186 | **BLACK BOX TESTING ENFORCEMENT:**
187 | - ✅ **Test only through Reloaderoo MCP interface** - Simulates real-world MCP client usage
188 | - ✅ **Use task lists** - Track progress with TodoWrite tool for every single tool
189 | - ✅ **Tick off each tool** - Mark completed in task list after manual verification
190 | - ✅ **Manual oversight** - Human verification of each tool's input and output
191 | - ❌ **Never examine source code** - No reading implementation files during testing
192 | - ❌ **Never diagnose internal issues** - No investigation of build processes or tool registration
193 | - ❌ **Never suggest implementation fixes** - Report issues as findings, don't solve them
194 | - ❌ **Never use scripts for tool testing** - Each tool must be manually executed and verified
195 |
196 | ## Testing Psychology & Bias Prevention
197 |
198 | **COMMON ANTI-PATTERNS TO AVOID:**
199 |
200 | **1. Efficiency Bias (FORBIDDEN)**
201 | - **Symptom**: "These tools look similar, I'll test one to validate the others"
202 | - **Correction**: Every tool is unique and must be tested individually
203 | - **Enforcement**: Count tools at start, verify same count tested at end
204 |
205 | **2. Pattern Recognition Override (FORBIDDEN)**
206 | - **Symptom**: "I see the pattern, the rest will work the same way"
207 | - **Correction**: Patterns may hide edge cases, bugs, or different implementations
208 | - **Enforcement**: No assumptions allowed, test every tool regardless of apparent similarity
209 |
210 | **3. Time Pressure Shortcuts (FORBIDDEN)**
211 | - **Symptom**: "This is taking too long, let me speed up by sampling"
212 | - **Correction**: This is explicitly a long-running task with no time constraints
213 | - **Enforcement**: Thoroughness is the ONLY priority, efficiency is irrelevant
214 |
215 | **4. False Confidence (FORBIDDEN)**
216 | - **Symptom**: "The architecture is solid, so all tools must work"
217 | - **Correction**: Architecture validation does not guarantee individual tool functionality
218 | - **Enforcement**: Test tools to discover actual issues, not to confirm assumptions
219 |
220 | **MANDATORY MINDSET:**
221 | - **Every tool is potentially broken** until individually tested
222 | - **Every tool may have unique edge cases** not covered by similar tools
223 | - **Every tool deserves individual attention** regardless of apparent redundancy
224 | - **Testing completion means EVERY tool tested**, not "enough tools to validate patterns"
225 | - **The goal is discovering problems**, not confirming everything works
226 |
227 | **TESTING COMPLETENESS CHECKLIST:**
228 | - [ ] Generated complete tool list using `npm run tools:list`
229 | - [ ] Created TodoWrite entry for every single tool
230 | - [ ] Tested every tool individually via Reloaderoo inspect
231 | - [ ] Marked every tool as completed in TodoWrite
232 | - [ ] Verified tool count: tested_count == total_count
233 | - [ ] Documented all results, including failures and blocked tools
234 | - [ ] Created final report covering ALL tools, not just successful ones
235 |
236 | ## Tool Dependency Graph Testing Strategy
237 |
238 | **CRITICAL: Tools must be tested in dependency order:**
239 |
240 | 1. **Foundation Tools** (provide data for other tools):
241 | - `doctor` - System info
242 | - `list_devices` - Device UUIDs
243 | - `list_sims` - Simulator UUIDs
244 | - `discover_projs` - Project/workspace paths
245 |
246 | 2. **Discovery Tools** (provide metadata for build tools):
247 | - `list_schemes` - Scheme names
248 | - `show_build_settings` - Build settings
249 |
250 | 3. **Build Tools** (create artifacts for install tools):
251 | - `build_*` tools - Create app bundles
252 | - `get_*_app_path_*` tools - Locate built app bundles
253 | - `get_*_bundle_id` tools - Extract bundle IDs
254 |
255 | 4. **Installation Tools** (depend on built artifacts):
256 | - `install_app_*` tools - Install built apps
257 | - `launch_app_*` tools - Launch installed apps
258 |
259 | 5. **Testing Tools** (depend on projects/schemes):
260 | - `test_*` tools - Run test suites
261 |
262 | 6. **UI Automation Tools** (depend on running apps):
263 | - `describe_ui`, `screenshot`, `tap`, etc.
264 |
265 | **MANDATORY: Record Key Outputs**
266 |
267 | Must capture and document these values for dependent tools:
268 | - **Device UUIDs** from `list_devices`
269 | - **Simulator UUIDs** from `list_sims`
270 | - **Project/workspace paths** from `discover_projs`
271 | - **Scheme names** from `list_schems_*`
272 | - **App bundle paths** from `get_*_app_path_*`
273 | - **Bundle IDs** from `get_*_bundle_id`
274 |
275 | ## Prerequisites
276 |
277 | 1. **Build the server**: `npm run build`
278 | 2. **Install jq**: `brew install jq` (required for JSON parsing)
279 | 3. **System Requirements**: macOS with Xcode installed, connected devices/simulators optional
280 | 4. **AXe video capture (optional)**: run `npm run bundle:axe` before using `record_sim_video` in local tests (not required for unit tests)
281 |
282 | ## Step-by-Step Testing Process
283 |
284 | **Note**: All tool and resource discovery now uses the official `tool-summary.js` script (available as `npm run tools`, `npm run tools:list`, and `npm run tools:all`) instead of direct reloaderoo calls. This ensures accurate counts and lists without hardcoded values.
285 |
286 | ### Step 1: Programmatic Discovery and Official Testing Lists
287 |
288 | #### Generate Official Tool and Resource Lists using tool-summary.js
289 |
290 | ```bash
291 | # Use the official tool summary script to get accurate counts and lists
292 | npm run tools > /tmp/summary_output.txt
293 |
294 | # Extract tool and resource counts from summary
295 | TOOL_COUNT=$(grep "Tools:" /tmp/summary_output.txt | awk '{print $2}')
296 | RESOURCE_COUNT=$(grep "Resources:" /tmp/summary_output.txt | awk '{print $2}')
297 | echo "Official tool count: $TOOL_COUNT"
298 | echo "Official resource count: $RESOURCE_COUNT"
299 |
300 | # Generate detailed tool list for testing checklist
301 | npm run tools:list > /tmp/tools_detailed.txt
302 |
303 | # Extract tool names from the detailed output
304 | grep "^ • " /tmp/tools_detailed.txt | sed 's/^ • //' > /tmp/tool_names.txt
305 | echo "Tool names saved to /tmp/tool_names.txt"
306 |
307 | # Generate detailed resource list for testing checklist
308 | npm run tools:all > /tmp/tools_and_resources.txt
309 |
310 | # Extract resource URIs from the detailed output
311 | sed -n '/📚 Available Resources:/,/✅ Tool summary complete!/p' /tmp/tools_and_resources.txt | grep "^ • " | sed 's/^ • //' | cut -d' ' -f1 > /tmp/resource_uris.txt
312 | echo "Resource URIs saved to /tmp/resource_uris.txt"
313 | ```
314 |
315 | #### Create Tool Testing Checklist
316 |
317 | ```bash
318 | # Generate markdown checklist from actual tool list
319 | echo "# Official Tool Testing Checklist" > /tmp/tool_testing_checklist.md
320 | echo "" >> /tmp/tool_testing_checklist.md
321 | echo "Total Tools: $TOOL_COUNT" >> /tmp/tool_testing_checklist.md
322 | echo "" >> /tmp/tool_testing_checklist.md
323 |
324 | # Add each tool as unchecked item
325 | while IFS= read -r tool_name; do
326 | echo "- [ ] $tool_name" >> /tmp/tool_testing_checklist.md
327 | done < /tmp/tool_names.txt
328 |
329 | echo "Tool testing checklist created at /tmp/tool_testing_checklist.md"
330 | ```
331 |
332 | #### Create Resource Testing Checklist
333 |
334 | ```bash
335 | # Generate markdown checklist from actual resource list
336 | echo "# Official Resource Testing Checklist" > /tmp/resource_testing_checklist.md
337 | echo "" >> /tmp/resource_testing_checklist.md
338 | echo "Total Resources: $RESOURCE_COUNT" >> /tmp/resource_testing_checklist.md
339 | echo "" >> /tmp/resource_testing_checklist.md
340 |
341 | # Add each resource as unchecked item
342 | while IFS= read -r resource_uri; do
343 | echo "- [ ] $resource_uri" >> /tmp/resource_testing_checklist.md
344 | done < /tmp/resource_uris.txt
345 |
346 | echo "Resource testing checklist created at /tmp/resource_testing_checklist.md"
347 | ```
348 |
349 | ### Step 2: Tool Schema Discovery for Parameter Testing
350 |
351 | #### Extract Tool Schema Information
352 |
353 | ```bash
354 | # Get schema for specific tool to understand required parameters
355 | TOOL_NAME="list_devices"
356 | jq --arg tool "$TOOL_NAME" '.tools[] | select(.name == $tool) | .inputSchema' /tmp/tools.json
357 |
358 | # Get tool description for usage guidance
359 | jq --arg tool "$TOOL_NAME" '.tools[] | select(.name == $tool) | .description' /tmp/tools.json
360 |
361 | # Generate parameter template for tool testing
362 | jq --arg tool "$TOOL_NAME" '.tools[] | select(.name == $tool) | .inputSchema.properties // {}' /tmp/tools.json
363 | ```
364 |
365 | #### Batch Schema Extraction
366 |
367 | ```bash
368 | # Create schema reference file for all tools
369 | echo "# Tool Schema Reference" > /tmp/tool_schemas.md
370 | echo "" >> /tmp/tool_schemas.md
371 |
372 | while IFS= read -r tool_name; do
373 | echo "## $tool_name" >> /tmp/tool_schemas.md
374 | echo "" >> /tmp/tool_schemas.md
375 |
376 | # Get description
377 | description=$(jq -r --arg tool "$tool_name" '.tools[] | select(.name == $tool) | .description' /tmp/tools.json)
378 | echo "**Description:** $description" >> /tmp/tool_schemas.md
379 | echo "" >> /tmp/tool_schemas.md
380 |
381 | # Get required parameters
382 | required=$(jq -r --arg tool "$tool_name" '.tools[] | select(.name == $tool) | .inputSchema.required // [] | join(", ")' /tmp/tools.json)
383 | if [ "$required" != "" ]; then
384 | echo "**Required Parameters:** $required" >> /tmp/tool_schemas.md
385 | else
386 | echo "**Required Parameters:** None" >> /tmp/tool_schemas.md
387 | fi
388 | echo "" >> /tmp/tool_schemas.md
389 |
390 | # Get all parameters
391 | echo "**All Parameters:**" >> /tmp/tool_schemas.md
392 | jq --arg tool "$tool_name" '.tools[] | select(.name == $tool) | .inputSchema.properties // {} | keys[]' /tmp/tools.json | while read param; do
393 | echo "- $param" >> /tmp/tool_schemas.md
394 | done
395 | echo "" >> /tmp/tool_schemas.md
396 |
397 | done < /tmp/tool_names.txt
398 |
399 | echo "Tool schema reference created at /tmp/tool_schemas.md"
400 | ```
401 |
402 | ### Step 3: Manual Tool-by-Tool Testing
403 |
404 | #### 🚨 CRITICAL: STEP-BY-STEP BLACK BOX TESTING PROCESS
405 |
406 | **ABSOLUTE RULE: ALL TESTING MUST BE DONE MANUALLY, ONE TOOL AT A TIME USING RELOADEROO INSPECT**
407 |
408 | **SYSTEMATIC TESTING PROCESS:**
409 |
410 | 1. **Create TodoWrite Task List**
411 | - Add all tools (from `npm run tools` count) to task list before starting
412 | - Mark each tool as "pending" initially
413 | - Update status to "in_progress" when testing begins
414 | - Mark "completed" only after manual verification
415 |
416 | 2. **Test Each Tool Individually**
417 | - Execute ONLY via `npx reloaderoo@latest inspect call-tool "TOOL_NAME" --params 'JSON' -- node build/index.js`
418 | - Wait for complete response before proceeding to next tool
419 | - Read and verify each tool's output manually
420 | - Record key outputs (UUIDs, paths, schemes) for dependent tools
421 |
422 | 3. **Manual Verification Requirements**
423 | - ✅ **Read each response** - Manually verify tool output makes sense
424 | - ✅ **Check for errors** - Identify any tool failures or unexpected responses
425 | - ✅ **Record UUIDs/paths** - Save outputs needed for dependent tools
426 | - ✅ **Update task list** - Mark each tool complete after verification
427 | - ✅ **Document issues** - Record any problems found during testing
428 |
429 | 4. **FORBIDDEN SHORTCUTS:**
430 | - ❌ **NO SCRIPTS** - Scripts hide what's happening and prevent proper verification
431 | - ❌ **NO AUTOMATION** - Every tool call must be manually executed and verified
432 | - ❌ **NO BATCHING** - Cannot test multiple tools simultaneously
433 | - ❌ **NO MCP DIRECT CALLS** - Only Reloaderoo inspect commands allowed
434 |
435 | #### Phase 1: Infrastructure Validation
436 |
437 | **Manual Commands (execute individually):**
438 |
439 | ```bash
440 | # Test server connectivity
441 | npx reloaderoo@latest inspect ping -- node build/index.js
442 |
443 | # Get server information
444 | npx reloaderoo@latest inspect server-info -- node build/index.js
445 |
446 | # Verify tool count manually
447 | npx reloaderoo@latest inspect list-tools -- node build/index.js 2>/dev/null | jq '.tools | length'
448 |
449 | # Verify resource count manually
450 | npx reloaderoo@latest inspect list-resources -- node build/index.js 2>/dev/null | jq '.resources | length'
451 | ```
452 |
453 | #### Phase 2: Resource Testing
454 |
455 | ```bash
456 | # Test each resource systematically
457 | while IFS= read -r resource_uri; do
458 | echo "Testing resource: $resource_uri"
459 | npx reloaderoo@latest inspect read-resource "$resource_uri" -- node build/index.js 2>/dev/null
460 | echo "---"
461 | done < /tmp/resource_uris.txt
462 | ```
463 |
464 | #### Phase 3: Foundation Tools (Data Collection)
465 |
466 | **CRITICAL: Capture ALL key outputs for dependent tools**
467 |
468 | ```bash
469 | echo "=== FOUNDATION TOOL TESTING & DATA COLLECTION ==="
470 |
471 | # 1. Test doctor (no dependencies)
472 | echo "Testing doctor..."
473 | npx reloaderoo@latest inspect call-tool "doctor" --params '{}' -- node build/index.js 2>/dev/null
474 |
475 | # 2. Collect device data
476 | echo "Collecting device UUIDs..."
477 | npx reloaderoo@latest inspect call-tool "list_devices" --params '{}' -- node build/index.js 2>/dev/null > /tmp/devices_output.json
478 | DEVICE_UUIDS=$(jq -r '.content[0].text' /tmp/devices_output.json | grep -E "UDID: [A-F0-9-]+" | sed 's/.*UDID: //' | head -2)
479 | echo "Device UUIDs captured: $DEVICE_UUIDS"
480 |
481 | # 3. Collect simulator data
482 | echo "Collecting simulator UUIDs..."
483 | npx reloaderoo@latest inspect call-tool "list_sims" --params '{}' -- node build/index.js 2>/dev/null > /tmp/sims_output.json
484 | SIMULATOR_UUIDS=$(jq -r '.content[0].text' /tmp/sims_output.json | grep -E "\([A-F0-9-]+\)" | sed 's/.*(\([A-F0-9-]*\)).*/\1/' | head -3)
485 | echo "Simulator UUIDs captured: $SIMULATOR_UUIDS"
486 |
487 | # 4. Collect project data
488 | echo "Collecting project paths..."
489 | npx reloaderoo@latest inspect call-tool "discover_projs" --params '{"workspaceRoot": "/Volumes/Developer/XcodeBuildMCP"}' -- node build/index.js 2>/dev/null > /tmp/projects_output.json
490 | PROJECT_PATHS=$(jq -r '.content[1].text' /tmp/projects_output.json | grep -E "\.xcodeproj$" | sed 's/.*- //' | head -3)
491 | WORKSPACE_PATHS=$(jq -r '.content[2].text' /tmp/projects_output.json | grep -E "\.xcworkspace$" | sed 's/.*- //' | head -2)
492 | echo "Project paths captured: $PROJECT_PATHS"
493 | echo "Workspace paths captured: $WORKSPACE_PATHS"
494 |
495 | # Save key data for dependent tools
496 | echo "$DEVICE_UUIDS" > /tmp/device_uuids.txt
497 | echo "$SIMULATOR_UUIDS" > /tmp/simulator_uuids.txt
498 | echo "$PROJECT_PATHS" > /tmp/project_paths.txt
499 | echo "$WORKSPACE_PATHS" > /tmp/workspace_paths.txt
500 | ```
501 |
502 | #### Phase 4: Discovery Tools (Metadata Collection)
503 |
504 | ```bash
505 | echo "=== DISCOVERY TOOL TESTING & METADATA COLLECTION ==="
506 |
507 | # Collect schemes for each project
508 | while IFS= read -r project_path; do
509 | if [ -n "$project_path" ]; then
510 | echo "Getting schemes for: $project_path"
511 | npx reloaderoo@latest inspect call-tool "list_schems_proj" --params "{\"projectPath\": \"$project_path\"}" -- node build/index.js 2>/dev/null > /tmp/schemes_$$.json
512 | SCHEMES=$(jq -r '.content[1].text' /tmp/schemes_$$.json 2>/dev/null || echo "NoScheme")
513 | echo "$project_path|$SCHEMES" >> /tmp/project_schemes.txt
514 | echo "Schemes captured for $project_path: $SCHEMES"
515 | fi
516 | done < /tmp/project_paths.txt
517 |
518 | # Collect schemes for each workspace
519 | while IFS= read -r workspace_path; do
520 | if [ -n "$workspace_path" ]; then
521 | echo "Getting schemes for: $workspace_path"
522 | npx reloaderoo@latest inspect call-tool "list_schemes" --params "{\"workspacePath\": \"$workspace_path\"}" -- node build/index.js 2>/dev/null > /tmp/ws_schemes_$$.json
523 | SCHEMES=$(jq -r '.content[1].text' /tmp/ws_schemes_$$.json 2>/dev/null || echo "NoScheme")
524 | echo "$workspace_path|$SCHEMES" >> /tmp/workspace_schemes.txt
525 | echo "Schemes captured for $workspace_path: $SCHEMES"
526 | fi
527 | done < /tmp/workspace_paths.txt
528 | ```
529 |
530 | #### Phase 5: Manual Individual Tool Testing (All Tools)
531 |
532 | **CRITICAL: Test every single tool manually, one at a time**
533 |
534 | **Manual Testing Process:**
535 |
536 | 1. **Create task list** with TodoWrite tool for all tools (using count from `npm run tools`)
537 | 2. **Test each tool individually** with proper parameters
538 | 3. **Mark each tool complete** in task list after manual verification
539 | 4. **Record results** and observations for each tool
540 | 5. **NO SCRIPTS** - Each command executed manually
541 |
542 | **STEP-BY-STEP MANUAL TESTING COMMANDS:**
543 |
544 | ```bash
545 | # STEP 1: Test foundation tools (no parameters required)
546 | # Execute each command individually, wait for response, verify manually
547 | npx reloaderoo@latest inspect call-tool "doctor" --params '{}' -- node build/index.js
548 | # [Wait for response, read output, mark tool complete in task list]
549 |
550 | npx reloaderoo@latest inspect call-tool "list_devices" --params '{}' -- node build/index.js
551 | # [Record device UUIDs from response for dependent tools]
552 |
553 | npx reloaderoo@latest inspect call-tool "list_sims" --params '{}' -- node build/index.js
554 | # [Record simulator UUIDs from response for dependent tools]
555 |
556 | # STEP 2: Test project discovery (use discovered project paths)
557 | npx reloaderoo@latest inspect call-tool "list_schems_proj" --params '{"projectPath": "/actual/path/from/discover_projs.xcodeproj"}' -- node build/index.js
558 | # [Record scheme names from response for build tools]
559 |
560 | # STEP 3: Test workspace tools (use discovered workspace paths)
561 | npx reloaderoo@latest inspect call-tool "list_schemes" --params '{"workspacePath": "/actual/path/from/discover_projs.xcworkspace"}' -- node build/index.js
562 | # [Record scheme names from response for build tools]
563 |
564 | # STEP 4: Test simulator tools (use captured simulator UUIDs from step 1)
565 | npx reloaderoo@latest inspect call-tool "boot_sim" --params '{"simulatorUuid": "ACTUAL_UUID_FROM_LIST_SIMS"}' -- node build/index.js
566 | # [Verify simulator boots successfully]
567 |
568 | # STEP 5: Test build tools (requires project + scheme + simulator from previous steps)
569 | npx reloaderoo@latest inspect call-tool "build_sim_id_proj" --params '{"projectPath": "/actual/project.xcodeproj", "scheme": "ActualSchemeName", "simulatorId": "ACTUAL_SIMULATOR_UUID"}' -- node build/index.js
570 | # [Verify build succeeds and record app bundle path]
571 | ```
572 |
573 | **CRITICAL: EACH COMMAND MUST BE:**
574 | 1. **Executed individually** - One command at a time, manually typed or pasted
575 | 2. **Verified manually** - Read the complete response before continuing
576 | 3. **Tracked in task list** - Mark tool complete only after verification
577 | 4. **Use real data** - Replace placeholder values with actual captured data
578 | 5. **Wait for completion** - Allow each command to finish before proceeding
579 |
580 | ### TESTING VIOLATIONS AND ENFORCEMENT
581 |
582 | **🚨 CRITICAL VIOLATIONS THAT WILL TERMINATE TESTING:**
583 |
584 | 1. **Direct MCP Tool Usage Violation:**
585 | ```typescript
586 | // ❌ IMMEDIATE TERMINATION - Using MCP tools directly
587 | await mcp__XcodeBuildMCP__list_devices();
588 | const result = await list_sims();
589 | ```
590 |
591 | 2. **Script-Based Testing Violation:**
592 | ```bash
593 | # ❌ IMMEDIATE TERMINATION - Using scripts to test tools
594 | for tool in $(cat tool_list.txt); do
595 | npx reloaderoo inspect call-tool "$tool" --params '{}' -- node build/index.js
596 | done
597 | ```
598 |
599 | 3. **Batching/Automation Violation:**
600 | ```bash
601 | # ❌ IMMEDIATE TERMINATION - Testing multiple tools simultaneously
602 | npx reloaderoo inspect call-tool "list_devices" & npx reloaderoo inspect call-tool "list_sims" &
603 | ```
604 |
605 | 4. **Source Code Examination Violation:**
606 | ```typescript
607 | // ❌ IMMEDIATE TERMINATION - Reading implementation during testing
608 | const toolImplementation = await Read('/src/mcp/tools/device-shared/list_devices.ts');
609 | ```
610 |
611 | **ENFORCEMENT PROCEDURE:**
612 | 1. **First Violation**: Immediate correction and restart of testing process
613 | 2. **Documentation Update**: Add explicit prohibition to prevent future violations
614 | 3. **Method Validation**: Ensure all future testing uses only Reloaderoo inspect commands
615 | 4. **Progress Reset**: Restart testing from foundation tools if direct MCP usage detected
616 |
617 | **VALID TESTING SEQUENCE EXAMPLE:**
618 | ```bash
619 | # ✅ CORRECT - Step-by-step manual execution via Reloaderoo
620 | # Tool 1: Test doctor
621 | npx reloaderoo@latest inspect call-tool "doctor" --params '{}' -- node build/index.js
622 | # [Read response, verify, mark complete in TodoWrite]
623 |
624 | # Tool 2: Test list_devices
625 | npx reloaderoo@latest inspect call-tool "list_devices" --params '{}' -- node build/index.js
626 | # [Read response, capture UUIDs, mark complete in TodoWrite]
627 |
628 | # Tool 3: Test list_sims
629 | npx reloaderoo@latest inspect call-tool "list_sims" --params '{}' -- node build/index.js
630 | # [Read response, capture UUIDs, mark complete in TodoWrite]
631 |
632 | # Tool X: Test stateful tool (expected to fail)
633 | npx reloaderoo@latest inspect call-tool "swift_package_stop" --params '{"pid": 12345}' -- node build/index.js
634 | # [Tool fails as expected - no in-memory state available]
635 | # [Mark as "false negative - stateful tool limitation" in TodoWrite]
636 | # [Continue to next tool without investigation]
637 |
638 | # Continue individually for all tools (use count from npm run tools)...
639 | ```
640 |
641 | **HANDLING STATEFUL TOOL FAILURES:**
642 | ```bash
643 | # ✅ CORRECT Response to Expected Stateful Tool Failure
644 | # Tool fails with "No process found" or similar state-related error
645 | # Response: Mark tool as "tested - false negative (stateful)" in task list
646 | # Do NOT attempt to diagnose, fix, or investigate the failure
647 | # Continue immediately to next tool in sequence
648 | ```
649 |
650 | ## Error Testing
651 |
652 | ```bash
653 | # Test error handling systematically
654 | echo "=== Error Testing ==="
655 |
656 | # Test with invalid JSON parameters
657 | echo "Testing invalid parameter types..."
658 | npx reloaderoo@latest inspect call-tool list_schems_proj --params '{"projectPath": 123}' -- node build/index.js 2>/dev/null
659 |
660 | # Test with non-existent paths
661 | echo "Testing non-existent paths..."
662 | npx reloaderoo@latest inspect call-tool list_schems_proj --params '{"projectPath": "/nonexistent/path.xcodeproj"}' -- node build/index.js 2>/dev/null
663 |
664 | # Test with invalid UUIDs
665 | echo "Testing invalid UUIDs..."
666 | npx reloaderoo@latest inspect call-tool boot_sim --params '{"simulatorUuid": "invalid-uuid"}' -- node build/index.js 2>/dev/null
667 | ```
668 |
669 | ## Testing Report Generation
670 |
671 | ```bash
672 | # Create comprehensive testing session report
673 | cat > TESTING_SESSION_$(date +%Y-%m-%d).md << EOF
674 | # Manual Testing Session - $(date +%Y-%m-%d)
675 |
676 | ## Environment
677 | - macOS Version: $(sw_vers -productVersion)
678 | - XcodeBuildMCP Version: $(jq -r '.version' package.json 2>/dev/null || echo "unknown")
679 | - Testing Method: Reloaderoo @latest via npx
680 |
681 | ## Official Counts (Programmatically Verified)
682 | - Total Tools: $TOOL_COUNT
683 | - Total Resources: $RESOURCE_COUNT
684 |
685 | ## Test Results
686 | [Document test results here]
687 |
688 | ## Issues Found
689 | [Document any discrepancies or failures]
690 |
691 | ## Performance Notes
692 | [Document response times and performance observations]
693 | EOF
694 |
695 | echo "Testing session template created: TESTING_SESSION_$(date +%Y-%m-%d).md"
696 | ```
697 |
698 | ### Key Commands Reference
699 |
700 | ```bash
701 | # Essential testing commands
702 | npx reloaderoo@latest inspect ping -- node build/index.js
703 | npx reloaderoo@latest inspect server-info -- node build/index.js
704 | npx reloaderoo@latest inspect list-tools -- node build/index.js | jq '.tools | length'
705 | npx reloaderoo@latest inspect list-resources -- node build/index.js | jq '.resources | length'
706 | npx reloaderoo@latest inspect call-tool TOOL_NAME --params '{}' -- node build/index.js
707 | npx reloaderoo@latest inspect read-resource "xcodebuildmcp://RESOURCE" -- node build/index.js
708 |
709 | # Schema extraction
710 | jq --arg tool "TOOL_NAME" '.tools[] | select(.name == $tool) | .inputSchema' /tmp/tools.json
711 | jq --arg tool "TOOL_NAME" '.tools[] | select(.name == $tool) | .description' /tmp/tools.json
712 | ```
713 |
714 | ## Troubleshooting
715 |
716 | ### Common Issues
717 |
718 | #### 1. Reloaderoo Command Timeouts
719 | **Symptoms**: Commands hang or timeout after extended periods
720 | **Cause**: Server startup issues or MCP protocol communication problems
721 | **Resolution**:
722 | - Verify server builds successfully: `npm run build`
723 | - Test direct server startup: `node build/index.js`
724 | - Check for TypeScript compilation errors
725 |
726 | #### 2. Tool Parameter Validation Errors
727 | **Symptoms**: Tools return parameter validation errors
728 | **Cause**: Missing or incorrect required parameters
729 | **Resolution**:
730 | - Check tool schema: `jq --arg tool "TOOL_NAME" '.tools[] | select(.name == $tool) | .inputSchema' /tmp/tools.json`
731 | - Verify parameter types and required fields
732 | - Use captured dependency data (UUIDs, paths, schemes)
733 |
734 | #### 3. "No Such Tool" Errors
735 | **Symptoms**: Reloaderoo reports tool not found
736 | **Cause**: Tool name mismatch or server registration issues
737 | **Resolution**:
738 | - Verify tool exists in list: `npx reloaderoo@latest inspect list-tools -- node build/index.js | jq '.tools[].name'`
739 | - Check exact tool name spelling and case sensitivity
740 | - Ensure server built successfully
741 |
742 | #### 4. Empty or Malformed Responses
743 | **Symptoms**: Tools return empty responses or JSON parsing errors
744 | **Cause**: Tool implementation issues or server errors
745 | **Resolution**:
746 | - Document as testing finding - do not investigate implementation
747 | - Mark tool as "failed - empty response" in task list
748 | - Continue with next tool in sequence
749 |
750 | This systematic approach ensures comprehensive, accurate testing using programmatic discovery and validation of all XcodeBuildMCP functionality through the MCP interface exclusively.
751 |
```